@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 +3 -3
- package/src/test-vectors.test.ts +185 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/agent-core",
|
|
3
|
-
"version": "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.
|
|
68
|
-
"@orangecheck/lock-core": "^0.1
|
|
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",
|
package/src/test-vectors.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|