@orangecheck/agent-core 0.2.0 → 1.0.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.d.mts +47 -2
- package/dist/index.d.ts +47 -2
- package/dist/index.js +184 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +180 -17
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +39 -4
- package/dist/types.d.ts +39 -4
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +12 -0
- package/src/private-scope.test.ts +223 -0
- package/src/private-scope.ts +122 -0
- package/src/test-vectors.test.ts +185 -1
- package/src/types.ts +59 -5
- package/src/verify.ts +234 -18
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
|
|
372
|
-
canonicalScopes = canonicalizeScopes(
|
|
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 (
|
|
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 ${
|
|
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:
|
|
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
|
-
|
|
536
|
+
const rootHydrated = dr.envelope;
|
|
537
|
+
let parent = rootHydrated;
|
|
538
|
+
const hydratedChain = [];
|
|
446
539
|
for (const sub of chain) {
|
|
447
|
-
const
|
|
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
|
-
|
|
544
|
+
hydratedChain.push(hydrated.envelope);
|
|
545
|
+
parent = hydrated.envelope;
|
|
450
546
|
}
|
|
451
|
-
const leaf =
|
|
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 = [
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|