@orangecheck/agent-core 0.2.0 → 0.3.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/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
2
  import { hexEncode, canonicalize } from '@orangecheck/stamp-core/canonical';
3
3
  export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
4
+ import { seal, unseal } from '@orangecheck/lock-core';
4
5
 
5
6
  // src/types.ts
6
7
  var ENVELOPE_VERSION = 1;
@@ -351,6 +352,50 @@ function canonicalRevocationBytes(env) {
351
352
  function sha256Hex(bytes) {
352
353
  return hexEncode(sha256(bytes));
353
354
  }
355
+ function encodeScopesPayload(scopes) {
356
+ const canonical = canonicalizeScopes(scopes);
357
+ const json = JSON.stringify(canonical);
358
+ return new TextEncoder().encode(json);
359
+ }
360
+ function decodeScopesPayload(bytes) {
361
+ const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
362
+ const parsed = JSON.parse(json);
363
+ if (!Array.isArray(parsed) || !parsed.every((s) => typeof s === "string")) {
364
+ throw new Error("scopes payload must be a JSON array of strings");
365
+ }
366
+ return parsed;
367
+ }
368
+ async function sealScopes(input) {
369
+ const env = await seal({
370
+ kind: "identity",
371
+ payload: encodeScopesPayload(input.scopes),
372
+ sender: input.sender,
373
+ recipients: input.recipients,
374
+ ...input.hint !== void 0 && { hint: input.hint },
375
+ ...input.expiresAt !== void 0 && { expiresAt: input.expiresAt }
376
+ });
377
+ return env;
378
+ }
379
+ async function unsealScopes(input) {
380
+ const r = await unseal({
381
+ envelope: input.envelope,
382
+ device: input.device,
383
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
384
+ ...input.skipSenderVerification !== void 0 && {
385
+ skipSenderVerification: input.skipSenderVerification
386
+ }
387
+ });
388
+ return {
389
+ scopes: decodeScopesPayload(r.payload),
390
+ sender: r.sender,
391
+ matchedDeviceId: r.matchedDeviceId
392
+ };
393
+ }
394
+ function hasPrivateScopes(envelope) {
395
+ return !!envelope.scopes_encrypted;
396
+ }
397
+
398
+ // src/verify.ts
354
399
  var DEFAULT_MAX_CHAIN_DEPTH = 5;
355
400
  var AgentError = class extends Error {
356
401
  constructor(code, message) {
@@ -366,19 +411,63 @@ async function verifyDelegation(input) {
366
411
  }
367
412
  const shape = checkDelegationShape(env);
368
413
  if (shape) return shape;
414
+ if (env.scopes !== void 0 && env.scopes_encrypted !== void 0) {
415
+ return err(
416
+ "E_SCOPES_BOTH_PROVIDED",
417
+ "envelope carries both scopes and scopes_encrypted; pick one"
418
+ );
419
+ }
420
+ if (env.scopes === void 0 && env.scopes_encrypted === void 0) {
421
+ return err(
422
+ "E_SCOPES_NEITHER_PROVIDED",
423
+ "envelope carries neither scopes nor scopes_encrypted"
424
+ );
425
+ }
426
+ let workingScopes;
427
+ if (env.scopes_encrypted !== void 0) {
428
+ if (env.scopes_encrypted.from?.address !== env.principal.address) {
429
+ return err(
430
+ "E_MALFORMED",
431
+ `scopes_encrypted.from.address (${env.scopes_encrypted.from?.address}) does not match principal.address (${env.principal.address})`
432
+ );
433
+ }
434
+ if (!input.decryptScopesWith) {
435
+ return err(
436
+ "E_SCOPES_UNREADABLE",
437
+ "envelope carries scopes_encrypted; no decryption key provided"
438
+ );
439
+ }
440
+ try {
441
+ const r = await unsealScopes({
442
+ envelope: env.scopes_encrypted,
443
+ device: input.decryptScopesWith,
444
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
445
+ skipSenderVerification: !!input.skipSignatureVerification
446
+ });
447
+ workingScopes = r.scopes;
448
+ } catch (e) {
449
+ const msg = e.message ?? String(e);
450
+ if (/no matching recipient|no recipient|device_id/i.test(msg)) {
451
+ return err("E_SCOPES_UNREADABLE", msg);
452
+ }
453
+ return err("E_BAD_LOCK_ENVELOPE", msg);
454
+ }
455
+ } else {
456
+ workingScopes = env.scopes;
457
+ }
369
458
  let canonicalScopes;
370
459
  try {
371
- for (const s of env.scopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
372
- canonicalScopes = canonicalizeScopes(env.scopes);
460
+ for (const s of workingScopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
461
+ canonicalScopes = canonicalizeScopes(workingScopes);
373
462
  } catch (e) {
374
463
  const msg = e instanceof ScopeParseError ? e.message : e.message;
375
464
  return err("E_BAD_SCOPE_GRAMMAR", msg);
376
465
  }
377
466
  for (let i = 0; i < canonicalScopes.length; i++) {
378
- if (env.scopes[i] !== canonicalScopes[i]) {
467
+ if (workingScopes[i] !== canonicalScopes[i]) {
379
468
  return err(
380
469
  "E_BAD_SCOPE_GRAMMAR",
381
- `scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${env.scopes[i]}`
470
+ `scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${workingScopes[i]}`
382
471
  );
383
472
  }
384
473
  }
@@ -415,9 +504,10 @@ async function verifyDelegation(input) {
415
504
  if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
416
505
  if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
417
506
  }
507
+ const returnedEnvelope = env.scopes_encrypted !== void 0 ? { ...env, scopes: canonicalScopes } : env;
418
508
  return {
419
509
  ok: true,
420
- envelope: env,
510
+ envelope: returnedEnvelope,
421
511
  canonicalMessage: reconstructedMessage,
422
512
  id: env.id
423
513
  };
@@ -438,17 +528,23 @@ async function verifyAction(input) {
438
528
  verifyBip322: input.verifyBip322,
439
529
  skipSignatureVerification: input.skipSignatureVerification,
440
530
  scopeMode: input.scopeMode,
441
- skipTemporalCheck: true
531
+ skipTemporalCheck: true,
442
532
  // action window check dominates
533
+ ...input.decryptScopesWith ? { decryptScopesWith: input.decryptScopesWith } : {}
443
534
  });
444
535
  if (!dr.ok) return dr;
445
- let parent = d;
536
+ const rootHydrated = dr.envelope;
537
+ let parent = rootHydrated;
538
+ const hydratedChain = [];
446
539
  for (const sub of chain) {
447
- const r = await verifyChainLink(sub, parent, input);
540
+ const hydrated = await hydrateSubdelegationScopes(sub, input);
541
+ if (!hydrated.ok) return hydrated;
542
+ const r = await verifyChainLink(hydrated.envelope, parent, input);
448
543
  if (!r.ok) return r;
449
- parent = sub;
544
+ hydratedChain.push(hydrated.envelope);
545
+ parent = hydrated.envelope;
450
546
  }
451
- const leaf = chain.length > 0 ? chain[chain.length - 1] : d;
547
+ const leaf = hydratedChain.length > 0 ? hydratedChain[hydratedChain.length - 1] : rootHydrated;
452
548
  if (a.v !== ENVELOPE_VERSION) {
453
549
  return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
454
550
  }
@@ -488,6 +584,9 @@ async function verifyAction(input) {
488
584
  if (signed < issued || signed >= expires) {
489
585
  return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
490
586
  }
587
+ if (!leaf.scopes) {
588
+ return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
589
+ }
491
590
  let exercised, accepted;
492
591
  try {
493
592
  exercised = canonicalizeScope(parseScope(a.scope_exercised));
@@ -501,7 +600,7 @@ async function verifyAction(input) {
501
600
  }
502
601
  if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
503
602
  if (input.revocations && input.revocations.length > 0) {
504
- const allLinks = [d, ...chain];
603
+ const allLinks = [rootHydrated, ...hydratedChain];
505
604
  for (const link of allLinks) {
506
605
  for (const rev of input.revocations) {
507
606
  if (rev.delegation_id !== link.id) continue;
@@ -552,12 +651,56 @@ async function verifyAction(input) {
552
651
  envelope: a,
553
652
  canonicalMessage: reconstructedMessage,
554
653
  id: a.id,
555
- delegation: d,
556
- chain,
654
+ delegation: rootHydrated,
655
+ chain: hydratedChain,
557
656
  scopeExercised: exercised,
558
657
  anchor
559
658
  };
560
659
  }
660
+ async function hydrateSubdelegationScopes(sub, input) {
661
+ if (sub.scopes !== void 0 && sub.scopes_encrypted !== void 0) {
662
+ return err(
663
+ "E_SCOPES_BOTH_PROVIDED",
664
+ "subdelegation carries both scopes and scopes_encrypted"
665
+ );
666
+ }
667
+ if (sub.scopes === void 0 && sub.scopes_encrypted === void 0) {
668
+ return err(
669
+ "E_SCOPES_NEITHER_PROVIDED",
670
+ "subdelegation carries neither scopes nor scopes_encrypted"
671
+ );
672
+ }
673
+ if (sub.scopes_encrypted === void 0) {
674
+ return { ok: true, envelope: sub };
675
+ }
676
+ if (sub.scopes_encrypted.from?.address !== sub.principal.address) {
677
+ return err(
678
+ "E_MALFORMED",
679
+ `subdelegation.scopes_encrypted.from.address (${sub.scopes_encrypted.from?.address}) does not match principal.address (${sub.principal.address})`
680
+ );
681
+ }
682
+ if (!input.decryptScopesWith) {
683
+ return err(
684
+ "E_SCOPES_UNREADABLE",
685
+ "subdelegation carries scopes_encrypted; no decryption key provided for the chain"
686
+ );
687
+ }
688
+ try {
689
+ const r = await unsealScopes({
690
+ envelope: sub.scopes_encrypted,
691
+ device: input.decryptScopesWith,
692
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
693
+ skipSenderVerification: !!input.skipSignatureVerification
694
+ });
695
+ return { ok: true, envelope: { ...sub, scopes: r.scopes } };
696
+ } catch (e) {
697
+ const msg = e.message ?? String(e);
698
+ if (/no matching recipient|no recipient|device_id/i.test(msg)) {
699
+ return err("E_SCOPES_UNREADABLE", msg);
700
+ }
701
+ return err("E_BAD_LOCK_ENVELOPE", msg);
702
+ }
703
+ }
561
704
  async function verifyRevocation(input) {
562
705
  const env = input.envelope;
563
706
  const d = input.delegation;
@@ -599,7 +742,10 @@ function checkDelegationShape(env) {
599
742
  if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
600
743
  if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
601
744
  if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
602
- if (!Array.isArray(env.scopes) || env.scopes.length === 0) return err("E_MALFORMED", "scopes must be non-empty array");
745
+ if (env.scopes !== void 0) {
746
+ if (!Array.isArray(env.scopes) || env.scopes.length === 0)
747
+ return err("E_MALFORMED", "scopes must be non-empty array");
748
+ }
603
749
  if (env.bond !== null) {
604
750
  if (!Number.isInteger(env.bond.sats) || env.bond.sats < 0) return err("E_MALFORMED", "bond.sats must be non-negative integer");
605
751
  if (!isHex64(env.bond.attestation_id)) return err("E_MALFORMED", "bond.attestation_id must be 64-hex");
@@ -643,7 +789,10 @@ function checkSubdelegationShape(env) {
643
789
  if (!isHex64(env.parent_id)) return err("E_MALFORMED", "parent_id must be 64-hex");
644
790
  if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
645
791
  if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
646
- if (!Array.isArray(env.scopes) || env.scopes.length === 0) return err("E_MALFORMED", "scopes must be non-empty array");
792
+ if (env.scopes !== void 0) {
793
+ if (!Array.isArray(env.scopes) || env.scopes.length === 0)
794
+ return err("E_MALFORMED", "scopes must be non-empty array");
795
+ }
647
796
  if ("bond" in env && env.bond !== void 0) {
648
797
  return err("E_MALFORMED", "sub-delegation envelopes MUST NOT carry a bond field");
649
798
  }
@@ -660,6 +809,12 @@ async function verifyChainLink(s, parent, input) {
660
809
  }
661
810
  const shape = checkSubdelegationShape(s);
662
811
  if (shape) return shape;
812
+ if (!s.scopes) {
813
+ return err(
814
+ "E_SCOPES_NEITHER_PROVIDED",
815
+ "subdelegation has no plaintext scopes \u2014 caller must hydrate v1.2 envelopes before invoking verifyChainLink"
816
+ );
817
+ }
663
818
  let canonicalScopesList;
664
819
  try {
665
820
  canonicalScopesList = canonicalizeScopes(s.scopes);
@@ -721,6 +876,12 @@ async function verifyChainLink(s, parent, input) {
721
876
  `subdelegation window [${s.issued_at}, ${s.expires_at}) is not contained in parent's [${parent.issued_at}, ${parent.expires_at})`
722
877
  );
723
878
  }
879
+ if (!parent.scopes) {
880
+ return err(
881
+ "E_SCOPES_NEITHER_PROVIDED",
882
+ "parent has no plaintext scopes \u2014 caller must hydrate v1.2 parents before invoking verifyChainLink"
883
+ );
884
+ }
724
885
  const parentScopesParsed = parent.scopes.map((str) => parseScope(str));
725
886
  for (let i = 0; i < parsedScopes.length; i++) {
726
887
  const childScope = parsedScopes[i];
@@ -741,7 +902,9 @@ async function verifyChainLink(s, parent, input) {
741
902
  };
742
903
  }
743
904
  async function verifySubdelegation(input) {
744
- const r = await verifyChainLink(input.envelope, input.parent, input);
905
+ const hydrated = await hydrateSubdelegationScopes(input.envelope, input);
906
+ if (!hydrated.ok) return hydrated;
907
+ const r = await verifyChainLink(hydrated.envelope, input.parent, input);
745
908
  if (!r.ok) return r;
746
909
  if (!input.skipTemporalCheck) {
747
910
  const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
@@ -782,6 +945,6 @@ function isIsoUtc(s) {
782
945
  return typeof s === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(s);
783
946
  }
784
947
 
785
- export { AgentError, DEFAULT_MAX_CHAIN_DEPTH, ENVELOPE_VERSION, REGISTERED_SCOPES, ScopeParseError, actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScope, canonicalizeScopeString, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, computeSubdelegationId, delegationCanonicalBytes, delegationCanonicalMessage, isSubScope, parseAndCanonicalizeScopes, parseScope, revocationCanonicalBytes, revocationCanonicalMessage, sha256Hex, subdelegationCanonicalBytes, subdelegationCanonicalMessage, validateScope, verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation };
948
+ export { AgentError, DEFAULT_MAX_CHAIN_DEPTH, ENVELOPE_VERSION, REGISTERED_SCOPES, ScopeParseError, actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScope, canonicalizeScopeString, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, computeSubdelegationId, decodeScopesPayload, delegationCanonicalBytes, delegationCanonicalMessage, encodeScopesPayload, hasPrivateScopes, isSubScope, parseAndCanonicalizeScopes, parseScope, revocationCanonicalBytes, revocationCanonicalMessage, sealScopes, sha256Hex, subdelegationCanonicalBytes, subdelegationCanonicalMessage, unsealScopes, validateScope, verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation };
786
949
  //# sourceMappingURL=index.mjs.map
787
950
  //# sourceMappingURL=index.mjs.map