@byok-sdk/core 0.18.0 → 0.19.0

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.
@@ -52,16 +52,40 @@ export declare const DEVICE_ASSERTION_SCHEMA_ID = "byok-device-assertion-v1";
52
52
  * Domain separation prefix, prepended to the canonical claim bytes before
53
53
  * signing.
54
54
  *
55
- * Must remain mutually NON-PREFIX with the other two things this same Ed25519
55
+ * Must remain mutually NON-PREFIX with the other three things this same Ed25519
56
56
  * device key signs — `byok-nonce-v1\n` (challenge/token renewal, see
57
- * `@byok-sdk/client`'s `device-keys.ts`) and `byok-device-proof-v1\n`
58
- * (`attestation.ts`) — so no signature over one domain can ever be reinterpreted
59
- * as a signature over another. `packages/core/src/__tests__/device-assertion.test.ts`
60
- * asserts the three-way non-prefix property directly; that assertion is the
61
- * falsifier for this whole design, not a nicety.
57
+ * `@byok-sdk/client`'s `device-keys.ts`), `byok-device-proof-v1\n`
58
+ * (`attestation.ts`), and `byok-task-assertion-v1\n` ({@link
59
+ * TASK_ASSERTION_DOMAIN_PREFIX}) — so no signature over one domain can ever be
60
+ * reinterpreted as a signature over another.
61
+ * `packages/core/src/__tests__/device-assertion.test.ts` asserts the property
62
+ * across the three core-owned prefixes and
63
+ * `packages/client/src/__tests__/device-assertion-broker.test.ts` across all
64
+ * four; those assertions are the falsifier for this whole design, not a nicety.
62
65
  */
63
66
  export declare const DEVICE_ASSERTION_DOMAIN_PREFIX = "byok-device-assertion-v1\n";
64
67
  export declare const DEVICE_ASSERTION_VERSION = 1;
68
+ /**
69
+ * Envelope schema id of the task-scoped assertion (contract §8.1), kept beside
70
+ * the device one so the two are read — and reviewed — together.
71
+ */
72
+ export declare const TASK_ASSERTION_SCHEMA_ID = "byok-task-assertion-v1";
73
+ /**
74
+ * Domain separation prefix for `byok-task-assertion-v1`.
75
+ *
76
+ * Joins the mutually NON-PREFIX set the ONE Ed25519 device key signs under:
77
+ * `byok-nonce-v1\n`, `byok-device-proof-v1\n`,
78
+ * `byok-device-assertion-v1\n`, and this. Contract §8.1 requires the old and
79
+ * new envelopes to be non-interchangeable, and domain separation is the half of
80
+ * that which holds even when both envelopes parse: a device-lane signature can
81
+ * never be reinterpreted as a task-lane one, because the signed bytes differ in
82
+ * their first line. The four-way falsifier lives in
83
+ * `packages/client/src/__tests__/device-assertion-broker.test.ts` (the only
84
+ * suite that can import the real nonce domain); the three core-owned prefixes
85
+ * are cross-checked in `packages/core/src/__tests__/device-assertion.test.ts`.
86
+ */
87
+ export declare const TASK_ASSERTION_DOMAIN_PREFIX = "byok-task-assertion-v1\n";
88
+ export declare const TASK_ASSERTION_VERSION = 1;
65
89
  /** Signature algorithms this envelope version admits. */
66
90
  export declare const DEVICE_ASSERTION_ALGORITHMS: readonly ['ed25519'];
67
91
  export type DeviceAssertionAlgorithm = (typeof DEVICE_ASSERTION_ALGORITHMS)[number];
@@ -236,8 +260,21 @@ export interface DeviceAssertionExpectedBinding {
236
260
  readonly productId: string;
237
261
  readonly audience: string;
238
262
  }
263
+ /**
264
+ * The envelope kinds that share one replay authority.
265
+ *
266
+ * A closed union with no default: contract §8.2(2) requires the replay key to
267
+ * carry an envelope-kind discriminator so the same `jti` presented as a device
268
+ * assertion and as a task assertion occupies two key slots and neither lane can
269
+ * burn the other's. A defaulted or optional field would reintroduce exactly the
270
+ * collision the discriminator exists to prevent, so every caller states its
271
+ * lane and a caller that forgets does not compile.
272
+ */
273
+ export type DeviceAssertionReplaySchemaId = typeof DEVICE_ASSERTION_SCHEMA_ID | typeof TASK_ASSERTION_SCHEMA_ID;
239
274
  /** One replay key. Every field is derived from verified claims/current authority. */
240
275
  export interface DeviceAssertionReplayConsumeInput {
276
+ /** Which signed envelope kind produced this key. Required; never inferred, never defaulted. */
277
+ readonly schema: DeviceAssertionReplaySchemaId;
241
278
  readonly tenantId: TenantId;
242
279
  readonly issuer: string;
243
280
  readonly productId: string;
@@ -262,8 +299,13 @@ export interface AuthenticateDeviceAssertionDeps {
262
299
  readonly now: Date;
263
300
  readonly maxLifetimeMs?: number;
264
301
  }
265
- /** Audit-safe result of a consumed assertion; no credential or signature is retained. */
266
- export interface AuthenticatedDeviceAssertion {
302
+ /**
303
+ * What both lanes' results carry: the current-authority principal plus the
304
+ * audit fields of the credential that was spent. Never a name a caller holds —
305
+ * a value is always one lane or the other, and {@link AuthenticatedAssertion}
306
+ * is the union to accept when a consumer genuinely serves both.
307
+ */
308
+ interface AuthenticatedAssertionBase {
267
309
  readonly device: DevicePrincipal;
268
310
  readonly issuer: string;
269
311
  readonly audience: string;
@@ -271,6 +313,20 @@ export interface AuthenticatedDeviceAssertion {
271
313
  readonly issuedAt: string;
272
314
  readonly expiresAt: string;
273
315
  }
316
+ /**
317
+ * Audit-safe result of a consumed DEVICE assertion; no credential or signature
318
+ * is retained.
319
+ *
320
+ * `lane` is the discriminator contract §8.1 requires the result to carry. The
321
+ * two lanes have zero interchange, so a consumer that serves only one of them
322
+ * has to be able to say which — and "which fields are present" is not that
323
+ * statement: a structural check drifts the moment either claim set grows, and
324
+ * an extension relationship would make a task credential satisfy every
325
+ * device-lane type check for free.
326
+ */
327
+ export interface AuthenticatedDeviceAssertion extends AuthenticatedAssertionBase {
328
+ readonly lane: 'device';
329
+ }
274
330
  /**
275
331
  * Authenticate one device assertion and atomically consume its JTI.
276
332
  *
@@ -279,3 +335,180 @@ export interface AuthenticatedDeviceAssertion {
279
335
  * availability error without ever degrading to signature-only acceptance.
280
336
  */
281
337
  export declare function authenticateDeviceAssertion(input: unknown, deps: AuthenticateDeviceAssertionDeps): Promise<AuthenticatedDeviceAssertion | undefined>;
338
+ /**
339
+ * The task-scoped assertion: a separate envelope, NOT a device assertion with
340
+ * extra claims.
341
+ *
342
+ * Contract §8.1 fixes the property this section exists to hold: the old and new
343
+ * envelopes are not interchangeable in either direction. Three independent
344
+ * mechanisms carry it, and none of them is "the verifier remembers to check":
345
+ *
346
+ * 1. **Different signed bytes.** {@link TASK_ASSERTION_DOMAIN_PREFIX} is
347
+ * non-prefix with every other domain this device key signs under, so a
348
+ * signature made in one lane cannot be reinterpreted in another even if the
349
+ * claim sets were made to coincide.
350
+ * 2. **Different strict schemas.** `schema` is a literal and `protected` is a
351
+ * `strictObject`, so a device envelope fails
352
+ * {@link parseTaskAssertionEnvelope} on its missing `taskId`/`agentRef`/
353
+ * `toolsetId` and a task envelope fails
354
+ * {@link parseDeviceAssertionEnvelope} on those same claims being unknown.
355
+ * 3. **Different replay keys.** {@link DeviceAssertionReplayConsumeInput}
356
+ * carries a required `schema` segment (§8.2(2)), so the two lanes' ledgers
357
+ * are distinguishable and neither can burn the other's `jti`.
358
+ *
359
+ * What is deliberately NOT duplicated: `jti`/signature encodings, the audience
360
+ * byte bound, {@link DEVICE_ASSERTION_MAX_TTL_MS}, the window rule and the
361
+ * device-row rule are the SAME validators the device lane uses, referenced not
362
+ * copied. Contract §8.1: the task lane inherits the SDK's existing limits and
363
+ * does not define a second, looser set.
364
+ *
365
+ * The claim set carries no caller identity for the reason stated at the top of
366
+ * this file — under one UID every process can reach the control socket, so a
367
+ * self-reported "who asked" is synthesized authority. The task/AgentRef binding
368
+ * here is authority precisely because the daemon signs it after checking its
369
+ * own local registry, not because a caller sent it.
370
+ */
371
+ /** Bound on each AgentRef scalar, in UTF-8 bytes. Mirrors `AGENT_REF_MAX_BYTES` in `@byok-sdk/protocol`. */
372
+ export declare const TASK_ASSERTION_AGENT_REF_MAX_BYTES = 160;
373
+ /** Bound on `toolsetId`. Mirrors the `ToolsetIdSchema` bound in `@byok-sdk/protocol`. */
374
+ export declare const TASK_ASSERTION_TOOLSET_ID_MAX_LENGTH = 128;
375
+ /** The frozen offer's Agent identity. Same shape and same rules as the SDK's `AgentRefSchema`. */
376
+ export declare const TaskAssertionAgentRefSchema: z.ZodObject<{
377
+ agentId: z.ZodString;
378
+ profileRevision: z.ZodString;
379
+ }, z.core.$strict>;
380
+ export type TaskAssertionAgentRef = z.infer<typeof TaskAssertionAgentRefSchema>;
381
+ /**
382
+ * The signed task claim set: the device claim set plus the three bindings that
383
+ * make the assertion task-scoped, every member required for the same reason the
384
+ * device claims are.
385
+ */
386
+ export declare const TaskAssertionClaimsSchema: z.ZodObject<{
387
+ version: z.ZodLiteral<1>;
388
+ issuer: z.ZodString;
389
+ productId: z.ZodString;
390
+ deviceId: z.ZodString;
391
+ audience: z.ZodString;
392
+ jti: z.ZodString;
393
+ issuedAt: z.ZodISODateTime;
394
+ expiresAt: z.ZodISODateTime;
395
+ taskId: z.ZodString;
396
+ agentRef: z.ZodObject<{
397
+ agentId: z.ZodString;
398
+ profileRevision: z.ZodString;
399
+ }, z.core.$strict>;
400
+ toolsetId: z.ZodString;
401
+ }, z.core.$strict>;
402
+ export type TaskAssertionClaims = z.infer<typeof TaskAssertionClaimsSchema>;
403
+ export declare const TaskAssertionEnvelopeV1Schema: z.ZodObject<{
404
+ schema: z.ZodLiteral<"byok-task-assertion-v1">;
405
+ algorithm: z.ZodEnum<{
406
+ ed25519: "ed25519";
407
+ }>;
408
+ protected: z.ZodObject<{
409
+ version: z.ZodLiteral<1>;
410
+ issuer: z.ZodString;
411
+ productId: z.ZodString;
412
+ deviceId: z.ZodString;
413
+ audience: z.ZodString;
414
+ jti: z.ZodString;
415
+ issuedAt: z.ZodISODateTime;
416
+ expiresAt: z.ZodISODateTime;
417
+ taskId: z.ZodString;
418
+ agentRef: z.ZodObject<{
419
+ agentId: z.ZodString;
420
+ profileRevision: z.ZodString;
421
+ }, z.core.$strict>;
422
+ toolsetId: z.ZodString;
423
+ }, z.core.$strict>;
424
+ signature: z.ZodString;
425
+ }, z.core.$strict>;
426
+ export type TaskAssertionEnvelopeV1 = z.infer<typeof TaskAssertionEnvelopeV1Schema>;
427
+ /**
428
+ * Parses a task envelope fail-closed. A device assertion is not a degraded task
429
+ * assertion and is rejected here, not tolerated (§8.1: task lane zero fallback).
430
+ *
431
+ * @throws {ByokCoreError} code `assertion_envelope_invalid`.
432
+ */
433
+ export declare function parseTaskAssertionEnvelope(input: unknown): TaskAssertionEnvelopeV1;
434
+ /**
435
+ * Projects task claims into the exact JSON object that gets canonicalized.
436
+ *
437
+ * Built field by field, like the device projection, and covering EVERY claim —
438
+ * contract §8.1 requires the canonical projection to include all added claims,
439
+ * so an unsigned `taskId`, `agentRef` or `toolsetId` would be a claim a verifier
440
+ * reads but no signature covers.
441
+ */
442
+ export declare function taskAssertionCanonicalClaims(claims: TaskAssertionClaims): JsonObject;
443
+ /** Canonical JSON text of the task claim set, without the domain prefix. */
444
+ export declare function taskAssertionCanonicalJson(claims: TaskAssertionClaims): string;
445
+ /**
446
+ * The exact bytes a daemon signs and a verifier reconstructs:
447
+ * `byok-task-assertion-v1\n` followed by the canonical claim JSON, UTF-8
448
+ * encoded.
449
+ *
450
+ * Frozen by `src/__tests__/golden/task-assertion-v1.canonical.json`.
451
+ */
452
+ export declare function taskAssertionSigningInput(claims: TaskAssertionClaims): Uint8Array;
453
+ /**
454
+ * Verifies a task assertion and returns its claims, or `undefined`.
455
+ *
456
+ * The twin of {@link verifyDeviceAssertion}, with the same
457
+ * collapse-to-`undefined` discipline, the same injected row lookup, and the
458
+ * same window and ceiling rules ({@link assertionWindowAdmits},
459
+ * {@link resolveMaxLifetimeMs}) — and a schema this function will not widen: a
460
+ * `byok-device-assertion-v1` envelope is rejected at the parse step.
461
+ *
462
+ * What the caller MUST still do, exactly as in the device lane: compare
463
+ * `issuer`/`productId`/`audience` against its own deployment, burn the `jti`
464
+ * under this envelope's `schema` segment, and check that `taskId`/`agentRef`/
465
+ * `toolsetId` belong to the frozen offer (§8.2(2)) — the last of which is Host
466
+ * authority that core cannot hold.
467
+ */
468
+ export declare function verifyTaskAssertion(input: unknown, deps: DeviceAssertionVerifyDeps): Promise<TaskAssertionClaims | undefined>;
469
+ /**
470
+ * Audit-safe result of a consumed TASK assertion; no credential or signature is
471
+ * retained.
472
+ *
473
+ * Deliberately NOT an extension of {@link AuthenticatedDeviceAssertion}. It was
474
+ * one, and that was the same mistake in the type system that §8.1 forbids on
475
+ * the wire: an extension is assignable to the thing it extends, so every
476
+ * consumer written for device authority would have accepted a task credential
477
+ * silently, with no fallback ever written down. The two results share
478
+ * {@link AuthenticatedAssertionBase} and differ in `lane`, so a consumer that
479
+ * must accept exactly one of them compares one field, and a consumer that must
480
+ * accept either says so with {@link AuthenticatedAssertion}.
481
+ */
482
+ export interface AuthenticatedTaskAssertion extends AuthenticatedAssertionBase {
483
+ readonly lane: 'task';
484
+ readonly taskId: string;
485
+ readonly agentRef: TaskAssertionAgentRef;
486
+ readonly toolsetId: string;
487
+ }
488
+ /**
489
+ * Either lane's result, discriminated by `lane`.
490
+ *
491
+ * For the consumer that genuinely serves both (an audit projection, a ledger
492
+ * writer). A consumer that serves ONE lane must name that lane's type instead —
493
+ * this union exists so "both" is stated on purpose, not reached by accident.
494
+ */
495
+ export type AuthenticatedAssertion = AuthenticatedDeviceAssertion | AuthenticatedTaskAssertion;
496
+ /**
497
+ * Authenticate one task assertion and atomically consume its JTI under the
498
+ * `byok-task-assertion-v1` replay segment.
499
+ *
500
+ * Identical in shape and failure discipline to
501
+ * {@link authenticateDeviceAssertion}: every invalid state collapses to
502
+ * `undefined`, and a replay-store operational failure REJECTS rather than
503
+ * degrading to signature-only acceptance (§8.2(2): an unavailable replay store
504
+ * fails, it does not downgrade authentication).
505
+ *
506
+ * Consumption is the last step, after every binding comparison, so a rejected
507
+ * assertion never burns a key — the "reject before side effects" ordering AC11
508
+ * requires. The frozen-offer check (`taskId`/`agentRef`/`toolsetId` belong to
509
+ * this Execution) is deliberately NOT here: that authority lives in the Host,
510
+ * and a core function that accepted it as an argument would be inviting a
511
+ * caller to synthesize it.
512
+ */
513
+ export declare function authenticateTaskAssertion(input: unknown, deps: AuthenticateDeviceAssertionDeps): Promise<AuthenticatedTaskAssertion | undefined>;
514
+ export {};
package/dist/index.d.ts CHANGED
@@ -39,7 +39,8 @@ export { CORE_NON_COMPOSITION_PORT_NAMES, CORE_PORT_INTERFACES, CORE_PORT_METHOD
39
39
  export { DEVICE_PROOF_ALGORITHMS, DEVICE_PROOF_DOMAIN_PREFIX, DEVICE_PROOF_HEADER, DEVICE_PROOF_SCHEMA_ID, DEVICE_PROOF_VERSION, DeviceProofEnvelopeV1Schema, DeviceProofProtectedClaimsSchema, canonicalizeJson, canonicalizeJsonBytes, deviceProofCanonicalClaims, deviceProofCanonicalJson, deviceProofSigningInput, parseDeviceProofEnvelope, } from './attestation';
40
40
  export type { DeviceProofAlgorithm, DeviceProofEnvelopeV1, DeviceProofProtectedClaims, DeviceProofVerifier, DeviceProofVerifyInput, JsonObject, JsonPrimitive, JsonValue, } from './attestation';
41
41
  export { authenticateDeviceAssertion, DEVICE_ASSERTION_ALGORITHMS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_DOMAIN_PREFIX, DEVICE_ASSERTION_MAX_TTL_MS, DEVICE_ASSERTION_SCHEMA_ID, DEVICE_ASSERTION_VERSION, DeviceAssertionClaimsSchema, DeviceAssertionEnvelopeV1Schema, deviceAssertionCanonicalClaims, deviceAssertionCanonicalJson, deviceAssertionSigningInput, parseDeviceAssertionEnvelope, verifyDeviceAssertion, } from './device-assertion';
42
- export type { AuthenticateDeviceAssertionDeps, AuthenticatedDeviceAssertion, DeviceAssertionAlgorithm, DeviceAssertionAuthorityRow, DeviceAssertionClaims, DeviceAssertionDeviceRow, DeviceAssertionEnvelopeV1, DeviceAssertionExpectedBinding, DeviceAssertionReplayConsumeInput, DeviceAssertionReplayAuthority, DeviceAssertionVerifier, DeviceAssertionVerifyDeps, DeviceAssertionVerifyInput, } from './device-assertion';
42
+ export { authenticateTaskAssertion, TASK_ASSERTION_AGENT_REF_MAX_BYTES, TASK_ASSERTION_DOMAIN_PREFIX, TASK_ASSERTION_SCHEMA_ID, TASK_ASSERTION_TOOLSET_ID_MAX_LENGTH, TASK_ASSERTION_VERSION, TaskAssertionAgentRefSchema, TaskAssertionClaimsSchema, TaskAssertionEnvelopeV1Schema, parseTaskAssertionEnvelope, taskAssertionCanonicalClaims, taskAssertionCanonicalJson, taskAssertionSigningInput, verifyTaskAssertion, } from './device-assertion';
43
+ export type { AuthenticateDeviceAssertionDeps, AuthenticatedAssertion, AuthenticatedDeviceAssertion, AuthenticatedTaskAssertion, DeviceAssertionAlgorithm, DeviceAssertionAuthorityRow, DeviceAssertionClaims, DeviceAssertionDeviceRow, DeviceAssertionEnvelopeV1, DeviceAssertionExpectedBinding, DeviceAssertionReplayConsumeInput, DeviceAssertionReplayAuthority, DeviceAssertionReplaySchemaId, DeviceAssertionVerifier, DeviceAssertionVerifyDeps, DeviceAssertionVerifyInput, TaskAssertionAgentRef, TaskAssertionClaims, TaskAssertionEnvelopeV1, } from './device-assertion';
43
44
  export { NONCE_SIGNING_DOMAIN, nonceSigningBytes } from './pairing';
44
45
  export { IN_MEMORY_CLOCK_EPOCH, InMemoryBoardStore, InMemoryMailboxStore, InMemoryDeviceAssertionReplayAuthority, InMemoryObjectStore, InMemoryPresenceStore, InMemoryQuotaStore, InMemorySkillPackStore, InMemoryTruthStore, createInMemoryCoreStores, createInMemoryCoreCompositionWithClock, createMutableClock, } from './in-memory/index';
45
46
  export type { InMemoryCoreComposition, InMemoryCoreOptions } from './in-memory/index';
package/dist/index.js CHANGED
@@ -746,6 +746,9 @@ function deviceProofSigningInput(claims) {
746
746
  var DEVICE_ASSERTION_SCHEMA_ID = "byok-device-assertion-v1";
747
747
  var DEVICE_ASSERTION_DOMAIN_PREFIX = "byok-device-assertion-v1\n";
748
748
  var DEVICE_ASSERTION_VERSION = 1;
749
+ var TASK_ASSERTION_SCHEMA_ID = "byok-task-assertion-v1";
750
+ var TASK_ASSERTION_DOMAIN_PREFIX = "byok-task-assertion-v1\n";
751
+ var TASK_ASSERTION_VERSION = 1;
749
752
  var DEVICE_ASSERTION_ALGORITHMS = ["ed25519"];
750
753
  var DEVICE_ASSERTION_DEFAULT_TTL_MS = 12e4;
751
754
  var DEVICE_ASSERTION_MAX_TTL_MS = 3e5;
@@ -755,6 +758,9 @@ var SIGNATURE_PATTERN = /^[A-Za-z0-9_-]{86}$/;
755
758
  function utf8ByteLength(value) {
756
759
  return new TextEncoder().encode(value).length;
757
760
  }
761
+ var ASSERTION_AUDIENCE = z.string().min(1).refine((value) => utf8ByteLength(value) <= DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, {
762
+ message: `audience must be at most ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
763
+ });
758
764
  var DeviceAssertionClaimsSchema = z.strictObject({
759
765
  version: z.literal(DEVICE_ASSERTION_VERSION),
760
766
  /** The paired server's origin, normalized (`new URL(serverUrl).origin`). */
@@ -763,9 +769,7 @@ var DeviceAssertionClaimsSchema = z.strictObject({
763
769
  /** The device row this assertion claims to be. A lookup key, never authority — the row is the authority. */
764
770
  deviceId: z.string().min(1),
765
771
  /** Exactly one audience, compared with `===` by every verifier. Never an array, never a prefix. */
766
- audience: z.string().min(1).refine((value) => utf8ByteLength(value) <= DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, {
767
- message: `audience must be at most ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
768
- }),
772
+ audience: ASSERTION_AUDIENCE,
769
773
  /** 128-bit CSPRNG token, base64url unpadded. The verifier burns it; the signer never reuses one. */
770
774
  jti: z.string().regex(JTI_PATTERN),
771
775
  issuedAt: z.iso.datetime(),
@@ -821,11 +825,26 @@ function isUsableDeviceRow(row) {
821
825
  const key = row.publicKeyJwkX;
822
826
  return typeof key === "string" && EDDSA_JWK_X_PATTERN.test(key);
823
827
  }
824
- async function verifyDeviceAssertion(input, deps) {
825
- const maxLifetimeMs = deps.maxLifetimeMs ?? DEVICE_ASSERTION_MAX_TTL_MS;
828
+ function resolveMaxLifetimeMs(requested) {
829
+ const maxLifetimeMs = requested ?? DEVICE_ASSERTION_MAX_TTL_MS;
826
830
  if (!Number.isSafeInteger(maxLifetimeMs) || maxLifetimeMs <= 0 || maxLifetimeMs > DEVICE_ASSERTION_MAX_TTL_MS) {
827
831
  return void 0;
828
832
  }
833
+ return maxLifetimeMs;
834
+ }
835
+ function assertionWindowAdmits(claims, now, maxLifetimeMs) {
836
+ const issuedAt = parseInstant(claims.issuedAt);
837
+ const expiresAt = parseInstant(claims.expiresAt);
838
+ if (issuedAt === void 0 || expiresAt === void 0) return false;
839
+ if (expiresAt <= issuedAt) return false;
840
+ if (expiresAt - issuedAt > maxLifetimeMs) return false;
841
+ const instant = now.getTime();
842
+ if (!Number.isFinite(instant)) return false;
843
+ return instant >= issuedAt && instant < expiresAt;
844
+ }
845
+ async function verifyDeviceAssertion(input, deps) {
846
+ const maxLifetimeMs = resolveMaxLifetimeMs(deps.maxLifetimeMs);
847
+ if (maxLifetimeMs === void 0) return void 0;
829
848
  let envelope;
830
849
  try {
831
850
  envelope = parseDeviceAssertionEnvelope(input);
@@ -835,14 +854,7 @@ async function verifyDeviceAssertion(input, deps) {
835
854
  const claims = envelope.protected;
836
855
  const row = await deps.lookupDevice(claims.deviceId);
837
856
  if (!isUsableDeviceRow(row)) return void 0;
838
- const issuedAt = parseInstant(claims.issuedAt);
839
- const expiresAt = parseInstant(claims.expiresAt);
840
- if (issuedAt === void 0 || expiresAt === void 0) return void 0;
841
- if (expiresAt <= issuedAt) return void 0;
842
- if (expiresAt - issuedAt > maxLifetimeMs) return void 0;
843
- const now = deps.now.getTime();
844
- if (!Number.isFinite(now)) return void 0;
845
- if (now < issuedAt || now >= expiresAt) return void 0;
857
+ if (!assertionWindowAdmits(claims, deps.now, maxLifetimeMs)) return void 0;
846
858
  const verified = await deps.verifier.verify({
847
859
  algorithm: envelope.algorithm,
848
860
  // The row's key, never an envelope-supplied one.
@@ -882,6 +894,7 @@ async function authenticateDeviceAssertion(input, deps) {
882
894
  return void 0;
883
895
  }
884
896
  const consumed = await deps.replay.consume({
897
+ schema: DEVICE_ASSERTION_SCHEMA_ID,
885
898
  tenantId: authorityRow.tenantId,
886
899
  issuer: claims.issuer,
887
900
  productId: authorityRow.productId,
@@ -892,6 +905,7 @@ async function authenticateDeviceAssertion(input, deps) {
892
905
  });
893
906
  if (!consumed) return void 0;
894
907
  return {
908
+ lane: "device",
895
909
  device: {
896
910
  kind: "device",
897
911
  tenantId: authorityRow.tenantId,
@@ -905,6 +919,162 @@ async function authenticateDeviceAssertion(input, deps) {
905
919
  expiresAt: claims.expiresAt
906
920
  };
907
921
  }
922
+ var TASK_ASSERTION_AGENT_REF_MAX_BYTES = 160;
923
+ var TASK_ASSERTION_TOOLSET_ID_MAX_LENGTH = 128;
924
+ var AGENT_REF_VALUE = z.string().min(1).max(TASK_ASSERTION_AGENT_REF_MAX_BYTES).refine((value) => utf8ByteLength(value) <= TASK_ASSERTION_AGENT_REF_MAX_BYTES, {
925
+ message: `AgentRef values must not exceed ${TASK_ASSERTION_AGENT_REF_MAX_BYTES} UTF-8 bytes`
926
+ }).regex(/^[^\u0000-\u001f\u007f\r\n]+$/u, "AgentRef values must not contain control characters");
927
+ var WINDOWS_RESERVED_AGENT_SEGMENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu;
928
+ var TaskAssertionAgentRefSchema = z.object({
929
+ agentId: AGENT_REF_VALUE.regex(/^[^\\/:<>"|?*]+$/u, "agentId must be one portable pathname segment").refine((value) => value !== "." && value !== "..", "agentId must not be a dot segment").refine(
930
+ (value) => !/[. ]$/u.test(value) && !WINDOWS_RESERVED_AGENT_SEGMENT.test(value),
931
+ "agentId must be a portable Windows-safe pathname segment"
932
+ ),
933
+ /**
934
+ * From the FROZEN offer. Contract §8.1 forbids substituting the current
935
+ * revision read from a working directory's `profile.json`; this module
936
+ * cannot enforce that (it never reads a file) — the signer's contract does.
937
+ */
938
+ profileRevision: AGENT_REF_VALUE
939
+ }).strict();
940
+ var TOOLSET_ID_VALUE = z.string().min(1).max(TASK_ASSERTION_TOOLSET_ID_MAX_LENGTH).regex(/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u, "toolset ids must be lowercase logical identifiers");
941
+ var TASK_ID_VALUE = z.string().min(1);
942
+ var TaskAssertionClaimsSchema = z.strictObject({
943
+ version: z.literal(TASK_ASSERTION_VERSION),
944
+ /** The paired server's origin, normalized (`new URL(serverUrl).origin`). */
945
+ issuer: z.string().min(1),
946
+ productId: z.string().min(1),
947
+ /** The device row this assertion claims to be. A lookup key, never authority. */
948
+ deviceId: z.string().min(1),
949
+ /** Exactly one audience, compared with `===`. Participates in the replay key (§8.2(2)). */
950
+ audience: ASSERTION_AUDIENCE,
951
+ /** Same 128-bit CSPRNG token shape the device lane uses; burned once per `schema` segment. */
952
+ jti: z.string().regex(JTI_PATTERN),
953
+ issuedAt: z.iso.datetime(),
954
+ expiresAt: z.iso.datetime(),
955
+ /** The only execution locator (§8.1). No attemptId is minted beside it. */
956
+ taskId: TASK_ID_VALUE,
957
+ /** The frozen offer's Agent identity, at the SDK's full AgentRef strength. */
958
+ agentRef: TaskAssertionAgentRefSchema,
959
+ /** The frozen toolset this invocation belongs to (the wire's `toolset` field). */
960
+ toolsetId: TOOLSET_ID_VALUE
961
+ });
962
+ var TaskAssertionEnvelopeV1Schema = z.strictObject({
963
+ schema: z.literal(TASK_ASSERTION_SCHEMA_ID),
964
+ algorithm: z.enum(DEVICE_ASSERTION_ALGORITHMS),
965
+ protected: TaskAssertionClaimsSchema,
966
+ /** Raw 64-byte Ed25519 signature, base64url unpadded — the device lane's encoding, unchanged. */
967
+ signature: z.string().regex(SIGNATURE_PATTERN)
968
+ });
969
+ function parseTaskAssertionEnvelope(input) {
970
+ const result = TaskAssertionEnvelopeV1Schema.safeParse(input);
971
+ if (!result.success) {
972
+ throw new ByokCoreError(
973
+ "assertion_envelope_invalid",
974
+ `Invalid task assertion envelope: ${result.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`,
975
+ { cause: result.error }
976
+ );
977
+ }
978
+ return result.data;
979
+ }
980
+ function taskAssertionCanonicalClaims(claims) {
981
+ const canonical = {
982
+ version: claims.version,
983
+ issuer: claims.issuer,
984
+ productId: claims.productId,
985
+ deviceId: claims.deviceId,
986
+ audience: claims.audience,
987
+ jti: claims.jti,
988
+ issuedAt: claims.issuedAt,
989
+ expiresAt: claims.expiresAt,
990
+ taskId: claims.taskId,
991
+ agentRef: {
992
+ agentId: claims.agentRef.agentId,
993
+ profileRevision: claims.agentRef.profileRevision
994
+ },
995
+ toolsetId: claims.toolsetId
996
+ };
997
+ return canonical;
998
+ }
999
+ function taskAssertionCanonicalJson(claims) {
1000
+ return canonicalizeJson(taskAssertionCanonicalClaims(claims));
1001
+ }
1002
+ function taskAssertionSigningInput(claims) {
1003
+ return new TextEncoder().encode(
1004
+ TASK_ASSERTION_DOMAIN_PREFIX + taskAssertionCanonicalJson(claims)
1005
+ );
1006
+ }
1007
+ async function verifyTaskAssertion(input, deps) {
1008
+ const maxLifetimeMs = resolveMaxLifetimeMs(deps.maxLifetimeMs);
1009
+ if (maxLifetimeMs === void 0) return void 0;
1010
+ let envelope;
1011
+ try {
1012
+ envelope = parseTaskAssertionEnvelope(input);
1013
+ } catch {
1014
+ return void 0;
1015
+ }
1016
+ const claims = envelope.protected;
1017
+ const row = await deps.lookupDevice(claims.deviceId);
1018
+ if (!isUsableDeviceRow(row)) return void 0;
1019
+ if (!assertionWindowAdmits(claims, deps.now, maxLifetimeMs)) return void 0;
1020
+ const verified = await deps.verifier.verify({
1021
+ algorithm: envelope.algorithm,
1022
+ publicKey: row.publicKeyJwkX,
1023
+ signature: envelope.signature,
1024
+ signingInput: taskAssertionSigningInput(claims)
1025
+ });
1026
+ if (!verified) return void 0;
1027
+ return claims;
1028
+ }
1029
+ async function authenticateTaskAssertion(input, deps) {
1030
+ if (!isNonEmptyExactString(deps.expected.issuer) || !isNonEmptyExactString(deps.expected.productId) || !isNonEmptyExactString(deps.expected.audience) || utf8ByteLength(deps.expected.audience) > DEVICE_ASSERTION_AUDIENCE_MAX_BYTES) {
1031
+ return void 0;
1032
+ }
1033
+ let authorityRow;
1034
+ const claims = await verifyTaskAssertion(input, {
1035
+ verifier: deps.verifier,
1036
+ lookupDevice: async (deviceId) => {
1037
+ const row = await deps.lookupDevice(deviceId);
1038
+ if (!isAuthorityRow(row, deviceId)) return void 0;
1039
+ authorityRow = row;
1040
+ return { publicKeyJwkX: row.publicKeyJwkX, revoked: row.revoked };
1041
+ },
1042
+ now: deps.now,
1043
+ ...deps.maxLifetimeMs === void 0 ? {} : { maxLifetimeMs: deps.maxLifetimeMs }
1044
+ });
1045
+ if (claims === void 0 || authorityRow === void 0) return void 0;
1046
+ if (claims.issuer !== deps.expected.issuer || claims.productId !== deps.expected.productId || claims.audience !== deps.expected.audience || authorityRow.productId !== deps.expected.productId || authorityRow.deviceId !== claims.deviceId) {
1047
+ return void 0;
1048
+ }
1049
+ const consumed = await deps.replay.consume({
1050
+ schema: TASK_ASSERTION_SCHEMA_ID,
1051
+ tenantId: authorityRow.tenantId,
1052
+ issuer: claims.issuer,
1053
+ productId: authorityRow.productId,
1054
+ deviceId: authorityRow.deviceId,
1055
+ audience: claims.audience,
1056
+ jti: claims.jti,
1057
+ expiresAt: claims.expiresAt
1058
+ });
1059
+ if (!consumed) return void 0;
1060
+ return {
1061
+ lane: "task",
1062
+ device: {
1063
+ kind: "device",
1064
+ tenantId: authorityRow.tenantId,
1065
+ productId: authorityRow.productId,
1066
+ deviceId: authorityRow.deviceId
1067
+ },
1068
+ issuer: claims.issuer,
1069
+ audience: claims.audience,
1070
+ jti: claims.jti,
1071
+ issuedAt: claims.issuedAt,
1072
+ expiresAt: claims.expiresAt,
1073
+ taskId: claims.taskId,
1074
+ agentRef: claims.agentRef,
1075
+ toolsetId: claims.toolsetId
1076
+ };
1077
+ }
908
1078
 
909
1079
  // src/pairing.ts
910
1080
  var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
@@ -2013,6 +2183,7 @@ function replayKey(input) {
2013
2183
  input.productId,
2014
2184
  input.deviceId,
2015
2185
  input.audience,
2186
+ input.schema,
2016
2187
  input.jti
2017
2188
  ]);
2018
2189
  }
@@ -2063,6 +2234,6 @@ function createInMemoryCoreCompositionWithClock() {
2063
2234
  return { stores: createInMemoryCoreStores({ clock }).stores, clock };
2064
2235
  }
2065
2236
 
2066
- export { BOARD_STATUSES, BOARD_TRANSITIONS, ByokCoreError, CANONICAL_TIMESTAMP_PATTERN, CAPABILITY_DECLARATION_SCHEMA_ID, CAPABILITY_NAME_PATTERN, CONTENT_HASH_PATTERN, CORE_ERROR_CODES, CORE_NON_COMPOSITION_PORT_NAMES, CORE_PORT_INTERFACES, CORE_PORT_METHODS, CORE_STORE_NAMES, CapabilityDeclarationSchema, CoreConflictError, DEVICE_ASSERTION_ALGORITHMS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_DOMAIN_PREFIX, DEVICE_ASSERTION_MAX_TTL_MS, DEVICE_ASSERTION_SCHEMA_ID, DEVICE_ASSERTION_VERSION, DEVICE_PROOF_ALGORITHMS, DEVICE_PROOF_DOMAIN_PREFIX, DEVICE_PROOF_HEADER, DEVICE_PROOF_SCHEMA_ID, DEVICE_PROOF_VERSION, DeviceAssertionClaimsSchema, DeviceAssertionEnvelopeV1Schema, DeviceProofEnvelopeV1Schema, DeviceProofProtectedClaimsSchema, IN_MEMORY_CLOCK_EPOCH, InMemoryBoardStore, InMemoryDeviceAssertionReplayAuthority, InMemoryMailboxStore, InMemoryObjectStore, InMemoryPresenceStore, InMemoryQuotaStore, InMemorySkillPackStore, InMemoryTruthStore, MAILBOX_MESSAGE_STATES, NONCE_SIGNING_DOMAIN, OBJECT_KEY_PREFIX_PATTERN, OBJECT_STATES, OBJECT_STATE_TRANSITIONS, PRESENCE_LEVELS, PRINCIPAL_KINDS, SKILL_FRONTMATTER_FIELDS, SKILL_PACK_DESCRIPTION_MAX_LENGTH, SKILL_PACK_ENTRY_PATH, SKILL_PACK_FILE_MAX_BYTES, SKILL_PACK_FILE_PATH_MAX_LENGTH, SKILL_PACK_FILE_PATH_PATTERN, SKILL_PACK_FORBIDDEN_FIELDS, SKILL_PACK_MANIFEST_SCHEMA_ID, SKILL_PACK_MAX_BYTES, SKILL_PACK_MAX_FILES, SKILL_PACK_NAME_MAX_LENGTH, SKILL_PACK_NAME_PATTERN, SKILL_PACK_REJECTIONS, SKILL_PACK_VERSION_PATTERN, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS, STORAGE_RESERVATION_STATES, STORAGE_WRITE_KINDS, STORAGE_WRITE_POSTURES, SkillPackFileSchema, SkillPackManifestSchema, TENANT_ID_MAX_LENGTH, TENANT_KEY_SEPARATOR, TRUTH_RECORD_KINDS, assertCanonicalTimestamp, assertCapability, authenticateDeviceAssertion, canonicalizeJson, canonicalizeJsonBytes, checkSkillPackEntry, checkSkillPackFileContent, checkSkillPackManifest, contentHash, createInMemoryCoreCompositionWithClock, createInMemoryCoreStores, createMutableClock, deviceAssertionCanonicalClaims, deviceAssertionCanonicalJson, deviceAssertionSigningInput, deviceProofCanonicalClaims, deviceProofCanonicalJson, deviceProofSigningInput, hasCapability, isCanonicalTimestamp, isContentHash, isControlPlanePrincipal, isCoreConflictError, isCoreError, isDevicePrincipal, isLegalBoardTransition, isLegalObjectTransition, isSkillPackPathSafe, isTenantId, nonceSigningBytes, objectKeyPrefix, parseCapabilityDeclaration, parseDeviceAssertionEnvelope, parseDeviceProofEnvelope, parseSkillFrontmatter, parseSkillPackManifest, principalTenant, skillPackContentHashInput, tenantId, tenantKey, tenantObjectKey, verifyDeviceAssertion };
2237
+ export { BOARD_STATUSES, BOARD_TRANSITIONS, ByokCoreError, CANONICAL_TIMESTAMP_PATTERN, CAPABILITY_DECLARATION_SCHEMA_ID, CAPABILITY_NAME_PATTERN, CONTENT_HASH_PATTERN, CORE_ERROR_CODES, CORE_NON_COMPOSITION_PORT_NAMES, CORE_PORT_INTERFACES, CORE_PORT_METHODS, CORE_STORE_NAMES, CapabilityDeclarationSchema, CoreConflictError, DEVICE_ASSERTION_ALGORITHMS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_DOMAIN_PREFIX, DEVICE_ASSERTION_MAX_TTL_MS, DEVICE_ASSERTION_SCHEMA_ID, DEVICE_ASSERTION_VERSION, DEVICE_PROOF_ALGORITHMS, DEVICE_PROOF_DOMAIN_PREFIX, DEVICE_PROOF_HEADER, DEVICE_PROOF_SCHEMA_ID, DEVICE_PROOF_VERSION, DeviceAssertionClaimsSchema, DeviceAssertionEnvelopeV1Schema, DeviceProofEnvelopeV1Schema, DeviceProofProtectedClaimsSchema, IN_MEMORY_CLOCK_EPOCH, InMemoryBoardStore, InMemoryDeviceAssertionReplayAuthority, InMemoryMailboxStore, InMemoryObjectStore, InMemoryPresenceStore, InMemoryQuotaStore, InMemorySkillPackStore, InMemoryTruthStore, MAILBOX_MESSAGE_STATES, NONCE_SIGNING_DOMAIN, OBJECT_KEY_PREFIX_PATTERN, OBJECT_STATES, OBJECT_STATE_TRANSITIONS, PRESENCE_LEVELS, PRINCIPAL_KINDS, SKILL_FRONTMATTER_FIELDS, SKILL_PACK_DESCRIPTION_MAX_LENGTH, SKILL_PACK_ENTRY_PATH, SKILL_PACK_FILE_MAX_BYTES, SKILL_PACK_FILE_PATH_MAX_LENGTH, SKILL_PACK_FILE_PATH_PATTERN, SKILL_PACK_FORBIDDEN_FIELDS, SKILL_PACK_MANIFEST_SCHEMA_ID, SKILL_PACK_MAX_BYTES, SKILL_PACK_MAX_FILES, SKILL_PACK_NAME_MAX_LENGTH, SKILL_PACK_NAME_PATTERN, SKILL_PACK_REJECTIONS, SKILL_PACK_VERSION_PATTERN, STORAGE_ERROR_CODES, STORAGE_ERROR_HTTP_STATUS, STORAGE_RESERVATION_STATES, STORAGE_WRITE_KINDS, STORAGE_WRITE_POSTURES, SkillPackFileSchema, SkillPackManifestSchema, TASK_ASSERTION_AGENT_REF_MAX_BYTES, TASK_ASSERTION_DOMAIN_PREFIX, TASK_ASSERTION_SCHEMA_ID, TASK_ASSERTION_TOOLSET_ID_MAX_LENGTH, TASK_ASSERTION_VERSION, TENANT_ID_MAX_LENGTH, TENANT_KEY_SEPARATOR, TRUTH_RECORD_KINDS, TaskAssertionAgentRefSchema, TaskAssertionClaimsSchema, TaskAssertionEnvelopeV1Schema, assertCanonicalTimestamp, assertCapability, authenticateDeviceAssertion, authenticateTaskAssertion, canonicalizeJson, canonicalizeJsonBytes, checkSkillPackEntry, checkSkillPackFileContent, checkSkillPackManifest, contentHash, createInMemoryCoreCompositionWithClock, createInMemoryCoreStores, createMutableClock, deviceAssertionCanonicalClaims, deviceAssertionCanonicalJson, deviceAssertionSigningInput, deviceProofCanonicalClaims, deviceProofCanonicalJson, deviceProofSigningInput, hasCapability, isCanonicalTimestamp, isContentHash, isControlPlanePrincipal, isCoreConflictError, isCoreError, isDevicePrincipal, isLegalBoardTransition, isLegalObjectTransition, isSkillPackPathSafe, isTenantId, nonceSigningBytes, objectKeyPrefix, parseCapabilityDeclaration, parseDeviceAssertionEnvelope, parseDeviceProofEnvelope, parseSkillFrontmatter, parseSkillPackManifest, parseTaskAssertionEnvelope, principalTenant, skillPackContentHashInput, taskAssertionCanonicalClaims, taskAssertionCanonicalJson, taskAssertionSigningInput, tenantId, tenantKey, tenantObjectKey, verifyDeviceAssertion, verifyTaskAssertion };
2067
2238
  //# sourceMappingURL=index.js.map
2068
2239
  //# sourceMappingURL=index.js.map