@byok-sdk/testkit 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ quietly green against nothing the moment upstream changes any of it.
|
|
|
13
13
|
Runtime dependencies are `@byok-sdk/core` and `@byok-sdk/protocol`, and nothing
|
|
14
14
|
else — **no test framework**. The negative assertions are async functions
|
|
15
15
|
returning structured results, so the same checks run under vitest, under a plain
|
|
16
|
-
CI script, and inside a Worker. Requires Node ≥ 22.
|
|
16
|
+
CI script, and inside a Worker. Requires Node ≥ 22.22 (or any runtime exposing
|
|
17
17
|
WebCrypto Ed25519 on `globalThis.crypto.subtle`).
|
|
18
18
|
|
|
19
19
|
## Install
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ async function createDeviceIdentity() {
|
|
|
12
12
|
const subtle = globalThis.crypto?.subtle;
|
|
13
13
|
if (subtle === void 0) {
|
|
14
14
|
throw new Error(
|
|
15
|
-
"@byok-sdk/testkit needs WebCrypto: globalThis.crypto.subtle is unavailable in this runtime (Node >=22.
|
|
15
|
+
"@byok-sdk/testkit needs WebCrypto: globalThis.crypto.subtle is unavailable in this runtime (Node >=22.22 or a Worker provides it)."
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
const generated = await subtle.generateKey({ name: "Ed25519" }, false, ["sign", "verify"]);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/identity.ts","../src/simulator.ts","../src/negatives.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * A simulated device's identity key (docs/protocol.md §6.1).\n *\n * Ed25519 through WebCrypto rather than `node:crypto`, for the same reason\n * `@byok-sdk/core` holds no `node:` import: a simulator that only runs on Node\n * cannot smoke-test a Workers deployment from inside a Worker. Node ≥22.19\n * (this workspace's floor) exposes Ed25519 on `globalThis.crypto.subtle`, so\n * there is no polyfill seam and no second code path.\n *\n * The public key crosses the wire as the JWK `x` member: RFC 8037 defines it as\n * the raw 32-byte key, base64url with no padding (43 characters), which is\n * exactly the encoding `PairRequest.devicePublicKey` is specified in and the\n * form both the reference server and the hosted surface hand to their verifier\n * without an ASN.1/SPKI unwrap. Signatures are the raw 64-byte Ed25519\n * signature, base64url.\n */\nimport { nonceSigningBytes } from '@byok-sdk/core';\n\n/** Length of a base64url-encoded raw 32-byte Ed25519 public key, unpadded. */\nexport const DEVICE_PUBLIC_KEY_LENGTH = 43;\n\nexport interface DeviceIdentity {\n /** JWK `x` — raw 32-byte Ed25519 public key, base64url, 43 characters. */\n readonly publicKeyBase64Url: string;\n /** Raw 64-byte Ed25519 signature over `message`, base64url. */\n sign(message: Uint8Array): Promise<string>;\n /**\n * Signature over the domain-separated challenge bytes — core's\n * {@link nonceSigningBytes}, never a local re-spelling of the domain. This is\n * the only signing shape any real device produces.\n */\n signNonce(nonce: string): Promise<string>;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n}\n\n/**\n * Generate a fresh device keypair. The private key stays a non-extractable\n * `CryptoKey` inside this object — a simulator has no reason to hand one out,\n * and a test that cannot leak a key cannot accidentally assert on one.\n */\nexport async function createDeviceIdentity(): Promise<DeviceIdentity> {\n const subtle = globalThis.crypto?.subtle;\n if (subtle === undefined) {\n throw new Error(\n '@byok-sdk/testkit needs WebCrypto: globalThis.crypto.subtle is unavailable in this runtime (Node >=22.19 or a Worker provides it).',\n );\n }\n\n const generated = await subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify']);\n // `generateKey` is typed as \"a key or a key pair\" because the algorithm decides\n // which; Ed25519 is asymmetric, so narrow rather than assert.\n if (!('privateKey' in generated)) {\n throw new Error('WebCrypto returned a single key for Ed25519, expected a keypair');\n }\n\n const jwk = await subtle.exportKey('jwk', generated.publicKey);\n const x = jwk.x;\n if (typeof x !== 'string' || x.length !== DEVICE_PUBLIC_KEY_LENGTH) {\n throw new Error(\n `Ed25519 public key JWK 'x' must be ${DEVICE_PUBLIC_KEY_LENGTH} base64url characters, got ${String(x)}`,\n );\n }\n\n const sign = async (message: Uint8Array): Promise<string> => {\n // `BufferSource` in the DOM lib is `ArrayBufferView<ArrayBuffer>`; a\n // Uint8Array over a SharedArrayBuffer is not one, so hand `subtle` the\n // backing bytes explicitly rather than widening the parameter type.\n const signature = await subtle.sign(\n { name: 'Ed25519' },\n generated.privateKey,\n message.slice().buffer as ArrayBuffer,\n );\n return base64url(new Uint8Array(signature));\n };\n\n return {\n publicKeyBase64Url: x,\n sign,\n signNonce: (nonce) => sign(nonceSigningBytes(nonce)),\n };\n}\n","/**\n * `createDeviceSimulator` — one device, driven over the real device wire.\n *\n * The problem this exists to remove: a host integrating the SDK writes a smoke\n * test, and to write it has to re-implement the device end of docs/protocol.md\n * §6 by hand — generate an Ed25519 key, export the JWK `x`, know that a nonce\n * is signed under a domain prefix, know the exact literal, know the three auth\n * route shapes. Every one of those is upstream knowledge living downstream, so\n * when upstream changes the domain or the pairing schema, every host's smoke\n * test keeps passing against nothing.\n *\n * So this package owns the device end, and owns *nothing else*:\n *\n * - The bytes signed for a challenge come from `@byok-sdk/core`'s\n * `nonceSigningBytes`. There is no domain literal in this package — a\n * simulator carrying its own copy would be the fourth copy of the drift this\n * slice removed.\n * - The request/response DTOs are parsed with `@byok-sdk/protocol`'s schemas,\n * fail-closed. A response this package cannot parse is an error, never a\n * partially-trusted object.\n * - The paths are the hosted surface's real ones (`POST /byok/pair`,\n * `POST /byok/challenge`, `POST /byok/token`, `PUT /byok/presence`).\n *\n * What it deliberately does NOT own is the host control plane. The SDK mounts\n * no admin route: minting a pairing code, listing presence, and revoking a\n * device are in-process calls on `ByokCloud`/`ByokServer`, and a deployment\n * that exposes them over HTTP defines those paths and their credential itself.\n * Inventing `/byok/admin/...` here would be publishing a wire contract that\n * does not exist, so the host surface arrives as a {@link SimulatorHost}\n * adapter the caller supplies — three methods, in-process or HTTP, its choice.\n */\nimport type { PresenceHint, PresenceLevel } from '@byok-sdk/core';\nimport {\n BYOK_CHALLENGE_PATH,\n BYOK_PAIR_PATH,\n BYOK_PRESENCE_PATH,\n BYOK_TOKEN_PATH,\n ChallengeResponseSchema,\n PairResponseSchema,\n TokenResponseSchema,\n type ToolsetId,\n} from '@byok-sdk/protocol';\nimport { createDeviceIdentity, type DeviceIdentity } from './identity';\n\n/** The device-surface routes this simulator drives (docs/protocol.md §6, §12.3). */\nexport const DEVICE_ROUTES = {\n pair: BYOK_PAIR_PATH,\n challenge: BYOK_CHALLENGE_PATH,\n token: BYOK_TOKEN_PATH,\n presence: BYOK_PRESENCE_PATH,\n} as const;\n\nexport const DEFAULT_DEVICE_NAME = 'byok-testkit-device';\n\n/**\n * The host control plane, supplied by the caller.\n *\n * In-process (`cloud.listPresence(tenant)`) and over HTTP (the host's own admin\n * route plus whatever credential guards it) are both one small adapter. What\n * this package will not do is guess either the path or the credential.\n */\nexport interface SimulatorHost {\n /** Every live presence hint the host can see for the tenant under test. */\n listPresence(): Promise<readonly PresenceHint[]>;\n /** Revoke a device. After this, its next challenge/token/authed call is a 401 (§6.3). */\n revokeDevice(deviceId: string): Promise<void>;\n}\n\nexport interface DeviceSimulatorOptions {\n /** Origin the device surface is mounted at, e.g. `https://cloud.example.com`. */\n readonly baseUrl: string;\n /** Defaults to `globalThis.fetch`. A composition with no socket passes its own `cloud.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** Host control plane. Absent is legal; `readHostPresence`/`revoke` then throw rather than guess. */\n readonly host?: SimulatorHost;\n /** Device name sent at pairing time. */\n readonly deviceName?: string;\n}\n\n/** What `POST /byok/pair` handed back — the device's identity on this deployment. */\nexport interface DeviceSession {\n readonly deviceId: string;\n readonly accessToken: string;\n}\n\nexport type Credential = 'device' | 'none';\n\nexport interface SimulatorRequest {\n readonly method: string;\n readonly path: string;\n readonly body?: unknown;\n /** `'device'` sends the current access token; `'none'` sends no Authorization header. */\n readonly credential?: Credential;\n}\n\nexport interface SimulatorResponse {\n readonly status: number;\n /** The response body verbatim. */\n readonly text: string;\n /**\n * `text` parsed as JSON, or `undefined` when it is empty or is not JSON at\n * all — a 404 from a router that answers in plain text is a real answer, and\n * the status is what an assertion is asking about. Absent means \"no JSON\n * here\", never \"here is what it probably meant\": every caller that needs a\n * DTO runs it through a `@byok-sdk/protocol` schema, which refuses\n * `undefined` outright.\n */\n readonly body: unknown;\n}\n\n/** A request that did not get the status this call requires. Carries the response, not a summary of it. */\nexport class DeviceSimulatorError extends Error {\n readonly status: number;\n readonly body: unknown;\n readonly text: string;\n\n constructor(message: string, response: SimulatorResponse) {\n super(`${message} (HTTP ${response.status}): ${response.text || '<empty body>'}`);\n this.name = 'DeviceSimulatorError';\n this.status = response.status;\n this.body = response.body;\n this.text = response.text;\n }\n}\n\nexport interface DeviceSimulator {\n readonly identity: DeviceIdentity;\n /** The paired session, or `undefined` before `pair()`. */\n readonly session: DeviceSession | undefined;\n\n /** §6.1 — redeem a one-time pairing code and register this device's public key. */\n pair(pairingCode: string, deviceName?: string): Promise<DeviceSession>;\n /** §6.2 — ask for a one-time nonce to sign. */\n challenge(deviceId?: string): Promise<string>;\n /** §6.2 — trade a signed nonce for a fresh access token. The signature is supplied, never assumed. */\n token(request: { deviceId: string; nonce: string; signature: string }): Promise<string>;\n /** `challenge()` → sign with the domain-separated bytes → `token()`, adopting the new access token. */\n renewAccessToken(): Promise<string>;\n /** §12.3 — publish a presence hint under the device bearer. */\n publishPresence(\n level: PresenceLevel,\n detail?: string,\n configuredToolsets?: readonly ToolsetId[],\n ): Promise<void>;\n /** Read presence back the way a host does, through the supplied {@link SimulatorHost}. */\n readHostPresence(): Promise<readonly PresenceHint[]>;\n /** §6.3 — revoke this device through the supplied {@link SimulatorHost}. */\n revoke(): Promise<void>;\n\n /**\n * The raw request primitive every call above is built from. Exposed because a\n * host's own extra assertions should not have to re-derive base URL joining,\n * bearer injection, and JSON parsing to make one off-path request.\n */\n request(input: SimulatorRequest): Promise<SimulatorResponse>;\n}\n\nfunction parseJson(text: string): unknown {\n if (text.length === 0) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\nexport async function createDeviceSimulator(\n options: DeviceSimulatorOptions,\n): Promise<DeviceSimulator> {\n const identity = await createDeviceIdentity();\n const doFetch = options.fetch ?? globalThis.fetch;\n const origin = options.baseUrl.replace(/\\/+$/, '');\n const defaultDeviceName = options.deviceName ?? DEFAULT_DEVICE_NAME;\n let session: DeviceSession | undefined;\n\n function requireSession(action: string): DeviceSession {\n if (session === undefined) {\n throw new Error(`${action} needs a paired device: call pair(pairingCode) first.`);\n }\n return session;\n }\n\n function requireHost(action: string): SimulatorHost {\n if (options.host === undefined) {\n throw new Error(\n `${action} is a host control-plane operation and the SDK mounts no admin route for it: pass a SimulatorHost to createDeviceSimulator.`,\n );\n }\n return options.host;\n }\n\n async function request(input: SimulatorRequest): Promise<SimulatorResponse> {\n const headers = new Headers();\n if (input.body !== undefined) headers.set('content-type', 'application/json');\n if ((input.credential ?? 'device') === 'device' && session !== undefined) {\n headers.set('authorization', `Bearer ${session.accessToken}`);\n }\n\n const response = await doFetch(\n new Request(`${origin}${input.path}`, {\n method: input.method,\n headers,\n ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),\n }),\n );\n const text = await response.text();\n return { status: response.status, text, body: parseJson(text) };\n }\n\n function expectOk(what: string, response: SimulatorResponse): SimulatorResponse {\n if (response.status !== 200) throw new DeviceSimulatorError(what, response);\n return response;\n }\n\n async function pair(pairingCode: string, deviceName = defaultDeviceName): Promise<DeviceSession> {\n const response = expectOk(\n 'pairing rejected',\n await request({\n method: 'POST',\n path: DEVICE_ROUTES.pair,\n credential: 'none',\n body: {\n pairingCode,\n deviceName,\n devicePublicKey: identity.publicKeyBase64Url,\n },\n }),\n );\n const parsed = PairResponseSchema.parse(response.body);\n session = { deviceId: parsed.deviceId, accessToken: parsed.accessToken };\n return session;\n }\n\n async function challenge(deviceId?: string): Promise<string> {\n const response = expectOk(\n 'challenge rejected',\n await request({\n method: 'POST',\n path: DEVICE_ROUTES.challenge,\n credential: 'none',\n body: { deviceId: deviceId ?? requireSession('challenge()').deviceId },\n }),\n );\n return ChallengeResponseSchema.parse(response.body).nonce;\n }\n\n async function token(input: {\n deviceId: string;\n nonce: string;\n signature: string;\n }): Promise<string> {\n const response = expectOk(\n 'token exchange rejected',\n await request({\n method: 'POST',\n path: DEVICE_ROUTES.token,\n credential: 'none',\n body: input,\n }),\n );\n return TokenResponseSchema.parse(response.body).accessToken;\n }\n\n async function renewAccessToken(): Promise<string> {\n const current = requireSession('renewAccessToken()');\n const nonce = await challenge(current.deviceId);\n const accessToken = await token({\n deviceId: current.deviceId,\n nonce,\n signature: await identity.signNonce(nonce),\n });\n session = { deviceId: current.deviceId, accessToken };\n return accessToken;\n }\n\n return {\n identity,\n get session() {\n return session;\n },\n\n pair,\n challenge,\n token,\n renewAccessToken,\n\n async publishPresence(level, detail, configuredToolsets) {\n requireSession('publishPresence()');\n expectOk(\n 'presence publication rejected',\n await request({\n method: 'PUT',\n path: DEVICE_ROUTES.presence,\n body: {\n level,\n ...(detail === undefined ? {} : { detail }),\n ...(configuredToolsets === undefined ? {} : { configuredToolsets }),\n },\n }),\n );\n },\n\n readHostPresence() {\n return requireHost('readHostPresence()').listPresence();\n },\n\n revoke() {\n return requireHost('revoke()').revokeDevice(requireSession('revoke()').deviceId);\n },\n\n request,\n };\n}\n","/**\n * The four negative assertions a host's pairing smoke test hand-writes.\n *\n * These are plain async functions on purpose. A negative assertion belongs to\n * the protocol, not to a test runner: the same four have to be runnable from\n * vitest, from a CI script, from a deployment gate, and from a Worker. So each\n * returns a structured {@link NegativeAssertionResult} instead of throwing —\n * the caller decides what a failure means, and a vitest consumer writes\n * `expect(result.ok).toBe(true)` and gets the observed status in the diff.\n *\n * Each one names a defense that must hold, and each is written so that pointing\n * it at the wrong input makes it FAIL. An assertion that cannot go red proves\n * nothing, and the ones here are checked that way in\n * `@byok-sdk/conformance`'s `pairing-simulator` suite before they are trusted\n * green: unauthenticated against a public route, single-use against an unused\n * code, undomained against a properly domained signature, revoked against a\n * device that was never revoked.\n */\nimport type { DeviceSimulator, SimulatorRequest, SimulatorResponse } from './simulator';\nimport { DEVICE_ROUTES } from './simulator';\n\nexport interface NegativeAssertionResult {\n /** Stable identifier — safe to use as a test name or a CI check id. */\n readonly name: string;\n readonly ok: boolean;\n /** What the protocol requires. */\n readonly expected: string;\n /** What the deployment actually did. */\n readonly actual: string;\n /** The response that decided it, when one status decided it. */\n readonly response?: SimulatorResponse;\n}\n\nfunction result(\n name: string,\n expected: string,\n response: SimulatorResponse,\n expectedStatus: number,\n): NegativeAssertionResult {\n return {\n name,\n ok: response.status === expectedStatus,\n expected,\n actual: `HTTP ${response.status}`,\n response,\n };\n}\n\n/**\n * A credentialed route answers 401 when the credential is absent.\n *\n * Defaults to `PUT /byok/presence` — the SDK's own credentialed HTTP surface is\n * device-class, because the host control plane (pairing-code mint, presence\n * read, revoke) is in-process and mounts no route. A deployment that publishes\n * its own admin routes passes one here and gets the same assertion against it.\n */\nexport async function assertUnauthenticatedRejected(\n simulator: DeviceSimulator,\n target: Omit<SimulatorRequest, 'credential'> = {\n method: 'PUT',\n path: DEVICE_ROUTES.presence,\n body: { level: 'online' },\n },\n): Promise<NegativeAssertionResult> {\n const response = await simulator.request({ ...target, credential: 'none' });\n return result(\n 'unauthenticated-request-rejected',\n `${target.method} ${target.path} without a credential is 401`,\n response,\n 401,\n );\n}\n\n/**\n * A pairing code is single-use (§6.1): redeeming an already-redeemed code is a\n * 401, indistinguishable from an unknown or expired one.\n *\n * Pass the code THIS simulator (or another one) already paired with. Pointing\n * it at a fresh code pairs a second device and returns `ok: false` — that is\n * the red form.\n */\nexport async function assertPairingCodeSingleUse(\n simulator: DeviceSimulator,\n redeemedPairingCode: string,\n deviceName = 'byok-testkit-replay',\n): Promise<NegativeAssertionResult> {\n const response = await simulator.request({\n method: 'POST',\n path: DEVICE_ROUTES.pair,\n credential: 'none',\n body: {\n pairingCode: redeemedPairingCode,\n deviceName,\n devicePublicKey: simulator.identity.publicKeyBase64Url,\n },\n });\n return result(\n 'pairing-code-single-use',\n 'a second redemption of the same pairing code is 401',\n response,\n 401,\n );\n}\n\n/**\n * A signature over the BARE nonce does not renew a token (§6.2, GAP-004).\n *\n * This is the one place in the package that signs something other than\n * `nonceSigningBytes` — and it signs *less*, never a second domain of its own.\n * The whole point is that the undomained encoding has no accepted form\n * anywhere, so there is no dual mode to accidentally validate.\n *\n * Set `domainSeparated: true` to get the red form: the correctly domained\n * signature renews the token, 200, and the assertion reports failure.\n */\nexport async function assertUndomainedSignatureRejected(\n simulator: DeviceSimulator,\n options: { readonly domainSeparated?: boolean } = {},\n): Promise<NegativeAssertionResult> {\n const session = simulator.session;\n if (session === undefined) {\n throw new Error('assertUndomainedSignatureRejected needs a paired device.');\n }\n\n const nonce = await simulator.challenge(session.deviceId);\n const signature =\n options.domainSeparated === true\n ? await simulator.identity.signNonce(nonce)\n : await simulator.identity.sign(new TextEncoder().encode(nonce));\n\n const response = await simulator.request({\n method: 'POST',\n path: DEVICE_ROUTES.token,\n credential: 'none',\n body: { deviceId: session.deviceId, nonce, signature },\n });\n return result(\n 'undomained-signature-rejected',\n 'a signature over the bare nonce, without the signing domain, is 401',\n response,\n 401,\n );\n}\n\n/**\n * A revoked device cannot start a renewal (§6.3): its next `/byok/challenge` is\n * a 401, indistinguishable from an unknown device.\n *\n * Does NOT revoke anything itself — revocation is the host's act, so the caller\n * revokes and then asserts. Calling it before revoking is the red form.\n */\nexport async function assertRevokedDeviceChallengeRejected(\n simulator: DeviceSimulator,\n): Promise<NegativeAssertionResult> {\n const session = simulator.session;\n if (session === undefined) {\n throw new Error('assertRevokedDeviceChallengeRejected needs a paired device.');\n }\n\n const response = await simulator.request({\n method: 'POST',\n path: DEVICE_ROUTES.challenge,\n credential: 'none',\n body: { deviceId: session.deviceId },\n });\n return result(\n 'revoked-device-challenge-rejected',\n 'a revoked device asking for a challenge is 401',\n response,\n 401,\n );\n}\n\nexport interface NegativeSuiteInput {\n /** A pairing code this simulator has already redeemed. */\n readonly redeemedPairingCode: string;\n /** Optional non-default target for the unauthenticated check (e.g. a host admin route). */\n readonly unauthenticatedTarget?: Omit<SimulatorRequest, 'credential'>;\n}\n\n/**\n * All four, in the only order they can run in: revocation is terminal for this\n * device, so the checks that need a live session go first.\n */\nexport async function runNegativeAssertions(\n simulator: DeviceSimulator,\n input: NegativeSuiteInput,\n): Promise<readonly NegativeAssertionResult[]> {\n const results: NegativeAssertionResult[] = [\n input.unauthenticatedTarget === undefined\n ? await assertUnauthenticatedRejected(simulator)\n : await assertUnauthenticatedRejected(simulator, input.unauthenticatedTarget),\n await assertPairingCodeSingleUse(simulator, input.redeemedPairingCode),\n await assertUndomainedSignatureRejected(simulator),\n ];\n await simulator.revoke();\n results.push(await assertRevokedDeviceChallengeRejected(simulator));\n return results;\n}\n\n/** Every failed assertion in `results`, for a caller that wants one error instead of four booleans. */\nexport function failedAssertions(\n results: readonly NegativeAssertionResult[],\n): readonly NegativeAssertionResult[] {\n return results.filter((entry) => !entry.ok);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/identity.ts","../src/simulator.ts","../src/negatives.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * A simulated device's identity key (docs/protocol.md §6.1).\n *\n * Ed25519 through WebCrypto rather than `node:crypto`, for the same reason\n * `@byok-sdk/core` holds no `node:` import: a simulator that only runs on Node\n * cannot smoke-test a Workers deployment from inside a Worker. Node ≥22.22\n * (this workspace's floor) exposes Ed25519 on `globalThis.crypto.subtle`, so\n * there is no polyfill seam and no second code path.\n *\n * The public key crosses the wire as the JWK `x` member: RFC 8037 defines it as\n * the raw 32-byte key, base64url with no padding (43 characters), which is\n * exactly the encoding `PairRequest.devicePublicKey` is specified in and the\n * form both the reference server and the hosted surface hand to their verifier\n * without an ASN.1/SPKI unwrap. Signatures are the raw 64-byte Ed25519\n * signature, base64url.\n */\nimport { nonceSigningBytes } from '@byok-sdk/core';\n\n/** Length of a base64url-encoded raw 32-byte Ed25519 public key, unpadded. */\nexport const DEVICE_PUBLIC_KEY_LENGTH = 43;\n\nexport interface DeviceIdentity {\n /** JWK `x` — raw 32-byte Ed25519 public key, base64url, 43 characters. */\n readonly publicKeyBase64Url: string;\n /** Raw 64-byte Ed25519 signature over `message`, base64url. */\n sign(message: Uint8Array): Promise<string>;\n /**\n * Signature over the domain-separated challenge bytes — core's\n * {@link nonceSigningBytes}, never a local re-spelling of the domain. This is\n * the only signing shape any real device produces.\n */\n signNonce(nonce: string): Promise<string>;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n}\n\n/**\n * Generate a fresh device keypair. The private key stays a non-extractable\n * `CryptoKey` inside this object — a simulator has no reason to hand one out,\n * and a test that cannot leak a key cannot accidentally assert on one.\n */\nexport async function createDeviceIdentity(): Promise<DeviceIdentity> {\n const subtle = globalThis.crypto?.subtle;\n if (subtle === undefined) {\n throw new Error(\n '@byok-sdk/testkit needs WebCrypto: globalThis.crypto.subtle is unavailable in this runtime (Node >=22.22 or a Worker provides it).',\n );\n }\n\n const generated = await subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify']);\n // `generateKey` is typed as \"a key or a key pair\" because the algorithm decides\n // which; Ed25519 is asymmetric, so narrow rather than assert.\n if (!('privateKey' in generated)) {\n throw new Error('WebCrypto returned a single key for Ed25519, expected a keypair');\n }\n\n const jwk = await subtle.exportKey('jwk', generated.publicKey);\n const x = jwk.x;\n if (typeof x !== 'string' || x.length !== DEVICE_PUBLIC_KEY_LENGTH) {\n throw new Error(\n `Ed25519 public key JWK 'x' must be ${DEVICE_PUBLIC_KEY_LENGTH} base64url characters, got ${String(x)}`,\n );\n }\n\n const sign = async (message: Uint8Array): Promise<string> => {\n // `BufferSource` in the DOM lib is `ArrayBufferView<ArrayBuffer>`; a\n // Uint8Array over a SharedArrayBuffer is not one, so hand `subtle` the\n // backing bytes explicitly rather than widening the parameter type.\n const signature = await subtle.sign(\n { name: 'Ed25519' },\n generated.privateKey,\n message.slice().buffer as ArrayBuffer,\n );\n return base64url(new Uint8Array(signature));\n };\n\n return {\n publicKeyBase64Url: x,\n sign,\n signNonce: (nonce) => sign(nonceSigningBytes(nonce)),\n };\n}\n","/**\n * `createDeviceSimulator` — one device, driven over the real device wire.\n *\n * The problem this exists to remove: a host integrating the SDK writes a smoke\n * test, and to write it has to re-implement the device end of docs/protocol.md\n * §6 by hand — generate an Ed25519 key, export the JWK `x`, know that a nonce\n * is signed under a domain prefix, know the exact literal, know the three auth\n * route shapes. Every one of those is upstream knowledge living downstream, so\n * when upstream changes the domain or the pairing schema, every host's smoke\n * test keeps passing against nothing.\n *\n * So this package owns the device end, and owns *nothing else*:\n *\n * - The bytes signed for a challenge come from `@byok-sdk/core`'s\n * `nonceSigningBytes`. There is no domain literal in this package — a\n * simulator carrying its own copy would be the fourth copy of the drift this\n * slice removed.\n * - The request/response DTOs are parsed with `@byok-sdk/protocol`'s schemas,\n * fail-closed. A response this package cannot parse is an error, never a\n * partially-trusted object.\n * - The paths are the hosted surface's real ones (`POST /byok/pair`,\n * `POST /byok/challenge`, `POST /byok/token`, `PUT /byok/presence`).\n *\n * What it deliberately does NOT own is the host control plane. The SDK mounts\n * no admin route: minting a pairing code, listing presence, and revoking a\n * device are in-process calls on `ByokCloud`/`ByokServer`, and a deployment\n * that exposes them over HTTP defines those paths and their credential itself.\n * Inventing `/byok/admin/...` here would be publishing a wire contract that\n * does not exist, so the host surface arrives as a {@link SimulatorHost}\n * adapter the caller supplies — three methods, in-process or HTTP, its choice.\n */\nimport type { PresenceHint, PresenceLevel } from '@byok-sdk/core';\nimport {\n BYOK_CHALLENGE_PATH,\n BYOK_PAIR_PATH,\n BYOK_PRESENCE_PATH,\n BYOK_TOKEN_PATH,\n ChallengeResponseSchema,\n PairResponseSchema,\n TokenResponseSchema,\n type ToolsetId,\n} from '@byok-sdk/protocol';\nimport { createDeviceIdentity, type DeviceIdentity } from './identity';\n\n/** The device-surface routes this simulator drives (docs/protocol.md §6, §12.3). */\nexport const DEVICE_ROUTES = {\n pair: BYOK_PAIR_PATH,\n challenge: BYOK_CHALLENGE_PATH,\n token: BYOK_TOKEN_PATH,\n presence: BYOK_PRESENCE_PATH,\n} as const;\n\nexport const DEFAULT_DEVICE_NAME = 'byok-testkit-device';\n\n/**\n * The host control plane, supplied by the caller.\n *\n * In-process (`cloud.listPresence(tenant)`) and over HTTP (the host's own admin\n * route plus whatever credential guards it) are both one small adapter. What\n * this package will not do is guess either the path or the credential.\n */\nexport interface SimulatorHost {\n /** Every live presence hint the host can see for the tenant under test. */\n listPresence(): Promise<readonly PresenceHint[]>;\n /** Revoke a device. After this, its next challenge/token/authed call is a 401 (§6.3). */\n revokeDevice(deviceId: string): Promise<void>;\n}\n\nexport interface DeviceSimulatorOptions {\n /** Origin the device surface is mounted at, e.g. `https://cloud.example.com`. */\n readonly baseUrl: string;\n /** Defaults to `globalThis.fetch`. A composition with no socket passes its own `cloud.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** Host control plane. Absent is legal; `readHostPresence`/`revoke` then throw rather than guess. */\n readonly host?: SimulatorHost;\n /** Device name sent at pairing time. */\n readonly deviceName?: string;\n}\n\n/** What `POST /byok/pair` handed back — the device's identity on this deployment. */\nexport interface DeviceSession {\n readonly deviceId: string;\n readonly accessToken: string;\n}\n\nexport type Credential = 'device' | 'none';\n\nexport interface SimulatorRequest {\n readonly method: string;\n readonly path: string;\n readonly body?: unknown;\n /** `'device'` sends the current access token; `'none'` sends no Authorization header. */\n readonly credential?: Credential;\n}\n\nexport interface SimulatorResponse {\n readonly status: number;\n /** The response body verbatim. */\n readonly text: string;\n /**\n * `text` parsed as JSON, or `undefined` when it is empty or is not JSON at\n * all — a 404 from a router that answers in plain text is a real answer, and\n * the status is what an assertion is asking about. Absent means \"no JSON\n * here\", never \"here is what it probably meant\": every caller that needs a\n * DTO runs it through a `@byok-sdk/protocol` schema, which refuses\n * `undefined` outright.\n */\n readonly body: unknown;\n}\n\n/** A request that did not get the status this call requires. Carries the response, not a summary of it. */\nexport class DeviceSimulatorError extends Error {\n readonly status: number;\n readonly body: unknown;\n readonly text: string;\n\n constructor(message: string, response: SimulatorResponse) {\n super(`${message} (HTTP ${response.status}): ${response.text || '<empty body>'}`);\n this.name = 'DeviceSimulatorError';\n this.status = response.status;\n this.body = response.body;\n this.text = response.text;\n }\n}\n\nexport interface DeviceSimulator {\n readonly identity: DeviceIdentity;\n /** The paired session, or `undefined` before `pair()`. */\n readonly session: DeviceSession | undefined;\n\n /** §6.1 — redeem a one-time pairing code and register this device's public key. */\n pair(pairingCode: string, deviceName?: string): Promise<DeviceSession>;\n /** §6.2 — ask for a one-time nonce to sign. */\n challenge(deviceId?: string): Promise<string>;\n /** §6.2 — trade a signed nonce for a fresh access token. The signature is supplied, never assumed. */\n token(request: { deviceId: string; nonce: string; signature: string }): Promise<string>;\n /** `challenge()` → sign with the domain-separated bytes → `token()`, adopting the new access token. */\n renewAccessToken(): Promise<string>;\n /** §12.3 — publish a presence hint under the device bearer. */\n publishPresence(\n level: PresenceLevel,\n detail?: string,\n configuredToolsets?: readonly ToolsetId[],\n ): Promise<void>;\n /** Read presence back the way a host does, through the supplied {@link SimulatorHost}. */\n readHostPresence(): Promise<readonly PresenceHint[]>;\n /** §6.3 — revoke this device through the supplied {@link SimulatorHost}. */\n revoke(): Promise<void>;\n\n /**\n * The raw request primitive every call above is built from. Exposed because a\n * host's own extra assertions should not have to re-derive base URL joining,\n * bearer injection, and JSON parsing to make one off-path request.\n */\n request(input: SimulatorRequest): Promise<SimulatorResponse>;\n}\n\nfunction parseJson(text: string): unknown {\n if (text.length === 0) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\nexport async function createDeviceSimulator(\n options: DeviceSimulatorOptions,\n): Promise<DeviceSimulator> {\n const identity = await createDeviceIdentity();\n const doFetch = options.fetch ?? globalThis.fetch;\n const origin = options.baseUrl.replace(/\\/+$/, '');\n const defaultDeviceName = options.deviceName ?? DEFAULT_DEVICE_NAME;\n let session: DeviceSession | undefined;\n\n function requireSession(action: string): DeviceSession {\n if (session === undefined) {\n throw new Error(`${action} needs a paired device: call pair(pairingCode) first.`);\n }\n return session;\n }\n\n function requireHost(action: string): SimulatorHost {\n if (options.host === undefined) {\n throw new Error(\n `${action} is a host control-plane operation and the SDK mounts no admin route for it: pass a SimulatorHost to createDeviceSimulator.`,\n );\n }\n return options.host;\n }\n\n async function request(input: SimulatorRequest): Promise<SimulatorResponse> {\n const headers = new Headers();\n if (input.body !== undefined) headers.set('content-type', 'application/json');\n if ((input.credential ?? 'device') === 'device' && session !== undefined) {\n headers.set('authorization', `Bearer ${session.accessToken}`);\n }\n\n const response = await doFetch(\n new Request(`${origin}${input.path}`, {\n method: input.method,\n headers,\n ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),\n }),\n );\n const text = await response.text();\n return { status: response.status, text, body: parseJson(text) };\n }\n\n function expectOk(what: string, response: SimulatorResponse): SimulatorResponse {\n if (response.status !== 200) throw new DeviceSimulatorError(what, response);\n return response;\n }\n\n async function pair(pairingCode: string, deviceName = defaultDeviceName): Promise<DeviceSession> {\n const response = expectOk(\n 'pairing rejected',\n await request({\n method: 'POST',\n path: DEVICE_ROUTES.pair,\n credential: 'none',\n body: {\n pairingCode,\n deviceName,\n devicePublicKey: identity.publicKeyBase64Url,\n },\n }),\n );\n const parsed = PairResponseSchema.parse(response.body);\n session = { deviceId: parsed.deviceId, accessToken: parsed.accessToken };\n return session;\n }\n\n async function challenge(deviceId?: string): Promise<string> {\n const response = expectOk(\n 'challenge rejected',\n await request({\n method: 'POST',\n path: DEVICE_ROUTES.challenge,\n credential: 'none',\n body: { deviceId: deviceId ?? requireSession('challenge()').deviceId },\n }),\n );\n return ChallengeResponseSchema.parse(response.body).nonce;\n }\n\n async function token(input: {\n deviceId: string;\n nonce: string;\n signature: string;\n }): Promise<string> {\n const response = expectOk(\n 'token exchange rejected',\n await request({\n method: 'POST',\n path: DEVICE_ROUTES.token,\n credential: 'none',\n body: input,\n }),\n );\n return TokenResponseSchema.parse(response.body).accessToken;\n }\n\n async function renewAccessToken(): Promise<string> {\n const current = requireSession('renewAccessToken()');\n const nonce = await challenge(current.deviceId);\n const accessToken = await token({\n deviceId: current.deviceId,\n nonce,\n signature: await identity.signNonce(nonce),\n });\n session = { deviceId: current.deviceId, accessToken };\n return accessToken;\n }\n\n return {\n identity,\n get session() {\n return session;\n },\n\n pair,\n challenge,\n token,\n renewAccessToken,\n\n async publishPresence(level, detail, configuredToolsets) {\n requireSession('publishPresence()');\n expectOk(\n 'presence publication rejected',\n await request({\n method: 'PUT',\n path: DEVICE_ROUTES.presence,\n body: {\n level,\n ...(detail === undefined ? {} : { detail }),\n ...(configuredToolsets === undefined ? {} : { configuredToolsets }),\n },\n }),\n );\n },\n\n readHostPresence() {\n return requireHost('readHostPresence()').listPresence();\n },\n\n revoke() {\n return requireHost('revoke()').revokeDevice(requireSession('revoke()').deviceId);\n },\n\n request,\n };\n}\n","/**\n * The four negative assertions a host's pairing smoke test hand-writes.\n *\n * These are plain async functions on purpose. A negative assertion belongs to\n * the protocol, not to a test runner: the same four have to be runnable from\n * vitest, from a CI script, from a deployment gate, and from a Worker. So each\n * returns a structured {@link NegativeAssertionResult} instead of throwing —\n * the caller decides what a failure means, and a vitest consumer writes\n * `expect(result.ok).toBe(true)` and gets the observed status in the diff.\n *\n * Each one names a defense that must hold, and each is written so that pointing\n * it at the wrong input makes it FAIL. An assertion that cannot go red proves\n * nothing, and the ones here are checked that way in\n * `@byok-sdk/conformance`'s `pairing-simulator` suite before they are trusted\n * green: unauthenticated against a public route, single-use against an unused\n * code, undomained against a properly domained signature, revoked against a\n * device that was never revoked.\n */\nimport type { DeviceSimulator, SimulatorRequest, SimulatorResponse } from './simulator';\nimport { DEVICE_ROUTES } from './simulator';\n\nexport interface NegativeAssertionResult {\n /** Stable identifier — safe to use as a test name or a CI check id. */\n readonly name: string;\n readonly ok: boolean;\n /** What the protocol requires. */\n readonly expected: string;\n /** What the deployment actually did. */\n readonly actual: string;\n /** The response that decided it, when one status decided it. */\n readonly response?: SimulatorResponse;\n}\n\nfunction result(\n name: string,\n expected: string,\n response: SimulatorResponse,\n expectedStatus: number,\n): NegativeAssertionResult {\n return {\n name,\n ok: response.status === expectedStatus,\n expected,\n actual: `HTTP ${response.status}`,\n response,\n };\n}\n\n/**\n * A credentialed route answers 401 when the credential is absent.\n *\n * Defaults to `PUT /byok/presence` — the SDK's own credentialed HTTP surface is\n * device-class, because the host control plane (pairing-code mint, presence\n * read, revoke) is in-process and mounts no route. A deployment that publishes\n * its own admin routes passes one here and gets the same assertion against it.\n */\nexport async function assertUnauthenticatedRejected(\n simulator: DeviceSimulator,\n target: Omit<SimulatorRequest, 'credential'> = {\n method: 'PUT',\n path: DEVICE_ROUTES.presence,\n body: { level: 'online' },\n },\n): Promise<NegativeAssertionResult> {\n const response = await simulator.request({ ...target, credential: 'none' });\n return result(\n 'unauthenticated-request-rejected',\n `${target.method} ${target.path} without a credential is 401`,\n response,\n 401,\n );\n}\n\n/**\n * A pairing code is single-use (§6.1): redeeming an already-redeemed code is a\n * 401, indistinguishable from an unknown or expired one.\n *\n * Pass the code THIS simulator (or another one) already paired with. Pointing\n * it at a fresh code pairs a second device and returns `ok: false` — that is\n * the red form.\n */\nexport async function assertPairingCodeSingleUse(\n simulator: DeviceSimulator,\n redeemedPairingCode: string,\n deviceName = 'byok-testkit-replay',\n): Promise<NegativeAssertionResult> {\n const response = await simulator.request({\n method: 'POST',\n path: DEVICE_ROUTES.pair,\n credential: 'none',\n body: {\n pairingCode: redeemedPairingCode,\n deviceName,\n devicePublicKey: simulator.identity.publicKeyBase64Url,\n },\n });\n return result(\n 'pairing-code-single-use',\n 'a second redemption of the same pairing code is 401',\n response,\n 401,\n );\n}\n\n/**\n * A signature over the BARE nonce does not renew a token (§6.2, GAP-004).\n *\n * This is the one place in the package that signs something other than\n * `nonceSigningBytes` — and it signs *less*, never a second domain of its own.\n * The whole point is that the undomained encoding has no accepted form\n * anywhere, so there is no dual mode to accidentally validate.\n *\n * Set `domainSeparated: true` to get the red form: the correctly domained\n * signature renews the token, 200, and the assertion reports failure.\n */\nexport async function assertUndomainedSignatureRejected(\n simulator: DeviceSimulator,\n options: { readonly domainSeparated?: boolean } = {},\n): Promise<NegativeAssertionResult> {\n const session = simulator.session;\n if (session === undefined) {\n throw new Error('assertUndomainedSignatureRejected needs a paired device.');\n }\n\n const nonce = await simulator.challenge(session.deviceId);\n const signature =\n options.domainSeparated === true\n ? await simulator.identity.signNonce(nonce)\n : await simulator.identity.sign(new TextEncoder().encode(nonce));\n\n const response = await simulator.request({\n method: 'POST',\n path: DEVICE_ROUTES.token,\n credential: 'none',\n body: { deviceId: session.deviceId, nonce, signature },\n });\n return result(\n 'undomained-signature-rejected',\n 'a signature over the bare nonce, without the signing domain, is 401',\n response,\n 401,\n );\n}\n\n/**\n * A revoked device cannot start a renewal (§6.3): its next `/byok/challenge` is\n * a 401, indistinguishable from an unknown device.\n *\n * Does NOT revoke anything itself — revocation is the host's act, so the caller\n * revokes and then asserts. Calling it before revoking is the red form.\n */\nexport async function assertRevokedDeviceChallengeRejected(\n simulator: DeviceSimulator,\n): Promise<NegativeAssertionResult> {\n const session = simulator.session;\n if (session === undefined) {\n throw new Error('assertRevokedDeviceChallengeRejected needs a paired device.');\n }\n\n const response = await simulator.request({\n method: 'POST',\n path: DEVICE_ROUTES.challenge,\n credential: 'none',\n body: { deviceId: session.deviceId },\n });\n return result(\n 'revoked-device-challenge-rejected',\n 'a revoked device asking for a challenge is 401',\n response,\n 401,\n );\n}\n\nexport interface NegativeSuiteInput {\n /** A pairing code this simulator has already redeemed. */\n readonly redeemedPairingCode: string;\n /** Optional non-default target for the unauthenticated check (e.g. a host admin route). */\n readonly unauthenticatedTarget?: Omit<SimulatorRequest, 'credential'>;\n}\n\n/**\n * All four, in the only order they can run in: revocation is terminal for this\n * device, so the checks that need a live session go first.\n */\nexport async function runNegativeAssertions(\n simulator: DeviceSimulator,\n input: NegativeSuiteInput,\n): Promise<readonly NegativeAssertionResult[]> {\n const results: NegativeAssertionResult[] = [\n input.unauthenticatedTarget === undefined\n ? await assertUnauthenticatedRejected(simulator)\n : await assertUnauthenticatedRejected(simulator, input.unauthenticatedTarget),\n await assertPairingCodeSingleUse(simulator, input.redeemedPairingCode),\n await assertUndomainedSignatureRejected(simulator),\n ];\n await simulator.revoke();\n results.push(await assertRevokedDeviceChallengeRejected(simulator));\n return results;\n}\n\n/** Every failed assertion in `results`, for a caller that wants one error instead of four booleans. */\nexport function failedAssertions(\n results: readonly NegativeAssertionResult[],\n): readonly NegativeAssertionResult[] {\n return results.filter((entry) => !entry.ok);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byok-sdk/testkit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "BYOK SDK headless device simulator: protocol-level pairing, token renewal, presence, and the built-in negative assertions a host's smoke test would otherwise hand-write",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://github.com/Ancienttwo/byok-sdk#readme",
|
|
16
16
|
"engines": {
|
|
17
|
-
"node": ">=22.
|
|
17
|
+
"node": ">=22.22.0"
|
|
18
18
|
},
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"main": "./dist/index.js",
|
|
@@ -35,10 +35,6 @@
|
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@byok-sdk/core": "0.4.0",
|
|
40
|
-
"@byok-sdk/protocol": "0.4.0"
|
|
41
|
-
},
|
|
42
38
|
"scripts": {
|
|
43
39
|
"build": "tsup && tsc -p tsconfig.build.json",
|
|
44
40
|
"dev": "tsup --watch",
|
|
@@ -46,5 +42,9 @@
|
|
|
46
42
|
"test:watch": "vitest",
|
|
47
43
|
"typecheck": "tsc --noEmit",
|
|
48
44
|
"clean": "rm -rf dist"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@byok-sdk/core": "0.4.0",
|
|
48
|
+
"@byok-sdk/protocol": "0.4.0"
|
|
49
49
|
}
|
|
50
|
-
}
|
|
50
|
+
}
|