@orangecheck/agent-core 0.3.0 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/agent-core",
3
- "version": "0.3.0",
3
+ "version": "1.0.1",
4
4
  "description": "OC Agent canonical messages, envelope formats (delegation/action/revocation), scope grammar, and verification. See https://github.com/orangecheck/oc-agent-protocol.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -64,8 +64,8 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@noble/hashes": "^1.5.0",
67
- "@orangecheck/stamp-core": "^0.1.1",
68
- "@orangecheck/lock-core": "^0.1.0"
67
+ "@orangecheck/stamp-core": "^1.0.0",
68
+ "@orangecheck/lock-core": "^1.0.1"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^22.10.2",
@@ -217,6 +217,7 @@ describe('oc-agent-protocol test vectors', () => {
217
217
  const subdelegationEnvelopes = new Map<string, SubdelegationEnvelope>();
218
218
  for (const { data } of vectors) {
219
219
  if (isNegative(data)) continue;
220
+ if ((data as { private_scope?: unknown }).private_scope) continue;
220
221
  if (data.kind === 'delegation') {
221
222
  delegationEnvelopes.set(data.expected.id, data.expected.envelope);
222
223
  } else if (data.kind === 'subdelegation') {
@@ -226,6 +227,7 @@ describe('oc-agent-protocol test vectors', () => {
226
227
 
227
228
  for (const { name, data } of vectors) {
228
229
  if (isNegative(data)) continue;
230
+ if ((data as { private_scope?: unknown }).private_scope) continue;
229
231
  it(`${name} — canonical message reconstructs byte-identical`, () => {
230
232
  const msg = reconstructCanonical(data);
231
233
  expect(msg).toBe(data.expected.canonical_message);
@@ -367,7 +369,14 @@ function reconstructId(v: PositiveVector): string {
367
369
  // ─────────────────────────────────────────────────────────────────────────────
368
370
 
369
371
  describe('oc-agent-protocol negative test vectors', () => {
370
- const negatives = vectors.filter(({ data }) => isNegative(data)) as {
372
+ // Filter out private-scope vectors they have their own describe block
373
+ // because the round-trip path requires sealing first, not just verifying
374
+ // a pre-built envelope.
375
+ const negatives = vectors.filter(
376
+ ({ data }) =>
377
+ isNegative(data) &&
378
+ !(data as { private_scope?: unknown }).private_scope
379
+ ) as {
371
380
  name: string;
372
381
  data: NegativeVector;
373
382
  }[];
@@ -543,3 +552,178 @@ describe('oc-agent-protocol negative test vectors', () => {
543
552
  });
544
553
  }
545
554
  });
555
+
556
+ // ─────────────────────────────────────────────────────────────────────────────
557
+ // Private-scope (v1.2) vectors — round-trip via sealScopes/verifyDelegation.
558
+ // The OC Lock envelope uses random nonces, so byte-identical reproducibility
559
+ // of the ciphertext is not asserted; what's pinned is the round-trip outcome.
560
+ // ─────────────────────────────────────────────────────────────────────────────
561
+
562
+ interface PrivateScopeRecipientFixture {
563
+ address: string;
564
+ device_id: string;
565
+ device_pk: string;
566
+ device_secret: string;
567
+ }
568
+
569
+ interface PrivateScopeBaseInputs {
570
+ principal: string;
571
+ agent: string;
572
+ scopes_plaintext: string[];
573
+ recipients: PrivateScopeRecipientFixture[];
574
+ bond: { sats: number; attestation_id: string } | null;
575
+ issued_at: string;
576
+ expires_at: string;
577
+ nonce: string;
578
+ }
579
+
580
+ interface PrivateScopePositiveVector {
581
+ description: string;
582
+ kind: 'delegation';
583
+ private_scope: true;
584
+ inputs: PrivateScopeBaseInputs;
585
+ expected: {
586
+ verification_outcome: 'ACCEPT';
587
+ decrypted_scopes: string[];
588
+ verifier_uses_device_id: string;
589
+ };
590
+ }
591
+
592
+ interface PrivateScopeNegativeVector {
593
+ description: string;
594
+ kind: 'delegation';
595
+ private_scope: true;
596
+ negative: true;
597
+ inputs: PrivateScopeBaseInputs & {
598
+ verifier_holds_unrelated_key: {
599
+ device_id: string;
600
+ device_pk: string;
601
+ device_secret: string;
602
+ };
603
+ };
604
+ expected: {
605
+ verification_outcome: 'REJECT';
606
+ error_code: AgentErrorCode;
607
+ };
608
+ }
609
+
610
+ type PrivateScopeVector = PrivateScopePositiveVector | PrivateScopeNegativeVector;
611
+
612
+ function isPrivateScopeVector(v: unknown): v is PrivateScopeVector {
613
+ return (
614
+ typeof v === 'object' &&
615
+ v !== null &&
616
+ (v as { private_scope?: unknown }).private_scope === true
617
+ );
618
+ }
619
+
620
+ describe('oc-agent-protocol private-scope vectors (v1.2)', () => {
621
+ const psVectors = vectors.filter(({ data }) =>
622
+ isPrivateScopeVector(data)
623
+ ) as { name: string; data: PrivateScopeVector }[];
624
+
625
+ if (psVectors.length === 0) {
626
+ it.skip('(no private-scope vectors found)', () => {});
627
+ return;
628
+ }
629
+
630
+ for (const { name, data } of psVectors) {
631
+ it(`${name} — ${data.expected.verification_outcome}`, async () => {
632
+ // Lazy-load to avoid pulling lock-core at module init time when no
633
+ // private-scope vectors are present.
634
+ const ps = await import('./private-scope.js');
635
+ const { hexDecode } = await import('@orangecheck/lock-crypto');
636
+
637
+ const sealed = await ps.sealScopes({
638
+ scopes: data.inputs.scopes_plaintext,
639
+ sender: {
640
+ address: data.inputs.principal,
641
+ signMessage: async () => 'AAAA',
642
+ },
643
+ recipients: data.inputs.recipients.map((r) => ({
644
+ address: r.address,
645
+ device_id: r.device_id,
646
+ device_pk: r.device_pk,
647
+ })),
648
+ });
649
+
650
+ // Build the OC Agent envelope. Use the canonical (sorted) scopes
651
+ // so the id reconstructs cleanly.
652
+ const canonScopes = canonicalizeScopes(data.inputs.scopes_plaintext);
653
+ const canonInput = {
654
+ principal: data.inputs.principal,
655
+ agent: data.inputs.agent,
656
+ scopes: canonScopes,
657
+ bond_sats: data.inputs.bond?.sats ?? 0,
658
+ bond_attestation: data.inputs.bond?.attestation_id ?? 'none',
659
+ issued_at: data.inputs.issued_at,
660
+ expires_at: data.inputs.expires_at,
661
+ nonce: data.inputs.nonce,
662
+ };
663
+ const id = computeDelegationId(canonInput);
664
+ const env: DelegationEnvelope = {
665
+ v: 1,
666
+ kind: 'agent-delegation',
667
+ id,
668
+ principal: { address: data.inputs.principal, alg: 'bip322' },
669
+ agent: { address: data.inputs.agent, alg: 'bip322' },
670
+ scopes_encrypted: sealed,
671
+ bond: data.inputs.bond,
672
+ issued_at: data.inputs.issued_at,
673
+ expires_at: data.inputs.expires_at,
674
+ nonce: data.inputs.nonce,
675
+ revocation: { holders: ['principal'], ref: null },
676
+ sig: { alg: 'bip322', pubkey: data.inputs.principal, value: 'AAAA' },
677
+ };
678
+
679
+ if (data.expected.verification_outcome === 'ACCEPT') {
680
+ const positive = data as PrivateScopePositiveVector;
681
+ const recip = data.inputs.recipients.find(
682
+ (r) => r.device_id === positive.expected.verifier_uses_device_id
683
+ );
684
+ if (!recip) {
685
+ throw new Error(
686
+ `vector ${name} names verifier_uses_device_id ${positive.expected.verifier_uses_device_id}, not in recipients`
687
+ );
688
+ }
689
+ const r = await verifyDelegation({
690
+ envelope: env,
691
+ skipSignatureVerification: true,
692
+ skipTemporalCheck: true,
693
+ decryptScopesWith: {
694
+ device_id: recip.device_id,
695
+ secretKey: hexDecode(recip.device_secret),
696
+ },
697
+ });
698
+ expect(r.ok).toBe(true);
699
+ if (r.ok) {
700
+ expect(r.envelope.scopes).toEqual(
701
+ canonicalizeScopes(positive.expected.decrypted_scopes)
702
+ );
703
+ }
704
+ } else {
705
+ const negative = data as PrivateScopeNegativeVector;
706
+ const r = await verifyDelegation({
707
+ envelope: env,
708
+ skipSignatureVerification: true,
709
+ skipTemporalCheck: true,
710
+ decryptScopesWith: {
711
+ device_id: negative.inputs.verifier_holds_unrelated_key.device_id,
712
+ secretKey: hexDecode(
713
+ negative.inputs.verifier_holds_unrelated_key.device_secret
714
+ ),
715
+ },
716
+ });
717
+ expect(r.ok).toBe(false);
718
+ if (!r.ok) {
719
+ // Accept either E_SCOPES_UNREADABLE (preferred) or
720
+ // E_BAD_LOCK_ENVELOPE (acceptable per harness_assertion).
721
+ expect([
722
+ 'E_SCOPES_UNREADABLE',
723
+ 'E_BAD_LOCK_ENVELOPE',
724
+ ]).toContain(r.code);
725
+ }
726
+ }
727
+ });
728
+ }
729
+ });