From 0bf6fd74e761d68a4994a3a1cd7eaed28bb64e9f Mon Sep 17 00:00:00 2001 From: Rajeh Taher Date: Tue, 8 Sep 2026 14:15:10 +0300 Subject: [PATCH] fix(sdk): enforce canonical credential grammar --- README.md | 6 +- packages/nextjs/test/nextjs.test.ts | 5 +- packages/typescript/README.md | 7 +- packages/typescript/src/credentials.ts | 52 +++++++------ packages/typescript/test/bridge.test.ts | 15 +++- packages/typescript/test/business.test.ts | 6 +- .../typescript/test/calls-lids-users.test.ts | 6 +- packages/typescript/test/campaigns.test.ts | 6 +- packages/typescript/test/channels.test.ts | 6 +- packages/typescript/test/client.test.ts | 68 ++++++++++++++++- .../test/coverage-reconciliation.test.ts | 10 ++- packages/typescript/test/coverage.test.ts | 16 +++- packages/typescript/test/credentials.test.ts | 73 ++++++++++++++++--- packages/typescript/test/customers.test.ts | 6 +- .../test/developer-operations.test.ts | 16 +++- packages/typescript/test/groups.test.ts | 6 +- packages/typescript/test/labels.test.ts | 6 +- packages/typescript/test/media.test.ts | 6 +- packages/typescript/test/messages.test.ts | 6 +- packages/typescript/test/messaging.test.ts | 6 +- .../test/observation-policies.test.ts | 6 +- packages/typescript/test/package.test.ts | 6 +- .../typescript/test/platform-access.test.ts | 6 +- .../test/platform-automation.test.ts | 6 +- .../typescript/test/platform-response.test.ts | 11 ++- .../test/platform-session-start.test.ts | 6 +- .../test/platform-widget-sessions.test.ts | 6 +- packages/typescript/test/platform.test.ts | 11 ++- packages/typescript/test/presence.test.ts | 6 +- packages/typescript/test/privacy.test.ts | 6 +- packages/typescript/test/profile.test.ts | 6 +- .../typescript/test/quick-replies.test.ts | 6 +- packages/typescript/test/quicklinks.test.ts | 18 ++++- .../typescript/test/support/credentials.ts | 2 + packages/typescript/test/voip.test.ts | 11 ++- 35 files changed, 358 insertions(+), 83 deletions(-) create mode 100644 packages/typescript/test/support/credentials.ts diff --git a/README.md b/README.md index ccd3ebf..d473451 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,11 @@ const project = new Client({ Project tokens require an explicit project ID. The server verifies the initial token-to-project binding. A later attempt to bind that client to another project fails before transport. `Client` also rejects browser client tokens and -the CLI-only `pmfa_ls_` listener credential before transport. +the CLI-only `pmfa_ls_` listener credential before transport. Organization +keys must use the single v1 form `pmfa_` plus 72 unpadded base64url characters; +project tokens must use `pmfa_pt_` plus 94. The SDK validates that grammar +without decoding the credential. Call-agent tickets, socket tickets, and +simulated-device capabilities are also rejected before transport. Both organization and project views expose owner-bound resources: diff --git a/packages/nextjs/test/nextjs.test.ts b/packages/nextjs/test/nextjs.test.ts index 92c0626..26e6147 100644 --- a/packages/nextjs/test/nextjs.test.ts +++ b/packages/nextjs/test/nextjs.test.ts @@ -113,7 +113,10 @@ describe("createMessagingClientTokenMint", () => { describe("createTemplateBuilderRoute", () => { it("accepts the handwritten server SDK templates resource without an adapter", () => { const messaging = new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_fixture" }, + credential: { + type: "apiKey", + value: `pmfa_${"A".repeat(72)}`, + }, }); expect(() => createTemplateBuilderRoute({ diff --git a/packages/typescript/README.md b/packages/typescript/README.md index 0f85910..183e0af 100644 --- a/packages/typescript/README.md +++ b/packages/typescript/README.md @@ -70,9 +70,14 @@ have a different authorization boundary. Messaging credentials are explicit: `apiKey` for an organization server key, `projectToken` for the single opaque project-token format, and `clientToken` for the browser action allowlist. Server credentials fail in browser runtimes. +An organization key is exactly `pmfa_` plus 72 unpadded base64url characters; +a project token is exactly `pmfa_pt_` plus 94. The SDK checks only this public +v1 grammar and never decodes or decrypts the credential. The SDK rejects `pmfa_ct_` browser tokens and CLI-only `pmfa_ls_` listener -credentials before a management request. It does not expose a listener, +credentials before a management request. It also rejects call-agent +`pmfa_at_` tickets, socket `pmfa_wst_` tickets, and simulated-device `pmfa_sd_` +capabilities as server API keys. It does not expose a listener, `AsyncIterable`, event emitter, or forwarding API. Live forwarding belongs to `polymorfa listen`. diff --git a/packages/typescript/src/credentials.ts b/packages/typescript/src/credentials.ts index 5745d65..29304e8 100644 --- a/packages/typescript/src/credentials.ts +++ b/packages/typescript/src/credentials.ts @@ -1,5 +1,8 @@ import { PolymorfaConfigurationError } from "./errors.js"; +const ORGANIZATION_API_KEY_V1 = /^pmfa_[A-Za-z0-9_-]{72}$/; +const PROJECT_TOKEN_V1 = /^pmfa_pt_[A-Za-z0-9_-]{93}[AQgw]$/; + export type MessagingCredential = | { readonly type: "apiKey"; readonly value: string } | { readonly type: "projectToken"; readonly value: string } @@ -40,10 +43,11 @@ export type ClientOptions = export function validateMessagingCredential( credential: MessagingCredential, ): MessagingCredential { + rejectListenerCredential(credential.value); if (credential.type === "projectToken") { if (!isProjectToken(credential.value)) { throw new PolymorfaConfigurationError( - "Messaging project tokens must use the pmfa_pt_ prefix.", + "Messaging project tokens must use the canonical pmfa_pt_ v1 format.", "credential", ); } @@ -62,12 +66,7 @@ export function validateMessagingCredential( return credential; } - if (!isServerApiKey(credential.value)) { - throw new PolymorfaConfigurationError( - "Messaging API key must be a pmfa_ server API key.", - "credential", - ); - } + validateOrganizationApiKey(credential.value); return credential; } @@ -76,12 +75,9 @@ export function validateClientCredential( ): ClientCredential { rejectListenerCredential(credential.value); if (credential.type === "projectToken") { - if ( - !credential.value.startsWith("pmfa_pt_") || - credential.value.length <= "pmfa_pt_".length - ) { + if (!isProjectToken(credential.value)) { throw new PolymorfaConfigurationError( - "Project tokens must use the pmfa_pt_ prefix.", + "Project tokens must use the canonical pmfa_pt_ v1 format.", "credential", ); } @@ -93,9 +89,10 @@ export function validateClientCredential( export function validateOrganizationApiKey(value: string): string { rejectListenerCredential(value); + rejectNonOrganizationCredential(value); if (!isServerApiKey(value)) { throw new PolymorfaConfigurationError( - "Organization server API keys must use the pmfa_ prefix.", + "Organization server API keys must use the canonical pmfa_ v1 format.", "credential", ); } @@ -111,6 +108,25 @@ function rejectListenerCredential(value: string): void { } } +function rejectNonOrganizationCredential(value: string): void { + if (value.startsWith("pmfa_ct_") || value.startsWith("pmfa_pt_")) { + throw new PolymorfaConfigurationError( + "Client and project tokens cannot be used as organization server API keys.", + "credential", + ); + } + if ( + value.startsWith("pmfa_at_") || + value.startsWith("pmfa_wst_") || + value.startsWith("pmfa_sd_") + ) { + throw new PolymorfaConfigurationError( + "Special-purpose tickets and capabilities cannot be used as organization server API keys.", + "credential", + ); + } +} + export function assertServerRuntime( runtime: { readonly window?: unknown; @@ -135,15 +151,9 @@ export function assertServerRuntime( } function isServerApiKey(value: string): boolean { - return ( - value.startsWith("pmfa_") && - value.length > "pmfa_".length && - !value.startsWith("pmfa_ct_") && - !value.startsWith("pmfa_pt_") && - !value.startsWith("pmfa_ls_") - ); + return ORGANIZATION_API_KEY_V1.test(value); } function isProjectToken(value: string): boolean { - return value.startsWith("pmfa_pt_") && value.length > "pmfa_pt_".length; + return PROJECT_TOKEN_V1.test(value); } diff --git a/packages/typescript/test/bridge.test.ts b/packages/typescript/test/bridge.test.ts index 6e70b84..d10835a 100644 --- a/packages/typescript/test/bridge.test.ts +++ b/packages/typescript/test/bridge.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY, PROJECT_TOKEN } from "./support/credentials.js"; import { describe, expect, expectTypeOf, it, vi } from "vitest"; import { @@ -21,7 +22,10 @@ describe("BridgeClient", () => { Response.json(route), ); const client = new BridgeClient({ - credential: { type: "projectToken", value: "pmfa_pt_bridge" }, + credential: { + type: "projectToken", + value: PROJECT_TOKEN, + }, baseUrl: "https://api.example.com", fetch, }); @@ -34,7 +38,7 @@ describe("BridgeClient", () => { const [url, init] = fetch.mock.calls[0]!; expect(new URL(String(url)).pathname).toBe("/v1/bridge/route"); expect(new Headers(init?.headers).get("authorization")).toBe( - "Bearer pmfa_pt_bridge", + `Bearer ${PROJECT_TOKEN}`, ); }); @@ -57,7 +61,7 @@ describe("BridgeClient", () => { new BridgeClient({ credential: { type: "organizationApiKey", - value: "pmfa_organization", + value: ORGANIZATION_API_KEY, }, fetch, } as never), @@ -69,7 +73,10 @@ describe("BridgeClient", () => { expect( () => new BridgeClient({ - credential: { type: "projectToken", value: "pmfa_pt_bridge" }, + credential: { + type: "projectToken", + value: PROJECT_TOKEN, + }, baseUrl: "http://api.example.com", }), ).toThrow(PolymorfaConfigurationError); diff --git a/packages/typescript/test/business.test.ts b/packages/typescript/test/business.test.ts index 9cad7b1..9ac5f27 100644 --- a/packages/typescript/test/business.test.ts +++ b/packages/typescript/test/business.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -35,7 +36,10 @@ async function businessServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/calls-lids-users.test.ts b/packages/typescript/test/calls-lids-users.test.ts index 27efd61..6447e53 100644 --- a/packages/typescript/test/calls-lids-users.test.ts +++ b/packages/typescript/test/calls-lids-users.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -55,7 +56,10 @@ async function compactSurfaceServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/campaigns.test.ts b/packages/typescript/test/campaigns.test.ts index 643c34b..7c50500 100644 --- a/packages/typescript/test/campaigns.test.ts +++ b/packages/typescript/test/campaigns.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -89,7 +90,10 @@ async function campaignsServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/channels.test.ts b/packages/typescript/test/channels.test.ts index c0b3db2..41cf708 100644 --- a/packages/typescript/test/channels.test.ts +++ b/packages/typescript/test/channels.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -44,7 +45,10 @@ async function channelsServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/client.test.ts b/packages/typescript/test/client.test.ts index cf0086f..e73d839 100644 --- a/packages/typescript/test/client.test.ts +++ b/packages/typescript/test/client.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY, PROJECT_TOKEN } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; import { @@ -47,11 +48,17 @@ async function testClient(projectId?: string): Promise<{ projectId === undefined ? new Client({ ...shared, - credential: { type: "organizationApiKey", value: "pmfa_org" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, }) : new Client({ ...shared, - credential: { type: "projectToken", value: "pmfa_pt_project" }, + credential: { + type: "projectToken", + value: PROJECT_TOKEN, + }, projectId, }), }; @@ -89,7 +96,10 @@ describe("Client ownership", () => { expect( () => new (Client as unknown as new (options: unknown) => Client<"project">)({ - credential: { type: "projectToken", value: "pmfa_pt_project" }, + credential: { + type: "projectToken", + value: PROJECT_TOKEN, + }, projectId: undefined, fetch, }), @@ -111,9 +121,59 @@ describe("Client ownership", () => { expect(fetch).not.toHaveBeenCalled(); }); + it("rejects malformed keys and special-purpose tickets before fetch", () => { + const fetch = vi.fn(); + const invalidOrganizationKeys = [ + `pmfa_at_${"A".repeat(69)}`, + `pmfa_wst_${"A".repeat(68)}`, + `pmfa_sd_${"A".repeat(69)}`, + `pmfa_${"A".repeat(71)}`, + `pmfa_${"A".repeat(73)}`, + `pmfa_${"A".repeat(71)}+`, + ]; + + for (const value of invalidOrganizationKeys) { + expect( + () => + new Client({ + credential: { type: "organizationApiKey", value }, + fetch, + }), + ).toThrow(PolymorfaConfigurationError); + expect( + () => + new MessagingClient({ + credential: { type: "apiKey", value }, + fetch, + }), + ).toThrow(PolymorfaConfigurationError); + } + + for (const value of [ + `pmfa_pt_${"A".repeat(93)}`, + `pmfa_pt_${"A".repeat(95)}`, + `pmfa_pt_${"A".repeat(93)}+`, + `pmfa_pt_${"A".repeat(93)}B`, + ]) { + expect( + () => + new Client({ + credential: { type: "projectToken", value }, + projectId: "project_1", + fetch, + }), + ).toThrow(PolymorfaConfigurationError); + } + + expect(fetch).not.toHaveBeenCalled(); + }); + it("treats an explicitly undefined organization projectId as unscoped", () => { const client = new (Client as unknown as new (options: unknown) => Client)({ - credential: { type: "organizationApiKey", value: "pmfa_org" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, projectId: undefined, }); diff --git a/packages/typescript/test/coverage-reconciliation.test.ts b/packages/typescript/test/coverage-reconciliation.test.ts index cf86fd0..1240a72 100644 --- a/packages/typescript/test/coverage-reconciliation.test.ts +++ b/packages/typescript/test/coverage-reconciliation.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; @@ -129,7 +130,7 @@ describe("reconciled coverage evidence", () => { Response.json(fixture.response, { status: fixture.status }), ); const api = new HttpCallsApi({ - apiKey: "pmfa_coverage", + apiKey: ORGANIZATION_API_KEY, baseUrl: "https://api.example.com", fetch, }); @@ -153,7 +154,7 @@ describe("reconciled coverage evidence", () => { expect(JSON.parse(init.body as string)).toEqual(fixture.body); } expect(new Headers(init.headers).get("authorization")).toBe( - "Bearer pmfa_coverage", + `Bearer ${ORGANIZATION_API_KEY}`, ); if (fixture.method === "place") { expect(new Headers(init.headers).get("idempotency-key")).toBe( @@ -166,7 +167,10 @@ describe("reconciled coverage evidence", () => { it("covers QuickLink settings through the exact management routes", async () => { const fetch = vi.fn(async () => Response.json({ data: {} })); const client = new Client({ - credential: { type: "organizationApiKey", value: "pmfa_coverage" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, fetch, }); await client.quickLinkSettings.retrieve(); diff --git a/packages/typescript/test/coverage.test.ts b/packages/typescript/test/coverage.test.ts index 202e2d9..e3dc615 100644 --- a/packages/typescript/test/coverage.test.ts +++ b/packages/typescript/test/coverage.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY, PROJECT_TOKEN } from "./support/credentials.js"; import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -257,17 +258,26 @@ describe("coverage checker", () => { }>; }; const client = new Client({ - credential: { type: "organizationApiKey", value: "pmfa_platform" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, }); const projectClient = client.project("project_coverage"); const roots: Readonly> = { MessagingClient: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_messaging" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, }), Client: client, SystemClient: new SystemClient(), BridgeClient: new BridgeClient({ - credential: { type: "projectToken", value: "pmfa_pt_bridge" }, + credential: { + type: "projectToken", + value: PROJECT_TOKEN, + }, }), HttpCallsApi: new HttpCallsApi({ apiKey: "pmfa_calls" }), BrowserMessagingClient: new BrowserMessagingClient({ diff --git a/packages/typescript/test/credentials.test.ts b/packages/typescript/test/credentials.test.ts index 1eef939..3742eb1 100644 --- a/packages/typescript/test/credentials.test.ts +++ b/packages/typescript/test/credentials.test.ts @@ -8,13 +8,19 @@ import { } from "../src/credentials.js"; import { PolymorfaConfigurationError } from "../src/errors.js"; +const ORGANIZATION_API_KEY = `pmfa_${"A".repeat(72)}`; +const PROJECT_TOKEN = `pmfa_pt_${"A".repeat(94)}`; + describe("credential validation", () => { it("accepts an explicit Messaging server API key", () => { expect( - validateMessagingCredential({ type: "apiKey", value: "pmfa_example" }), + validateMessagingCredential({ + type: "apiKey", + value: ORGANIZATION_API_KEY, + }), ).toEqual({ type: "apiKey", - value: "pmfa_example", + value: ORGANIZATION_API_KEY, }); }); @@ -34,25 +40,25 @@ describe("credential validation", () => { expect( validateMessagingCredential({ type: "projectToken", - value: "pmfa_pt_example", + value: PROJECT_TOKEN, }), - ).toEqual({ type: "projectToken", value: "pmfa_pt_example" }); + ).toEqual({ type: "projectToken", value: PROJECT_TOKEN }); }); it("rejects a mismatched Messaging credential discriminator", () => { expect(() => validateMessagingCredential({ type: "apiKey", value: "pmfa_ct_example" }), - ).toThrow(/Messaging API key/); + ).toThrow(/Client and project tokens/); expect(() => validateMessagingCredential({ type: "clientToken", - value: "pmfa_example", + value: ORGANIZATION_API_KEY, }), ).toThrow(/Messaging client token/); expect(() => validateMessagingCredential({ type: "projectToken", - value: "pmfa_example", + value: ORGANIZATION_API_KEY, }), ).toThrow(/Messaging project token/); }); @@ -62,7 +68,7 @@ describe("credential validation", () => { PolymorfaConfigurationError, ); expect(() => validateOrganizationApiKey("pmfa_pt_example")).toThrow( - /Organization server API key/, + /Client and project tokens/, ); expect(() => validateOrganizationApiKey("pmfa_ls_example")).toThrow( /Listener credentials/, @@ -73,9 +79,56 @@ describe("credential validation", () => { expect( validateClientCredential({ type: "projectToken", - value: "pmfa_pt_example", + value: PROJECT_TOKEN, }), - ).toEqual({ type: "projectToken", value: "pmfa_pt_example" }); + ).toEqual({ type: "projectToken", value: PROJECT_TOKEN }); + + for (const finalSymbol of ["A", "Q", "g", "w"]) { + const value = `pmfa_pt_${"A".repeat(93)}${finalSymbol}`; + expect(validateClientCredential({ type: "projectToken", value })).toEqual( + { type: "projectToken", value }, + ); + } + }); + + it("rejects special-purpose credentials before transport", () => { + for (const value of [ + `pmfa_at_${"A".repeat(69)}`, + `pmfa_wst_${"A".repeat(68)}`, + `pmfa_sd_${"A".repeat(69)}`, + ]) { + expect(() => + validateMessagingCredential({ type: "apiKey", value }), + ).toThrow(PolymorfaConfigurationError); + expect(() => validateOrganizationApiKey(value)).toThrow( + PolymorfaConfigurationError, + ); + } + }); + + it("rejects malformed and non-canonical organization keys", () => { + for (const value of [ + `pmfa_${"A".repeat(71)}`, + `pmfa_${"A".repeat(73)}`, + `pmfa_${"A".repeat(71)}+`, + ]) { + expect(() => validateOrganizationApiKey(value)).toThrow( + PolymorfaConfigurationError, + ); + } + }); + + it("rejects malformed and non-canonical project tokens", () => { + for (const value of [ + `pmfa_pt_${"A".repeat(93)}`, + `pmfa_pt_${"A".repeat(95)}`, + `pmfa_pt_${"A".repeat(93)}+`, + `pmfa_pt_${"A".repeat(93)}B`, + ]) { + expect(() => + validateClientCredential({ type: "projectToken", value }), + ).toThrow(PolymorfaConfigurationError); + } }); it("rejects server API keys in a browser runtime", () => { diff --git a/packages/typescript/test/customers.test.ts b/packages/typescript/test/customers.test.ts index d8e11b3..c2126fa 100644 --- a/packages/typescript/test/customers.test.ts +++ b/packages/typescript/test/customers.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, it } from "vitest"; import { Client } from "../src/client.js"; @@ -39,7 +40,10 @@ async function customersServer(): Promise<{ return { requests: server.requests, client: new Client({ - credential: { type: "organizationApiKey", value: "pmfa_platform" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/developer-operations.test.ts b/packages/typescript/test/developer-operations.test.ts index e06f0a5..d0811a5 100644 --- a/packages/typescript/test/developer-operations.test.ts +++ b/packages/typescript/test/developer-operations.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, it, vi } from "vitest"; import { @@ -42,7 +43,10 @@ describe("Client.operations.wait", () => { ) .mockResolvedValueOnce(Response.json(operation("succeeded"))); const client = new Client({ - credential: { type: "organizationApiKey", value: "pmfa_operations" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: "https://api.example.com", fetch, }); @@ -66,7 +70,10 @@ describe("Client.operations.wait", () => { Response.json(operation("running")), ); const client = new Client({ - credential: { type: "organizationApiKey", value: "pmfa_operations" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: "https://api.example.com", fetch, }); @@ -98,7 +105,10 @@ describe("Client.operations.wait", () => { }), ); const client = new Client({ - credential: { type: "organizationApiKey", value: "pmfa_operations" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: "https://api.example.com", timeoutMs: 60_000, fetch, diff --git a/packages/typescript/test/groups.test.ts b/packages/typescript/test/groups.test.ts index e153d16..eeb70b2 100644 --- a/packages/typescript/test/groups.test.ts +++ b/packages/typescript/test/groups.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -44,7 +45,10 @@ async function groupsServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/labels.test.ts b/packages/typescript/test/labels.test.ts index 51c55ff..1ee6fa5 100644 --- a/packages/typescript/test/labels.test.ts +++ b/packages/typescript/test/labels.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -49,7 +50,10 @@ async function labelsServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/media.test.ts b/packages/typescript/test/media.test.ts index aa799c1..f1e8bbd 100644 --- a/packages/typescript/test/media.test.ts +++ b/packages/typescript/test/media.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -24,7 +25,10 @@ afterEach(async () => { function mediaClient(baseUrl: string, timeoutMs = 500): MessagingClient { return new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl, timeoutMs, maxNetworkRetries: 0, diff --git a/packages/typescript/test/messages.test.ts b/packages/typescript/test/messages.test.ts index 9312870..1589873 100644 --- a/packages/typescript/test/messages.test.ts +++ b/packages/typescript/test/messages.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -56,7 +57,10 @@ async function messagesServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/messaging.test.ts b/packages/typescript/test/messaging.test.ts index 7b549f8..f574ad6 100644 --- a/packages/typescript/test/messaging.test.ts +++ b/packages/typescript/test/messaging.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, it } from "vitest"; import { MessagingClient } from "../src/messaging/client.js"; @@ -29,7 +30,10 @@ async function messagingServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/observation-policies.test.ts b/packages/typescript/test/observation-policies.test.ts index 3fd953c..95185aa 100644 --- a/packages/typescript/test/observation-policies.test.ts +++ b/packages/typescript/test/observation-policies.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -68,7 +69,10 @@ async function policiesServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/package.test.ts b/packages/typescript/test/package.test.ts index 6fa0dcc..17dbe6e 100644 --- a/packages/typescript/test/package.test.ts +++ b/packages/typescript/test/package.test.ts @@ -59,11 +59,11 @@ describe("npm package", () => { [ 'import * as sdk from "@polymorfa/sdk";', "const { MessagingClient, Client, SystemClient, BridgeClient, PRESENCE_STATES, PRIVACY_SETTING_VALUES, SDK_VERSION, webhooks } = sdk;", - 'const messaging = new MessagingClient({ credential: { type: "apiKey", value: "pmfa_fixture" } });', - 'const platform = new Client({ credential: { type: "organizationApiKey", value: "pmfa_fixture" } });', + 'const messaging = new MessagingClient({ credential: { type: "apiKey", value: "pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" } });', + 'const platform = new Client({ credential: { type: "organizationApiKey", value: "pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" } });', 'const project = platform.project("project_123");', "const system = new SystemClient();", - 'const bridge = new BridgeClient({ credential: { type: "projectToken", value: "pmfa_pt_fixture" } });', + 'const bridge = new BridgeClient({ credential: { type: "projectToken", value: "pmfa_pt_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" } });', "let listenerCredentialRejected = false;", 'try { new Client({ credential: { type: "organizationApiKey", value: "pmfa_ls_fixture" } }); } catch { listenerCredentialRejected = true; }', 'console.log(JSON.stringify({ version: SDK_VERSION, messaging: !!messaging.raw, business: typeof messaging.business.getCatalog, calls: typeof messaging.calls.reject, campaigns: typeof messaging.campaigns.launch, messagingMedia: typeof messaging.media.download, chats: typeof messaging.chats.editMessage, channels: typeof messaging.channels.listMessageUpdates, contacts: typeof messaging.contacts.list, groups: typeof messaging.groups.list, labels: typeof messaging.labels.list, lids: typeof messaging.lids.resolve, observationPolicies: typeof messaging.observationPolicies.retrieveForProject, profile: typeof messaging.profile.get, privacy: typeof messaging.privacy.set, privacyValues: PRIVACY_SETTING_VALUES.defense, presence: typeof messaging.presence.getForChat, presenceStates: PRESENCE_STATES, quickReplies: typeof messaging.quickReplies.list, pairing: typeof messaging.sessions.requestPairingCode, messagingOperations: typeof messaging.operations.retrieve, templates: typeof messaging.templates.create, users: typeof messaging.users.getSecurityCode, systemStatus: typeof system.status, systemVersion: typeof system.version, systemHealth: typeof system.health, systemPing: typeof system.ping, bridgeRoutes: typeof bridge.routes.resolve, bridgeListen: typeof bridge.listen, platform: !!platform.raw, platformOwner: platform.owner, projectOwner: project.owner, projectId: project.projectId, apiKeys: typeof platform.apiKeys.deactivate, auditLogs: typeof platform.auditLogs.list, billing: typeof platform.billing.usage, members: typeof platform.members.list, events: typeof project.events.replay, webhooks: typeof project.webhooks.rotateSecret, webhookDeliveries: typeof project.webhookDeliveries.retrieveAttempt, operations: typeof project.operations.wait, projectTokens: typeof platform.projectTokens.list, securityIncidents: typeof platform.securityIncidents.acknowledge, sessionBans: typeof platform.sessionBans.listActive, sessionStart: typeof platform.sessions.start, batchStop: typeof platform.sessions.stopMany, quickLinkSettings: typeof project.quickLinkSettings.update, verifyWebhook: typeof webhooks.verify, createWebhookFixture: typeof webhooks.createFixture, listenerCredentialRejected, platformClientExported: "PlatformClient" in sdk, listenerApiExported: "eventStreams" in sdk, memberInvite: typeof platform.members.invite, organizationUpdate: typeof platform.organizations.update }));', diff --git a/packages/typescript/test/platform-access.test.ts b/packages/typescript/test/platform-access.test.ts index 975bc52..0a2a9e7 100644 --- a/packages/typescript/test/platform-access.test.ts +++ b/packages/typescript/test/platform-access.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -46,7 +47,10 @@ async function platformAccessServer(): Promise<{ return { requests: server.requests, client: new Client({ - credential: { type: "organizationApiKey", value: "pmfa_platform" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/platform-automation.test.ts b/packages/typescript/test/platform-automation.test.ts index b5ff4e9..51d0aad 100644 --- a/packages/typescript/test/platform-automation.test.ts +++ b/packages/typescript/test/platform-automation.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, it } from "vitest"; import { Client } from "../src/client.js"; @@ -28,7 +29,10 @@ async function platformServer(): Promise<{ return { requests: server.requests, client: new Client({ - credential: { type: "organizationApiKey", value: "pmfa_platform" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/platform-response.test.ts b/packages/typescript/test/platform-response.test.ts index 3d17651..9ec87a4 100644 --- a/packages/typescript/test/platform-response.test.ts +++ b/packages/typescript/test/platform-response.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { describe, expect, it, vi } from "vitest"; import { Client, PolymorfaServerError } from "../src/index.js"; @@ -17,7 +18,10 @@ describe("management response envelopes", () => { }), ); const client = new Client({ - credential: { type: "organizationApiKey", value: "pmfa_org" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: "https://api.example.com", fetch, }); @@ -45,7 +49,10 @@ describe("management response envelopes", () => { }), ); const client = new Client({ - credential: { type: "organizationApiKey", value: "pmfa_org" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: "https://api.example.com", fetch, }); diff --git a/packages/typescript/test/platform-session-start.test.ts b/packages/typescript/test/platform-session-start.test.ts index 62ef5d6..4803f4f 100644 --- a/packages/typescript/test/platform-session-start.test.ts +++ b/packages/typescript/test/platform-session-start.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { describe, expect, expectTypeOf, it, vi } from "vitest"; import { @@ -13,7 +14,10 @@ describe("Client.sessions.start", () => { Response.json({ data: { starting: true, sessionId: "session-uuid" } }), ); const client = new Client({ - credential: { type: "organizationApiKey", value: "pmfa_sessions" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: "https://api.example.com", fetch, }); diff --git a/packages/typescript/test/platform-widget-sessions.test.ts b/packages/typescript/test/platform-widget-sessions.test.ts index aead2a8..7032f49 100644 --- a/packages/typescript/test/platform-widget-sessions.test.ts +++ b/packages/typescript/test/platform-widget-sessions.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -34,7 +35,10 @@ async function platformServer(): Promise<{ return { requests: server.requests, client: new Client({ - credential: { type: "organizationApiKey", value: "pmfa_platform" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/platform.test.ts b/packages/typescript/test/platform.test.ts index 294dbc1..00cb7b2 100644 --- a/packages/typescript/test/platform.test.ts +++ b/packages/typescript/test/platform.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY, PROJECT_TOKEN } from "./support/credentials.js"; import { afterEach, describe, expect, it } from "vitest"; import { PolymorfaConfigurationError } from "../src/errors.js"; @@ -29,7 +30,10 @@ async function platformServer(): Promise<{ return { requests: server.requests, client: new Client({ - credential: { type: "organizationApiKey", value: "pmfa_platform" }, + credential: { + type: "organizationApiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), @@ -47,7 +51,10 @@ describe("Client credentials", () => { expect( () => new Client({ - credential: { type: "organizationApiKey", value: "pmfa_pt_project" }, + credential: { + type: "organizationApiKey", + value: PROJECT_TOKEN, + }, }), ).toThrow(PolymorfaConfigurationError); }); diff --git a/packages/typescript/test/presence.test.ts b/packages/typescript/test/presence.test.ts index f7c8432..9ce7a46 100644 --- a/packages/typescript/test/presence.test.ts +++ b/packages/typescript/test/presence.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -50,7 +51,10 @@ async function presenceServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/privacy.test.ts b/packages/typescript/test/privacy.test.ts index 983a98f..ab3d1a9 100644 --- a/packages/typescript/test/privacy.test.ts +++ b/packages/typescript/test/privacy.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -57,7 +58,10 @@ async function privacyServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/profile.test.ts b/packages/typescript/test/profile.test.ts index 0ca5ba6..04c4733 100644 --- a/packages/typescript/test/profile.test.ts +++ b/packages/typescript/test/profile.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -45,7 +46,10 @@ async function profileServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/quick-replies.test.ts b/packages/typescript/test/quick-replies.test.ts index 0fac496..94dacd5 100644 --- a/packages/typescript/test/quick-replies.test.ts +++ b/packages/typescript/test/quick-replies.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -68,7 +69,10 @@ async function quickRepliesServer(): Promise<{ return { requests: server.requests, client: new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }), diff --git a/packages/typescript/test/quicklinks.test.ts b/packages/typescript/test/quicklinks.test.ts index f49fbc3..c6d8fde 100644 --- a/packages/typescript/test/quicklinks.test.ts +++ b/packages/typescript/test/quicklinks.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY, PROJECT_TOKEN } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest"; import { @@ -60,7 +61,10 @@ describe("MessagingClient.quickLinks", () => { }); servers.push(server); const client = new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_quicklink" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }); @@ -126,7 +130,10 @@ describe("MessagingClient.quickLinks", () => { }), ); const client = new MessagingClient({ - credential: { type: "projectToken", value: "pmfa_pt_project" }, + credential: { + type: "projectToken", + value: PROJECT_TOKEN, + }, baseUrl: "https://api.example.com", fetch, }); @@ -135,7 +142,7 @@ describe("MessagingClient.quickLinks", () => { expect( new Headers(fetch.mock.calls[0]?.[1]?.headers).get("authorization"), - ).toBe("Bearer pmfa_pt_project"); + ).toBe(`Bearer ${PROJECT_TOKEN}`); }); it("rejects project tokens before transport in a browser worker", () => { @@ -145,7 +152,10 @@ describe("MessagingClient.quickLinks", () => { expect( () => new MessagingClient({ - credential: { type: "projectToken", value: "pmfa_pt_project" }, + credential: { + type: "projectToken", + value: PROJECT_TOKEN, + }, baseUrl: "https://api.example.com", fetch, }), diff --git a/packages/typescript/test/support/credentials.ts b/packages/typescript/test/support/credentials.ts new file mode 100644 index 0000000..dbfd4c6 --- /dev/null +++ b/packages/typescript/test/support/credentials.ts @@ -0,0 +1,2 @@ +export const ORGANIZATION_API_KEY = `pmfa_${"A".repeat(72)}`; +export const PROJECT_TOKEN = `pmfa_pt_${"A".repeat(94)}`; diff --git a/packages/typescript/test/voip.test.ts b/packages/typescript/test/voip.test.ts index 2b21cff..6158943 100644 --- a/packages/typescript/test/voip.test.ts +++ b/packages/typescript/test/voip.test.ts @@ -1,3 +1,4 @@ +import { ORGANIZATION_API_KEY } from "./support/credentials.js"; import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { @@ -28,7 +29,10 @@ describe("VoipResource", () => { })); servers.push(server); const client = new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, }); @@ -65,7 +69,10 @@ describe("VoipResource", () => { })); servers.push(server); const client = new MessagingClient({ - credential: { type: "apiKey", value: "pmfa_example" }, + credential: { + type: "apiKey", + value: ORGANIZATION_API_KEY, + }, baseUrl: server.url, maxNetworkRetries: 0, });