fix(voice): allow authenticated Cloud callers

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-13 21:10:38 -05:00
1 parent b8ffeb973a
commit c2531c98d9
3 files changed
+98 -1

No files matched your search

@@ -17,6 +17,18 @@ unchanged. See the [live-call push qualification](2026-10-09-speko-pr-qualificat
for the real-provider text-input proof, timings, reproduction and remaining
PSTN/interruption qualification.
## Cloud caller authentication (2026-10-13)
Private voice routes accept both local authenticated sessions and trusted Cloud
user sessions. Cloud callers keep their authenticated user ID and current
company scope. Board API keys, MCP OAuth callers, Cloud control credentials,
and raw Cloud user headers do not create private caller authority.
On a Cloud workspace, verify that the signed-in operator can load call history
and start a browser session. Repeat with a user outside the company and confirm
access is denied. Provider configuration alone does not qualify conversation
or spoken result delivery.
## October task-parity qualification
Updated 2026-10-07: September's browser proof is historical. The current task-parity
@@ -0,0 +1,85 @@
import express from "express";
import request from "supertest";
import type { Db } from "@paperclipai/db";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { errorHandler } from "../middleware/error-handler.js";
import type { VoiceSessionService } from "../services/voice/voice-session-service.js";
import { voiceSessionRoutes } from "./voice-sessions.js";
vi.mock("../services/instance-settings.js", () => ({
instanceSettingsService: () => ({ getExperimental: async () => ({ enableChatConnectors: true }) }),
}));
const companyId = "11111111-1111-4111-8111-111111111111";
const endpointId = "22222222-2222-4222-8222-222222222222";
const idempotencyKey = "33333333-3333-4333-8333-333333333333";
const actor: Express.Request["actor"] = {
type: "board", source: "session", userId: "signed-in-user", companyIds: [companyId],
};
const service = { history: vi.fn(), start: vi.fn() };
function app(currentActor = actor) {
const server = express();
server.use(express.json());
server.use((req, _res, next) => { req.actor = currentActor; next(); });
server.use("/api", voiceSessionRoutes({} as Db, service as unknown as VoiceSessionService));
server.use(errorHandler);
return server;
}
beforeEach(() => {
vi.resetAllMocks();
service.history.mockResolvedValue([]);
service.start.mockResolvedValue({ session: { id: "test-session" } });
});
describe("voice caller authentication", () => {
it.each(["session", "cloud_tenant"] as const)("allows signed-in %s users to read calls and start a session", async (source) => {
const server = app({ ...actor, source });
await request(server).get(`/api/companies/${companyId}/voice-history/${endpointId}`).expect(200, []);
expect(service.history).toHaveBeenCalledWith(companyId, endpointId, { id: actor.userId, authority: "member" });
await request(server).post(`/api/companies/${companyId}/voice-sessions`)
.send({ endpointId, idempotencyKey }).expect(200);
expect(service.start).toHaveBeenCalledWith(expect.objectContaining({ companyId, endpointId, caller: { id: actor.userId, authority: "member" } }));
});
it("preserves authenticated Cloud instance-admin authority", async () => {
await request(app({ ...actor, source: "cloud_tenant", isInstanceAdmin: true }))
.get(`/api/companies/${companyId}/voice-history/${endpointId}`).expect(200);
expect(service.history).toHaveBeenCalledWith(companyId, endpointId, { id: actor.userId, authority: "instance_admin" });
});
it("preserves local trusted callers", async () => {
await request(app({ type: "board", source: "local_implicit" }))
.get(`/api/companies/${companyId}/voice-history/${endpointId}`).expect(200);
expect(service.history).toHaveBeenCalledWith(companyId, endpointId, { id: "local-board", authority: "local_board" });
});
it.each(["board_key", "mcp_oauth", "cloud_control"] as const)("rejects %s credentials as private human callers", async (source) => {
await request(app({ ...actor, source })).get(`/api/companies/${companyId}/voice-history/${endpointId}`).expect(403);
expect(service.history).not.toHaveBeenCalled();
});
it.each(["session", "cloud_tenant"] as const)("requires a user identity for %s callers", async (source) => {
await request(app({ ...actor, source, userId: undefined })).get(`/api/companies/${companyId}/voice-history/${endpointId}`).expect(403);
expect(service.history).not.toHaveBeenCalled();
});
it("retains Cloud company isolation", async () => {
const server = app({ ...actor, source: "cloud_tenant", companyIds: [] });
await request(server).get(`/api/companies/${companyId}/voice-history/${endpointId}`).expect(403);
await request(server).post(`/api/companies/${companyId}/voice-sessions`).send({ endpointId, idempotencyKey }).expect(403);
expect(service.history).not.toHaveBeenCalled();
expect(service.start).not.toHaveBeenCalled();
});
it("does not authenticate raw Cloud headers or agent callers", async () => {
await request(app({ type: "none", source: "none" }))
.get(`/api/companies/${companyId}/voice-history/${endpointId}`)
.set("x-paperclip-cloud-user-id", actor.userId!).expect(403);
await request(app({ type: "agent", source: "agent_key", companyId }))
.post(`/api/companies/${companyId}/voice-sessions`).send({ endpointId, idempotencyKey }).expect(403);
expect(service.history).not.toHaveBeenCalled();
expect(service.start).not.toHaveBeenCalled();
});
});
+1 -1
View File
@@ -13,7 +13,7 @@ import { assertBoard, assertCompanyAccess } from "./authz.js";
function caller(req: Request): VoiceCaller {
assertBoard(req);
if (req.actor.source === "local_implicit") return { id: "local-board", authority: "local_board" };
if (!req.actor.userId || req.actor.source !== "session") throw forbidden("Sign in to start a private voice conversation");
if (!req.actor.userId || !["session", "cloud_tenant"].includes(req.actor.source ?? "")) throw forbidden("Sign in to start a private voice conversation");
return { id: req.actor.userId, authority: req.actor.isInstanceAdmin ? "instance_admin" : "member" };
}
function company(req: Request, requireAccess = true) {