@orangecheck/agent-core 2.5.1 → 2.7.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @orangecheck/agent-core
2
2
 
3
- > **Full reference:** [docs.ochk.io/sdk/agent-core](https://docs.ochk.io/sdk/agent-core) — auto-generated from the TypeScript source on every release.
3
+ > **Full reference:** [ochk.io/docs/sdk/agent-core](https://ochk.io/docs/sdk/agent-core) — auto-generated from the TypeScript source on every release.
4
4
  > Hand-written prose below is the high-level overview; the docs site is the source of truth for every export, type, and signature.
5
5
 
6
6
 
@@ -114,7 +114,9 @@ if (!result.ok) throw new Error(result.code + ': ' + result.message);
114
114
  ### Verification (§SPEC 8)
115
115
 
116
116
  - `verifyDelegation({ envelope, revocations | skipRevocationCheck, verifyBip322, now?, skipTemporalCheck?, scopeMode? })`
117
- - `verifyAction({ action, delegation, revocations | skipRevocationCheck, verifyBip322, verifyOtsAnchor?, content?, resolveAnchorBlockHeight?, scopeMode? })`
117
+ - `verifyAction({ action, delegation, revocations | skipRevocationCheck, verifyBip322, now?, maxSkewMs?, verifyOtsAnchor?, content?, resolveAnchorBlockHeight?, scopeMode? })` — the live check, as of `now` (default: the current time). Every link of the chain must be in force at `now`, a revocation in effect at or before `now` refuses the action (`E_REVOKED`), and `signed_at` must lie within `maxSkewMs` of `now` (default 120 s; else `E_STALE`). The result's `asOf` is `{ basis: 'now', now }`.
118
+ - `verifyActionAsOfSignedAt({ … })` — an audit of a past action, as of its own `signed_at`. The agent chooses that time, so `asOf.anchored` says whether a verified OTS anchor fixes it. Never use it to decide whether to act now.
119
+ - `createReplayGuard(maxSkewMs?)` — `firstUse(action, now?)` is true once per action id. It keeps each id until `signed_at + maxSkewMs`, as long as the action could pass `verifyAction`. One process only; across instances keep the same record in a shared store. Replay is the caller's to stop: `verifyAction` keeps no state.
118
120
  - `verifyRevocation({ envelope, delegation, verifyBip322 })` — the principal may always revoke; `revocation.holders` can only add the agent
119
121
  - `verifyOtsAnchor(proofB64, blockHeight, blockHash, envelopeId)` — an anchor orders an action against a revocation only when this returns true
120
122
  - `verifyFederationRevocation({ envelope, delegation, verifyBip322 })`
package/dist/index.d.mts CHANGED
@@ -7,6 +7,7 @@ import { SealInput, DeviceRecord, UnsealInput } from '@orangecheck/lock-core';
7
7
  export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
8
8
 
9
9
  declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
10
+ declare const DEFAULT_MAX_SKEW_MS = 120000;
10
11
  interface VerifyBase {
11
12
  verifyBip322?: (msg: string, signatureB64: string, address: string) => Promise<boolean>;
12
13
  skipSignatureVerification?: boolean;
@@ -40,8 +41,15 @@ interface VerifyActionInput extends VerifyBase {
40
41
  device_id: string;
41
42
  secretKey: Uint8Array;
42
43
  };
44
+ now?: Date;
45
+ maxSkewMs?: number;
43
46
  }
47
+ type VerifyActionAsOfSignedAtInput = Omit<VerifyActionInput, 'now' | 'maxSkewMs'>;
44
48
  declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
49
+ declare function verifyActionAsOfSignedAt(input: VerifyActionAsOfSignedAtInput): Promise<VerifyActionResult>;
50
+ declare function createReplayGuard(maxSkewMs?: number): {
51
+ firstUse(action: Pick<ActionEnvelope, 'id' | 'signed_at'>, now?: Date): boolean;
52
+ };
45
53
  interface VerifyRevocationInput extends VerifyBase {
46
54
  envelope: RevocationEnvelope;
47
55
  delegation: ChainLink;
@@ -179,4 +187,4 @@ declare class ScopeNotGrantedError extends Error {
179
187
  }
180
188
  declare function assertScopeGranted(delegation: ScopeBearingDelegation, scopeExercised: string, fnName: string): void;
181
189
 
182
- export { ActionEnvelope, ActorRef, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DelegationBond, DelegationEnvelope, DelegationRevocationRef, type FederationDelegationEnvelope, type FederationDescriptor, type FederationGuardian, type FederationPrincipal, type FederationRevocationEnvelope, type FederationSignature, type FederationVerifyResult, RevocationEnvelope, type ScopeBearingDelegation, ScopeNotGrantedError, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyFederationBase, type VerifyFederationDelegationInput, type VerifyFederationRevocationInput, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, assertScopeGranted, computeFederationDescriptorId, decodeScopesPayload, encodeScopesPayload, federationDescriptorCanonicalMessage, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyDelegation, verifyFederationDelegation, verifyFederationRevocation, verifyRevocation, verifySubdelegation };
190
+ export { ActionEnvelope, ActorRef, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DEFAULT_MAX_SKEW_MS, DelegationBond, DelegationEnvelope, DelegationRevocationRef, type FederationDelegationEnvelope, type FederationDescriptor, type FederationGuardian, type FederationPrincipal, type FederationRevocationEnvelope, type FederationSignature, type FederationVerifyResult, RevocationEnvelope, type ScopeBearingDelegation, ScopeNotGrantedError, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionAsOfSignedAtInput, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyFederationBase, type VerifyFederationDelegationInput, type VerifyFederationRevocationInput, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, assertScopeGranted, computeFederationDescriptorId, createReplayGuard, decodeScopesPayload, encodeScopesPayload, federationDescriptorCanonicalMessage, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyActionAsOfSignedAt, verifyDelegation, verifyFederationDelegation, verifyFederationRevocation, verifyRevocation, verifySubdelegation };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ import { SealInput, DeviceRecord, UnsealInput } from '@orangecheck/lock-core';
7
7
  export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
8
8
 
9
9
  declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
10
+ declare const DEFAULT_MAX_SKEW_MS = 120000;
10
11
  interface VerifyBase {
11
12
  verifyBip322?: (msg: string, signatureB64: string, address: string) => Promise<boolean>;
12
13
  skipSignatureVerification?: boolean;
@@ -40,8 +41,15 @@ interface VerifyActionInput extends VerifyBase {
40
41
  device_id: string;
41
42
  secretKey: Uint8Array;
42
43
  };
44
+ now?: Date;
45
+ maxSkewMs?: number;
43
46
  }
47
+ type VerifyActionAsOfSignedAtInput = Omit<VerifyActionInput, 'now' | 'maxSkewMs'>;
44
48
  declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
49
+ declare function verifyActionAsOfSignedAt(input: VerifyActionAsOfSignedAtInput): Promise<VerifyActionResult>;
50
+ declare function createReplayGuard(maxSkewMs?: number): {
51
+ firstUse(action: Pick<ActionEnvelope, 'id' | 'signed_at'>, now?: Date): boolean;
52
+ };
45
53
  interface VerifyRevocationInput extends VerifyBase {
46
54
  envelope: RevocationEnvelope;
47
55
  delegation: ChainLink;
@@ -179,4 +187,4 @@ declare class ScopeNotGrantedError extends Error {
179
187
  }
180
188
  declare function assertScopeGranted(delegation: ScopeBearingDelegation, scopeExercised: string, fnName: string): void;
181
189
 
182
- export { ActionEnvelope, ActorRef, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DelegationBond, DelegationEnvelope, DelegationRevocationRef, type FederationDelegationEnvelope, type FederationDescriptor, type FederationGuardian, type FederationPrincipal, type FederationRevocationEnvelope, type FederationSignature, type FederationVerifyResult, RevocationEnvelope, type ScopeBearingDelegation, ScopeNotGrantedError, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyFederationBase, type VerifyFederationDelegationInput, type VerifyFederationRevocationInput, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, assertScopeGranted, computeFederationDescriptorId, decodeScopesPayload, encodeScopesPayload, federationDescriptorCanonicalMessage, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyDelegation, verifyFederationDelegation, verifyFederationRevocation, verifyRevocation, verifySubdelegation };
190
+ export { ActionEnvelope, ActorRef, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DEFAULT_MAX_SKEW_MS, DelegationBond, DelegationEnvelope, DelegationRevocationRef, type FederationDelegationEnvelope, type FederationDescriptor, type FederationGuardian, type FederationPrincipal, type FederationRevocationEnvelope, type FederationSignature, type FederationVerifyResult, RevocationEnvelope, type ScopeBearingDelegation, ScopeNotGrantedError, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionAsOfSignedAtInput, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyFederationBase, type VerifyFederationDelegationInput, type VerifyFederationRevocationInput, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, assertScopeGranted, computeFederationDescriptorId, createReplayGuard, decodeScopesPayload, encodeScopesPayload, federationDescriptorCanonicalMessage, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyActionAsOfSignedAt, verifyDelegation, verifyFederationDelegation, verifyFederationRevocation, verifyRevocation, verifySubdelegation };
package/dist/index.js CHANGED
@@ -411,6 +411,7 @@ function hasPrivateScopes(envelope) {
411
411
 
412
412
  // src/verify.ts
413
413
  var DEFAULT_MAX_CHAIN_DEPTH = 5;
414
+ var DEFAULT_MAX_SKEW_MS = 12e4;
414
415
  var AgentError = class extends Error {
415
416
  constructor(code, message) {
416
417
  super(message);
@@ -533,7 +534,6 @@ async function verifyDelegation(input) {
533
534
  const now = input.now ?? /* @__PURE__ */ new Date();
534
535
  const issued = new Date(env.issued_at);
535
536
  const expires = new Date(env.expires_at);
536
- if (expires <= issued) return err("E_MALFORMED", "expires_at <= issued_at");
537
537
  if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
538
538
  if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
539
539
  }
@@ -546,6 +546,12 @@ async function verifyDelegation(input) {
546
546
  };
547
547
  }
548
548
  async function verifyAction(input) {
549
+ return verifyActionAt(input, input.now ?? /* @__PURE__ */ new Date());
550
+ }
551
+ async function verifyActionAsOfSignedAt(input) {
552
+ return verifyActionAt(input, null);
553
+ }
554
+ async function verifyActionAt(input, now) {
549
555
  const a = input.action;
550
556
  const d = input.delegation;
551
557
  const chain = input.subdelegationChain ?? [];
@@ -623,6 +629,17 @@ async function verifyAction(input) {
623
629
  if (signed < issued || signed >= expires) {
624
630
  return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
625
631
  }
632
+ if (now) {
633
+ const t = now.getTime();
634
+ for (const link of [rootHydrated, ...hydratedChain]) {
635
+ if (t < new Date(link.issued_at).getTime()) {
636
+ return err("E_NOT_YET_VALID", `${link.id} is not valid until ${link.issued_at}`);
637
+ }
638
+ if (t >= new Date(link.expires_at).getTime()) {
639
+ return err("E_EXPIRED", `${link.id} expired at ${link.expires_at}`);
640
+ }
641
+ }
642
+ }
626
643
  if (!leaf.scopes) {
627
644
  return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
628
645
  }
@@ -671,9 +688,21 @@ async function verifyAction(input) {
671
688
  if (revocationPrecedes(rev, revAnchor, a, actionAnchor)) {
672
689
  return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id} before action was signed`);
673
690
  }
691
+ if (now && (revAnchor !== null || new Date(rev.signed_at).getTime() <= now.getTime())) {
692
+ return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id}`);
693
+ }
674
694
  }
675
695
  }
676
696
  }
697
+ if (now) {
698
+ const maxSkew = input.maxSkewMs ?? DEFAULT_MAX_SKEW_MS;
699
+ if (Math.abs(now.getTime() - signed) > maxSkew) {
700
+ return err(
701
+ "E_STALE",
702
+ `action.signed_at ${a.signed_at} is more than ${maxSkew} ms from the time of verification ${now.toISOString()}`
703
+ );
704
+ }
705
+ }
677
706
  if (input.content) {
678
707
  const actualHash = "sha256:" + canonical.hexEncode(sha256.sha256(input.content));
679
708
  if (actualHash !== a.content.hash) {
@@ -701,7 +730,20 @@ async function verifyAction(input) {
701
730
  delegation: rootHydrated,
702
731
  chain: hydratedChain,
703
732
  scopeExercised: exercised,
704
- anchor
733
+ anchor,
734
+ asOf: now ? { basis: "now", now: now.toISOString() } : { basis: "signed_at", signedAt: a.signed_at, anchored: actionAnchor !== null }
735
+ };
736
+ }
737
+ function createReplayGuard(maxSkewMs = DEFAULT_MAX_SKEW_MS) {
738
+ const keepUntil = /* @__PURE__ */ new Map();
739
+ return {
740
+ firstUse(action, now = /* @__PURE__ */ new Date()) {
741
+ const t = now.getTime();
742
+ for (const [id, until] of keepUntil) if (until <= t) keepUntil.delete(id);
743
+ if (keepUntil.has(action.id)) return false;
744
+ keepUntil.set(action.id, new Date(action.signed_at).getTime() + maxSkewMs);
745
+ return true;
746
+ }
705
747
  };
706
748
  }
707
749
  async function hydrateSubdelegationScopes(sub, input) {
@@ -798,11 +840,22 @@ function checkDelegationShape(env) {
798
840
  }
799
841
  if (!isIsoUtc(env.issued_at)) return err("E_MALFORMED", "issued_at must be ISO 8601 UTC");
800
842
  if (!isIsoUtc(env.expires_at)) return err("E_MALFORMED", "expires_at must be ISO 8601 UTC");
843
+ const windowError = delegationWindowError(env.issued_at, env.expires_at);
844
+ if (windowError) return err("E_MALFORMED", windowError);
801
845
  if (!/^[0-9a-f]{32}$/.test(env.nonce)) return err("E_MALFORMED", "nonce must be 32 lowercase hex chars");
802
846
  if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
803
847
  if (env.sig.pubkey !== env.principal.address) return err("E_MALFORMED", "sig.pubkey must equal principal.address");
804
848
  return null;
805
849
  }
850
+ var MAX_DELEGATION_MS = 365 * 864e5;
851
+ function delegationWindowError(issuedAt, expiresAt) {
852
+ const issued = Date.parse(issuedAt);
853
+ const expires = Date.parse(expiresAt);
854
+ if (Number.isNaN(issued) || Number.isNaN(expires)) return "issued_at / expires_at must be real UTC times";
855
+ if (expires <= issued) return "expires_at must be after issued_at";
856
+ if (expires - issued > MAX_DELEGATION_MS) return "expires_at is more than 365 days after issued_at (SPEC \xA74.4)";
857
+ return null;
858
+ }
806
859
  function checkActionShape(a) {
807
860
  if (a.kind !== "agent-action") return err("E_MALFORMED", 'kind must be "agent-action"');
808
861
  if (!isHex64(a.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
@@ -1074,6 +1127,8 @@ async function verifyFederationDelegation(input) {
1074
1127
  if (!ISO_UTC.test(env.issued_at) || !ISO_UTC.test(env.expires_at)) {
1075
1128
  return fail("E_MALFORMED", "issued_at / expires_at must be ISO 8601 UTC");
1076
1129
  }
1130
+ const windowError = delegationWindowError(env.issued_at, env.expires_at);
1131
+ if (windowError) return fail("E_MALFORMED", windowError);
1077
1132
  if (!/^[0-9a-f]{32}$/.test(env.nonce)) return fail("E_MALFORMED", "nonce must be 32 hex chars");
1078
1133
  let canonicalScopes;
1079
1134
  try {
@@ -1118,7 +1173,6 @@ async function verifyFederationDelegation(input) {
1118
1173
  const now = input.now ?? /* @__PURE__ */ new Date();
1119
1174
  const issued = new Date(env.issued_at);
1120
1175
  const expires = new Date(env.expires_at);
1121
- if (expires <= issued) return fail("E_MALFORMED", "expires_at <= issued_at");
1122
1176
  if (now < issued) return fail("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
1123
1177
  if (now >= expires) return fail("E_EXPIRED", `delegation expired at ${env.expires_at}`);
1124
1178
  }
@@ -1239,6 +1293,7 @@ Object.defineProperty(exports, "hexEncode", {
1239
1293
  });
1240
1294
  exports.AgentError = AgentError;
1241
1295
  exports.DEFAULT_MAX_CHAIN_DEPTH = DEFAULT_MAX_CHAIN_DEPTH;
1296
+ exports.DEFAULT_MAX_SKEW_MS = DEFAULT_MAX_SKEW_MS;
1242
1297
  exports.ENVELOPE_VERSION = ENVELOPE_VERSION;
1243
1298
  exports.REGISTERED_SCOPES = REGISTERED_SCOPES;
1244
1299
  exports.ScopeNotGrantedError = ScopeNotGrantedError;
@@ -1260,6 +1315,7 @@ exports.computeDelegationId = computeDelegationId;
1260
1315
  exports.computeFederationDescriptorId = computeFederationDescriptorId;
1261
1316
  exports.computeRevocationId = computeRevocationId;
1262
1317
  exports.computeSubdelegationId = computeSubdelegationId;
1318
+ exports.createReplayGuard = createReplayGuard;
1263
1319
  exports.decodeScopesPayload = decodeScopesPayload;
1264
1320
  exports.delegationCanonicalBytes = delegationCanonicalBytes;
1265
1321
  exports.delegationCanonicalMessage = delegationCanonicalMessage;
@@ -1278,6 +1334,7 @@ exports.subdelegationCanonicalMessage = subdelegationCanonicalMessage;
1278
1334
  exports.unsealScopes = unsealScopes;
1279
1335
  exports.validateScope = validateScope;
1280
1336
  exports.verifyAction = verifyAction;
1337
+ exports.verifyActionAsOfSignedAt = verifyActionAsOfSignedAt;
1281
1338
  exports.verifyDelegation = verifyDelegation;
1282
1339
  exports.verifyFederationDelegation = verifyFederationDelegation;
1283
1340
  exports.verifyFederationRevocation = verifyFederationRevocation;