mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 03:51:54 +01:00
test(evals): isolate private device authorization browser steps
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
30f854b0bc
commit
beca0672fb
6 files changed
+159
-21
No files matched your search
@@ -1,13 +1,15 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
|
||||
import { pollUntil } from "../api.js";
|
||||
import { definition, externalObservation, observeExternal, checks, verifyRenderedReply } from "./external-support.js";
|
||||
import { waitForReply } from "./identity-support.js";
|
||||
import { oauthPost, mcp, origin, resource, type Tokens } from "../public-mcp-client.js";
|
||||
|
||||
import { privateBrowserEntry } from "./external-private-browser.js";
|
||||
import { matchesDevicePrivateReceipt } from "./external-device-private-contract.js";
|
||||
|
||||
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
||||
interface DeviceCode { device_code: string; user_code: string; verification_uri: string; verification_uri_complete: string; expires_in: number; interval: number }
|
||||
interface DeviceFixture { clientId: string; otherClientId: string; primary: DeviceCode; expiring: DeviceCode; expiresAt: number; polledAt: number; tokens?: Tokens }
|
||||
interface DeviceFixture { personId: string; clientId: string; otherClientId: string; primary: DeviceCode; expiring: DeviceCode; expiresAt: number; polledAt: number; tokens?: Tokens }
|
||||
const fixtures = new WeakMap<ChatCatalogContext, DeviceFixture>();
|
||||
async function requestDevice(clientId: string, companyId: string): Promise<DeviceCode> {
|
||||
const response = await oauthPost("device_authorization", { client_id: clientId, resource, company_id: companyId, scope: "paperclip:read offline_access" });
|
||||
@@ -28,6 +30,8 @@ export function gradeDeviceLogin(value: unknown) {
|
||||
c.add("correct-human-verification", o.state.verificationPath === "/mcp-device" && o.state.currentCodeVisible === true && o.state.companyConsentVisible === true && o.state.readScopeVisible === true
|
||||
&& o.state.humanApproved === true && o.state.clientCompanyId === o.companyId && o.state.clientReadOnly === true,
|
||||
"The person follows the issuer's actual page/current code and authorizes exactly the intended company and read scope.");
|
||||
c.add("private-device-ui", matchesDevicePrivateReceipt("device-approve", o.state.approvalUi) && matchesDevicePrivateReceipt("device-expired", o.state.expiredUi),
|
||||
"Both actual device UI phases use the isolated finite browser with closed receipts, outside Test reporting.");
|
||||
c.add("pending-without-human-approval", o.state.pendingError === "authorization_pending", "The waiting CLI cannot obtain credentials before human approval.");
|
||||
c.add("resource-and-client-mismatch-denied", o.state.wrongResourceDenied === true && o.state.wrongClientDenied === true && o.state.correctExchangeSucceeded === true,
|
||||
"Mismatched resource/client requests fail without consuming the valid selected grant.");
|
||||
@@ -44,25 +48,33 @@ export const assistantDeviceExternalCases = [definition("assistant-connections.d
|
||||
const setup = await ctx.input.api.get<{ enabled: boolean; serverUrl: string }>("/api/mcp/setup");
|
||||
if (!setup.enabled || setup.serverUrl !== resource) throw new Error("Setup blocker: this disposable instance has no compatible public MCP issuer/resource");
|
||||
const clientId = await registerClient(), otherClientId = await registerClient();
|
||||
const primary = await requestDevice(clientId, ctx.input.fixtures.company.id), expiring = await requestDevice(clientId, ctx.input.fixtures.company.id);
|
||||
const primary = await requestDevice(clientId, ctx.input.fixtures.company.id);
|
||||
ctx.input.privateSecrets.push(primary.device_code, primary.user_code, primary.verification_uri_complete);
|
||||
const expiring = await requestDevice(clientId, ctx.input.fixtures.company.id);
|
||||
ctx.input.privateSecrets.push(expiring.device_code, expiring.user_code, expiring.verification_uri_complete);
|
||||
if (primary.expires_in !== 600 || expiring.expires_in !== 600 || primary.verification_uri !== `${origin}/mcp-device`) throw new Error("Declared bounded device-code lifetime or verification origin differs from the real issuer");
|
||||
ctx.input.privateSecrets.push(primary.device_code, primary.user_code, expiring.device_code, expiring.user_code);
|
||||
const session = await ctx.input.api.get<{ user: { id: string } }>("/api/auth/get-session");
|
||||
if (!session.user?.id) throw new Error("Device UI requires the authenticated fixture person");
|
||||
const expiresAt = Date.now() + expiring.expires_in * 1000;
|
||||
const fixture = { clientId, otherClientId, primary, expiring, expiresAt, polledAt: Date.now() }; fixtures.set(ctx, fixture);
|
||||
const fixture = { personId: session.user.id, clientId, otherClientId, primary, expiring, expiresAt, polledAt: Date.now() }; fixtures.set(ctx, fixture);
|
||||
const pending = await exchange(fixture, primary); const pendingBody = await pending.json() as { error?: string };
|
||||
return { verificationPath: new URL(primary.verification_uri).pathname, pendingError: pendingBody.error, expirationWaitMs: 600_000 };
|
||||
}, gradeDeviceLogin, {
|
||||
authentication: "human-session", attemptTimeoutMs: 780_000,
|
||||
sourceFiles: ["chat-catalog-domains/external-assistant-device.ts", "chat-catalog-domains/external-support.ts", "chat-catalog-domains/internal-support.ts", "chat-catalog-domains/identity-support.ts", "public-mcp-client.ts", "chat-scenarios/assistant-connections.yaml"],
|
||||
sourceFiles: ["chat-catalog-domains/external-assistant-device.ts", "chat-catalog-domains/external-private-browser.ts", "chat-catalog-domains/external-private-browser-worker.ts", "chat-catalog-domains/external-device-private-contract.ts", "../../ui/src/pages/McpConnect.tsx", "../../server/src/routes/public-mcp.ts", "../../server/src/services/public-mcp/oauth.ts", "../../packages/shared/src/public-mcp.ts", "chat-catalog-domains/external-support.ts", "chat-catalog-domains/internal-support.ts", "chat-catalog-domains/identity-support.ts", "public-mcp-client.ts", "chat-scenarios/assistant-connections.yaml"],
|
||||
async act(ctx, state) {
|
||||
await waitForReply(ctx); const fixture = fixtures.get(ctx); if (!fixture) throw new Error("Device fixture missing");
|
||||
const source = ctx.input.page.url(); await ctx.input.page.goto(fixture.primary.verification_uri_complete);
|
||||
await expect(ctx.input.page.getByRole("heading", { name: /^Connect .+ to Paperclip$/ })).toBeVisible();
|
||||
await expect(ctx.input.page.getByText(fixture.primary.user_code.toUpperCase(), { exact: true })).toBeVisible(); state.currentCodeVisible = true;
|
||||
await expect(ctx.input.page.getByText(ctx.input.fixtures.company.name, { exact: true })).toBeVisible(); state.companyConsentVisible = true;
|
||||
await expect(ctx.input.page.getByText("Read all of your Paperclip data", { exact: true })).toBeVisible(); state.readScopeVisible = true;
|
||||
await ctx.input.page.getByRole("button", { name: "Connect organization", exact: true }).click();
|
||||
await expect(ctx.input.page.getByText(/connected|approved/i).first()).toBeVisible(); state.humanApproved = true;
|
||||
const approvalUi = await privateBrowserEntry({
|
||||
mode: "device-approve", baseURL: origin, setupURL: fixture.primary.verification_uri_complete,
|
||||
providerURL: origin, credential: fixture.primary.user_code, companyId: ctx.input.fixtures.company.id,
|
||||
device: { userId: fixture.personId, companyName: ctx.input.fixtures.company.name },
|
||||
cookies: await ctx.input.page.context().cookies(origin),
|
||||
});
|
||||
state.approvalUi = approvalUi;
|
||||
state.currentCodeVisible = approvalUi.currentCodeVisible;
|
||||
state.companyConsentVisible = approvalUi.companyConsentVisible;
|
||||
state.readScopeVisible = approvalUi.readScopeVisible;
|
||||
state.humanApproved = approvalUi.humanApproved;
|
||||
const wrongResource = await exchange(fixture, fixture.primary, { resource: origin + "/mcp/not-the-resource" }); state.wrongResourceDenied = wrongResource.status === 400; await wrongResource.body?.cancel();
|
||||
const wrongClient = await exchange(fixture, fixture.primary, { client_id: fixture.otherClientId }); state.wrongClientDenied = wrongClient.status === 400; await wrongClient.body?.cancel();
|
||||
if (Date.now() < fixture.polledAt + fixture.primary.interval * 1000) await pollUntil({ label: "advertised device polling interval", deadlineAt: Date.now() + 15_000, intervalMs: 200, load: async () => Date.now(), accept: now => now >= fixture.polledAt + fixture.primary.interval * 1000 });
|
||||
@@ -81,9 +93,14 @@ export const assistantDeviceExternalCases = [definition("assistant-connections.d
|
||||
state.elapsedBeforeExpiryProbe = Date.now() - (fixture.expiresAt - 600_000);
|
||||
const expired = await exchange(fixture, fixture.expiring); const body = await expired.json() as { error?: string };
|
||||
state.expiredCodeDenied = expired.status === 400 && body.error === "expired_token";
|
||||
await ctx.input.page.goto(fixture.expiring.verification_uri_complete);
|
||||
await expect(ctx.input.page.getByText(/expired|invalid/i).first()).toBeVisible(); state.expiredPageVisible = true;
|
||||
await ctx.input.page.goto(source, { waitUntil: "domcontentloaded" });
|
||||
const expiredUi = await privateBrowserEntry({
|
||||
mode: "device-expired", baseURL: origin, setupURL: fixture.expiring.verification_uri_complete,
|
||||
providerURL: origin, credential: fixture.expiring.user_code, companyId: ctx.input.fixtures.company.id,
|
||||
device: { userId: fixture.personId, companyName: ctx.input.fixtures.company.name },
|
||||
cookies: await ctx.input.page.context().cookies(origin),
|
||||
});
|
||||
state.expiredUi = expiredUi;
|
||||
state.expiredPageVisible = expiredUi.expiredPageVisible;
|
||||
},
|
||||
async observe(ctx, state) { return observeExternal(ctx, state); },
|
||||
async verifyResult(ctx, _state, value) { await verifyRenderedReply(ctx, value); },
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { matchesDevicePrivateReceipt, validDevicePrivateInput } from "./external-device-private-contract.js";
|
||||
|
||||
const base = "http://127.0.0.1:3100";
|
||||
const code = "ABCD-EFGH"; // Synthetic calibration only, never a provider-issued code.
|
||||
const url = `${base}/mcp-device?user_code=${code}`;
|
||||
const company = "11111111-1111-4111-8111-111111111111";
|
||||
const device = { userId: "fixture-person", companyName: "Owned company" };
|
||||
|
||||
it("allows only the exact owned device page and current code for two finite phases", () => {
|
||||
for (const mode of ["device-approve", "device-expired"]) {
|
||||
expect(validDevicePrivateInput(mode, base, url, company, device, code)).toBe(true);
|
||||
for (const target of [
|
||||
url.replace("127.0.0.1", "example.test"), url.replace("http:", "https:"),
|
||||
url.replace("/mcp-device", "/mcp-connect"), url + "&next=/other", url + "#secret",
|
||||
url.replace(code, "WXYZ-1234"), url + `&user_code=${code}`,
|
||||
url.replace("http://", "http://user:password@"),
|
||||
]) expect(validDevicePrivateInput(mode, base, target, company, device, code)).toBe(false);
|
||||
}
|
||||
expect(validDevicePrivateInput("arbitrary", base, url, company, device, code)).toBe(false);
|
||||
expect(validDevicePrivateInput("device-approve", base, url, company, undefined, code)).toBe(false);
|
||||
expect(validDevicePrivateInput("device-approve", base, url, "wrong", device, code)).toBe(false);
|
||||
expect(validDevicePrivateInput("device-approve", base, url, company, { ...device, userId: "" }, code)).toBe(false);
|
||||
expect(validDevicePrivateInput("device-approve", base, url, company, device, "bad\r\ncode")).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts only complete boolean-only private receipts, never codes, URLs or forged capture claims", () => {
|
||||
const boundary = { processIsolation: true, testRunnerAttached: false, tracingEnabled: false, videoEnabled: false, apiStepRecorder: false, deviceSessionVerified: true };
|
||||
const approval = { ...boundary, currentCodeVisible: true, companyConsentVisible: true, readScopeVisible: true, humanApproved: true };
|
||||
const expired = { ...boundary, expiredPageVisible: true };
|
||||
expect(matchesDevicePrivateReceipt("device-approve", approval)).toBe(true);
|
||||
expect(matchesDevicePrivateReceipt("device-expired", expired)).toBe(true);
|
||||
for (const wrong of [undefined, {}, expired, { ...approval, tracingEnabled: true }, { ...approval, deviceSessionVerified: false }, { ...approval, currentCodeVisible: false }, { ...approval, userCode: code }, { ...approval, verificationUri: url }, { ...approval, humanApproved: "true" }])
|
||||
expect(matchesDevicePrivateReceipt("device-approve", wrong)).toBe(false);
|
||||
expect(matchesDevicePrivateReceipt("device-expired", { ...expired, expiredPageVisible: false })).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
export interface DevicePrivateInput {
|
||||
userId: string;
|
||||
companyName: string;
|
||||
}
|
||||
|
||||
/** Only the owned issuer's exact device-code UI is allowed, never arbitrary
|
||||
* navigation. The current code and complete URL remain private worker input. */
|
||||
export function validDevicePrivateInput(
|
||||
mode: string,
|
||||
baseURL: string,
|
||||
setupURL: string,
|
||||
companyId: unknown,
|
||||
device: DevicePrivateInput | undefined,
|
||||
userCode: string,
|
||||
) {
|
||||
try {
|
||||
const base = new URL(baseURL), setup = new URL(setupURL);
|
||||
return ["device-approve", "device-expired"].includes(mode)
|
||||
&& base.protocol === "http:" && base.hostname === "127.0.0.1"
|
||||
&& !base.username && !base.password && base.pathname === "/" && !base.search && !base.hash
|
||||
&& setup.origin === base.origin && !setup.username && !setup.password
|
||||
&& setup.pathname === "/mcp-device" && !setup.hash
|
||||
&& setup.searchParams.size === 1 && setup.searchParams.get("user_code") === userCode
|
||||
&& /^[A-Za-z0-9 -]{8,12}$/.test(userCode)
|
||||
&& typeof companyId === "string" && /^[a-f0-9-]{36}$/i.test(companyId)
|
||||
&& !!device && typeof device.userId === "string" && device.userId.length > 0 && device.userId.length <= 256
|
||||
&& typeof device.companyName === "string" && device.companyName.trim().length > 0 && device.companyName.length <= 256
|
||||
&& !/[\r\n]/.test(device.userId + device.companyName);
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
export function matchesDevicePrivateReceipt(mode: string, value: unknown) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const receipt = value as Record<string, unknown>;
|
||||
const expected = {
|
||||
processIsolation: true, testRunnerAttached: false, tracingEnabled: false,
|
||||
videoEnabled: false, apiStepRecorder: false, deviceSessionVerified: true,
|
||||
...(mode === "device-approve" ? {
|
||||
currentCodeVisible: true, companyConsentVisible: true, readScopeVisible: true,
|
||||
humanApproved: true,
|
||||
} : mode === "device-expired" ? { expiredPageVisible: true } : {}),
|
||||
};
|
||||
return ["device-approve", "device-expired"].includes(mode)
|
||||
&& Object.keys(receipt).length === Object.keys(expected).length
|
||||
&& Object.entries(expected).every(([key, expectedValue]) => receipt[key] === expectedValue);
|
||||
}
|
||||
@@ -1,23 +1,57 @@
|
||||
import { validDevicePrivateInput, type DevicePrivateInput } from "./external-device-private-contract.js";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, test, type Cookie } from "@playwright/test";
|
||||
import { validGrokRoutinePrivateInput, matchesConfiguredGrokRoutine, type GrokRoutinePrivateInput } from "./external-grok-private-contract.js";
|
||||
import { validSlackIdentityInput, validSlackWizardIdentityInput, matchingSlackIdentityPreview, type SlackIdentityPrivateInput } from "./external-slack-identity-contract.js";
|
||||
import { validSlackSetupPrivate, type SlackSetupPrivateInput } from "./external-slack-setup-private-contract.js";
|
||||
interface Request { mode: "secure-key" | "canary" | "gateway-token-rotation" | "company-secret-create" | "personal-secret-create" | "grok-routine-configure" | "slack-identity-confirm" | "slack-endpoint-configure" | "slack-wizard-confirm" | "slack-wizard-resume" | "slack-endpoint-reconnect"; slackSetup?:SlackSetupPrivateInput; slackIdentity?:SlackIdentityPrivateInput; grok?:GrokRoutinePrivateInput; companyId?:string; personal?:{definitionId:string;userId:string}; gateway?: { id: string; oldTokenId: string; otherToken: string; endpointPath: string; readName: string }; baseURL: string; setupURL: string; providerURL: string; credential: string; cookies: Cookie[]; executablePath?: string; channel?: string }
|
||||
interface Request { mode: "secure-key" | "canary" | "gateway-token-rotation" | "company-secret-create" | "personal-secret-create" | "grok-routine-configure" | "slack-identity-confirm" | "slack-endpoint-configure" | "slack-wizard-confirm" | "slack-wizard-resume" | "slack-endpoint-reconnect" | "device-approve" | "device-expired"; device?: DevicePrivateInput; slackSetup?:SlackSetupPrivateInput; slackIdentity?:SlackIdentityPrivateInput; grok?:GrokRoutinePrivateInput; companyId?:string; personal?:{definitionId:string;userId:string}; gateway?: { id: string; oldTokenId: string; otherToken: string; endpointPath: string; readName: string }; baseURL: string; setupURL: string; providerURL: string; credential: string; cookies: Cookie[]; executablePath?: string; channel?: string }
|
||||
/** Separate Node process: no Playwright Test worker, API-step listener or artifact
|
||||
* recorder exists. This module never enables tracing, video or screenshots. */
|
||||
async function main() {
|
||||
let attached = false; try { test.info(); attached = true; } catch { /* expected outside Test */ }
|
||||
if (attached) throw new Error("Private browser worker cannot run inside Playwright Test");
|
||||
const input = JSON.parse(await readFile(process.argv[2]!, "utf8")) as Request;
|
||||
if (!input.credential || input.credential.length > (input.mode === "grok-routine-configure" ? 16384 : 8192) || !["secure-key", "canary", "gateway-token-rotation", "company-secret-create", "personal-secret-create", "grok-routine-configure", "slack-identity-confirm", "slack-endpoint-configure", "slack-wizard-confirm", "slack-wizard-resume", "slack-endpoint-reconnect"].includes(input.mode)) throw new Error("Invalid private browser request");
|
||||
if (!input.credential || input.credential.length > (input.mode === "grok-routine-configure" ? 16384 : 8192) || !["secure-key", "canary", "gateway-token-rotation", "company-secret-create", "personal-secret-create", "grok-routine-configure", "slack-identity-confirm", "slack-endpoint-configure", "slack-wizard-confirm", "slack-wizard-resume", "slack-endpoint-reconnect", "device-approve", "device-expired"].includes(input.mode)) throw new Error("Invalid private browser request");
|
||||
const base = new URL(input.baseURL);
|
||||
if (base.hostname !== "127.0.0.1" || base.protocol !== "http:" || new URL(input.setupURL).origin !== base.origin) throw new Error("Private browser target is outside the disposable loopback instance");
|
||||
const browser = await chromium.launch({ headless: true, ...(input.executablePath ? { executablePath: input.executablePath } : {}), ...(input.channel ? { channel: input.channel } : {}) });
|
||||
try {
|
||||
const context = await browser.newContext({ baseURL: base.origin });
|
||||
await context.addCookies(input.cookies); const page = await context.newPage(); page.setDefaultTimeout(30_000);
|
||||
if (input.mode === "device-approve" || input.mode === "device-expired") {
|
||||
if (!validDevicePrivateInput(input.mode, input.baseURL, input.setupURL, input.companyId, input.device, input.credential) || !input.device) throw new Error("Invalid finite device verification request");
|
||||
const session = await context.request.get("/api/auth/get-session");
|
||||
if (!session.ok() || (await session.json()).user?.id !== input.device.userId) throw new Error("Device UI requires the same signed-in fixture person");
|
||||
const description = page.waitForResponse(response => {
|
||||
const url = new URL(response.url());
|
||||
return url.origin === base.origin && url.pathname === "/api/mcp/device" && url.searchParams.get("user_code") === input.credential && response.request().method() === "GET";
|
||||
});
|
||||
await page.goto(input.setupURL, { waitUntil: "domcontentloaded" });
|
||||
const described = await description;
|
||||
if (input.mode === "device-expired") {
|
||||
const body = await described.json();
|
||||
if (described.status() !== 404 || body.error !== "invalid_request") throw new Error("Expired device UI did not receive the actual issuer rejection");
|
||||
await page.getByText(/expired|invalid/i).first().waitFor();
|
||||
if (await page.getByRole("button", { name: "Connect organization", exact: true }).isVisible()) throw new Error("Expired device still offers approval");
|
||||
return { processIsolation: true, testRunnerAttached: false, tracingEnabled: false, videoEnabled: false, apiStepRecorder: false, deviceSessionVerified: true, expiredPageVisible: true };
|
||||
}
|
||||
if (!described.ok()) throw new Error("Device consent description unavailable");
|
||||
const request = await described.json();
|
||||
if (request.clientName !== "Paperclip disposable CLI" || request.requestedCompanyId !== input.companyId || request.requiresSignIn !== false || request.requestedWrite !== false || request.requestedConfigure !== false || request.agentConnection === true
|
||||
|| !request.companies?.some((company: { id: string; name: string }) => company.id === input.companyId && company.name === input.device!.companyName)) throw new Error("Device consent does not match the exact owned read-only request");
|
||||
await page.getByRole("heading", { name: "Connect Paperclip disposable CLI to Paperclip", exact: true }).waitFor();
|
||||
await page.getByText(input.credential.toUpperCase(), { exact: true }).waitFor();
|
||||
await page.getByText(input.device.companyName, { exact: true }).waitFor();
|
||||
await page.getByText("Read all of your Paperclip data", { exact: true }).waitFor();
|
||||
if (await page.getByText("Write all of your Paperclip data", { exact: true }).isVisible()) throw new Error("Device read-only request unexpectedly offers write authority");
|
||||
const approval = page.waitForResponse(response => new URL(response.url()).origin === base.origin && new URL(response.url()).pathname === "/api/mcp/device/consent" && response.request().method() === "POST");
|
||||
await page.getByRole("button", { name: "Connect organization", exact: true }).click();
|
||||
const approved = await approval, submitted = approved.request().postDataJSON();
|
||||
if (!approved.ok() || (await approved.json()).status !== "approved" || submitted.userCode !== input.credential || submitted.companyId !== input.companyId || submitted.decision !== "approve" || submitted.allowWrites !== false || submitted.allowConfiguration !== false) throw new Error("Actual device UI did not approve exactly the selected read-only company/code");
|
||||
await page.getByRole("heading", { name: "Access approved", exact: true }).waitFor();
|
||||
return { processIsolation: true, testRunnerAttached: false, tracingEnabled: false, videoEnabled: false, apiStepRecorder: false, deviceSessionVerified: true, currentCodeVisible: true, companyConsentVisible: true, readScopeVisible: true, humanApproved: true };
|
||||
}
|
||||
if (input.mode === "canary") {
|
||||
await page.setContent('<input type="password" aria-label="App key" autocomplete="off">');
|
||||
const field = page.getByLabel("App key"); await field.fill(input.credential);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { validDevicePrivateInput, matchesDevicePrivateReceipt, type DevicePrivateInput } from "./external-device-private-contract.js";
|
||||
import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -8,8 +9,9 @@ import { chromium, type Cookie } from "@playwright/test";
|
||||
import type { GrokRoutinePrivateInput } from "./external-grok-private-contract.js";
|
||||
import type { SlackIdentityPrivateInput } from "./external-slack-identity-contract.js";
|
||||
import type { SlackSetupPrivateInput } from "./external-slack-setup-private-contract.js";
|
||||
export interface PrivateBrowserReceipt { processIsolation: true; testRunnerAttached: false; tracingEnabled: false; videoEnabled: false; apiStepRecorder: false; credentialEntered?: boolean; connectionId?: string; challengeVisible?: boolean; passwordInput?: boolean; autocompleteOff?: boolean; humanSubmittedSecurely?: boolean; visibleReady?: boolean; storageClean?: boolean; newTokenId?: string; tokenUiVisible?: boolean; newWorkedBeforeRevocation?: boolean; oldWorkedUntilRevocation?: boolean; oldRevoked?: boolean; newWorksAfter?: boolean; oldStatusAfter?: number; otherWorksAfter?: boolean; secretId?:string; secretUiVisible?:boolean; bindingId?:string; routineConfigured?:boolean; routineUiVisible?:boolean; identityConfirmed?:boolean; identityUiVisible?:boolean; principalId?:string; paperclipUserId?:string; endpointId?:string; slackCredentialUiVisible?:boolean; activationUiVisible?:boolean; endpointActive?:boolean; identityWasAlreadyLinked?:boolean; credentialUiRejected?:boolean }
|
||||
export async function privateBrowserEntry(input: { mode: "secure-key" | "canary" | "gateway-token-rotation" | "company-secret-create" | "personal-secret-create" | "grok-routine-configure" | "slack-identity-confirm" | "slack-endpoint-configure" | "slack-wizard-confirm" | "slack-wizard-resume" | "slack-endpoint-reconnect"; slackSetup?:SlackSetupPrivateInput; slackIdentity?:SlackIdentityPrivateInput; grok?:GrokRoutinePrivateInput; companyId?:string; personal?:{definitionId:string;userId:string}; gateway?: { id: string; oldTokenId: string; otherToken: string; endpointPath: string; readName: string }; baseURL: string; setupURL: string; providerURL: string; credential: string; cookies: Cookie[] }, onToken?: (token: { id: string; token: string }) => void) {
|
||||
export interface PrivateBrowserReceipt { processIsolation: true; testRunnerAttached: false; tracingEnabled: false; videoEnabled: false; apiStepRecorder: false; credentialEntered?: boolean; connectionId?: string; challengeVisible?: boolean; passwordInput?: boolean; autocompleteOff?: boolean; humanSubmittedSecurely?: boolean; visibleReady?: boolean; storageClean?: boolean; newTokenId?: string; tokenUiVisible?: boolean; newWorkedBeforeRevocation?: boolean; oldWorkedUntilRevocation?: boolean; oldRevoked?: boolean; newWorksAfter?: boolean; oldStatusAfter?: number; otherWorksAfter?: boolean; secretId?:string; secretUiVisible?:boolean; bindingId?:string; routineConfigured?:boolean; routineUiVisible?:boolean; identityConfirmed?:boolean; identityUiVisible?:boolean; principalId?:string; paperclipUserId?:string; endpointId?:string; slackCredentialUiVisible?:boolean; activationUiVisible?:boolean; endpointActive?:boolean; identityWasAlreadyLinked?:boolean; credentialUiRejected?:boolean; deviceSessionVerified?:boolean; currentCodeVisible?:boolean; companyConsentVisible?:boolean; readScopeVisible?:boolean; humanApproved?:boolean; expiredPageVisible?:boolean }
|
||||
export async function privateBrowserEntry(input: { mode: "secure-key" | "canary" | "gateway-token-rotation" | "company-secret-create" | "personal-secret-create" | "grok-routine-configure" | "slack-identity-confirm" | "slack-endpoint-configure" | "slack-wizard-confirm" | "slack-wizard-resume" | "slack-endpoint-reconnect" | "device-approve" | "device-expired"; device?: DevicePrivateInput; slackSetup?:SlackSetupPrivateInput; slackIdentity?:SlackIdentityPrivateInput; grok?:GrokRoutinePrivateInput; companyId?:string; personal?:{definitionId:string;userId:string}; gateway?: { id: string; oldTokenId: string; otherToken: string; endpointPath: string; readName: string }; baseURL: string; setupURL: string; providerURL: string; credential: string; cookies: Cookie[] }, onToken?: (token: { id: string; token: string }) => void) {
|
||||
if (input.mode.startsWith("device-") && !validDevicePrivateInput(input.mode, input.baseURL, input.setupURL, input.companyId, input.device, input.credential)) throw new Error("Invalid finite private device UI operation");
|
||||
const directory = await mkdtemp(path.join(tmpdir(), "paperclip-private-browser-"));
|
||||
try {
|
||||
const request = path.join(directory, "request.json");
|
||||
@@ -24,7 +26,8 @@ export async function privateBrowserEntry(input: { mode: "secure-key" | "canary"
|
||||
} catch { throw new Error("Private browser entry failed; child output and credential-bearing request withheld"); }
|
||||
let receipt: PrivateBrowserReceipt; try { receipt = JSON.parse(output) as PrivateBrowserReceipt; } catch { throw new Error("Private browser returned invalid closed receipt"); }
|
||||
if (receipt.processIsolation !== true || receipt.testRunnerAttached !== false || receipt.tracingEnabled !== false || receipt.videoEnabled !== false || receipt.apiStepRecorder !== false
|
||||
|| Object.keys(receipt).some(key => !["processIsolation", "testRunnerAttached", "tracingEnabled", "videoEnabled", "apiStepRecorder", "credentialEntered", "connectionId", "challengeVisible", "passwordInput", "autocompleteOff", "humanSubmittedSecurely", "visibleReady", "storageClean", "newTokenId", "tokenUiVisible", "newWorkedBeforeRevocation", "oldWorkedUntilRevocation", "oldRevoked", "newWorksAfter", "oldStatusAfter", "otherWorksAfter", "secretId", "secretUiVisible", "bindingId", "routineConfigured", "routineUiVisible", "identityConfirmed", "identityUiVisible", "principalId", "paperclipUserId", "endpointId", "slackCredentialUiVisible", "activationUiVisible", "endpointActive", "identityWasAlreadyLinked", "credentialUiRejected"].includes(key))) throw new Error("Private browser receipt exceeds its closed contract");
|
||||
|| Object.keys(receipt).some(key => !["processIsolation", "testRunnerAttached", "tracingEnabled", "videoEnabled", "apiStepRecorder", "credentialEntered", "connectionId", "challengeVisible", "passwordInput", "autocompleteOff", "humanSubmittedSecurely", "visibleReady", "storageClean", "newTokenId", "tokenUiVisible", "newWorkedBeforeRevocation", "oldWorkedUntilRevocation", "oldRevoked", "newWorksAfter", "oldStatusAfter", "otherWorksAfter", "secretId", "secretUiVisible", "bindingId", "routineConfigured", "routineUiVisible", "identityConfirmed", "identityUiVisible", "principalId", "paperclipUserId", "endpointId", "slackCredentialUiVisible", "activationUiVisible", "endpointActive", "identityWasAlreadyLinked", "credentialUiRejected", "deviceSessionVerified", "currentCodeVisible", "companyConsentVisible", "readScopeVisible", "humanApproved", "expiredPageVisible"].includes(key))) throw new Error("Private browser receipt exceeds its closed contract");
|
||||
if (input.mode.startsWith("device-") && !matchesDevicePrivateReceipt(input.mode, receipt)) throw new Error("Invalid closed device UI receipt");
|
||||
if ((input.mode === "slack-wizard-confirm" || input.mode === "slack-wizard-resume") && (receipt.endpointId !== input.slackIdentity?.endpointId || receipt.principalId !== input.slackIdentity?.principalId || receipt.paperclipUserId !== input.slackIdentity?.userId || receipt.identityConfirmed !== true || receipt.activationUiVisible !== true || receipt.endpointActive !== true)) throw new Error("Invalid closed Slack wizard completion receipt");
|
||||
if ((input.mode === "slack-endpoint-configure" || input.mode === "slack-endpoint-reconnect") && (receipt.endpointId !== input.slackSetup?.endpointId || receipt.slackCredentialUiVisible !== true || (receipt.credentialUiRejected === true ? receipt.humanSubmittedSecurely !== false : receipt.humanSubmittedSecurely !== true || receipt.storageClean !== true))) throw new Error("Invalid closed Slack credential form receipt");
|
||||
if (input.mode === "slack-identity-confirm" && (receipt.identityConfirmed !== true || receipt.identityUiVisible !== true || receipt.principalId !== input.slackIdentity?.principalId || receipt.paperclipUserId !== input.slackIdentity?.userId)) throw new Error("Invalid closed Slack identity confirmation receipt");
|
||||
|
||||
@@ -59,6 +59,8 @@ function valid(id: string) {
|
||||
o.reply = "The launch profile returned Unpredictable launch result.";
|
||||
} else if (id === "assistant-connections.device-login") {
|
||||
Object.assign(o.state, { verificationPath: "/mcp-device", currentCodeVisible: true, companyConsentVisible: true, readScopeVisible: true, humanApproved: true,
|
||||
approvalUi: { processIsolation: true, testRunnerAttached: false, tracingEnabled: false, videoEnabled: false, apiStepRecorder: false, deviceSessionVerified: true, currentCodeVisible: true, companyConsentVisible: true, readScopeVisible: true, humanApproved: true },
|
||||
expiredUi: { processIsolation: true, testRunnerAttached: false, tracingEnabled: false, videoEnabled: false, apiStepRecorder: false, deviceSessionVerified: true, expiredPageVisible: true },
|
||||
clientCompanyId: "company", clientReadOnly: true, pendingError: "authorization_pending", wrongResourceDenied: true, wrongClientDenied: true,
|
||||
correctExchangeSucceeded: true, reusedCodeDenied: true, expirationWaitMs: 600000, elapsedBeforeExpiryProbe: 600501, expiredCodeDenied: true, expiredPageVisible: true });
|
||||
o.reply = "Open the verification page and sign in to approve the CLI device code as yourself.";
|
||||
|
||||
Reference in new issue
Block a user