@orangecheck/agent-core 0.1.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/canonical.d.mts +5 -2
- package/dist/canonical.d.ts +5 -2
- package/dist/canonical.js +22 -0
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +20 -1
- package/dist/canonical.mjs.map +1 -1
- package/dist/index.d.mts +60 -5
- package/dist/index.d.ts +60 -5
- package/dist/index.js +356 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +347 -30
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +65 -4
- package/dist/types.d.ts +65 -4
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +73 -69
- package/src/canonical.ts +23 -0
- package/src/index.ts +18 -0
- package/src/private-scope.test.ts +223 -0
- package/src/private-scope.ts +122 -0
- package/src/test-vectors.test.ts +358 -12
- package/src/types.ts +109 -4
- package/src/verify.ts +487 -40
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;
|
|
@@ -293,6 +294,19 @@ function revocationCanonicalMessage(input) {
|
|
|
293
294
|
`signed_at: ${input.signed_at}`
|
|
294
295
|
].join("\n");
|
|
295
296
|
}
|
|
297
|
+
function subdelegationCanonicalMessage(input) {
|
|
298
|
+
const scopeField = input.scopes.join(",");
|
|
299
|
+
return [
|
|
300
|
+
"oc-agent:subdelegation:v1",
|
|
301
|
+
`parent_id: ${input.parent_id}`,
|
|
302
|
+
`principal: ${input.principal}`,
|
|
303
|
+
`agent: ${input.agent}`,
|
|
304
|
+
`scopes: ${scopeField}`,
|
|
305
|
+
`issued_at: ${input.issued_at}`,
|
|
306
|
+
`expires_at: ${input.expires_at}`,
|
|
307
|
+
`nonce: ${input.nonce}`
|
|
308
|
+
].join("\n");
|
|
309
|
+
}
|
|
296
310
|
function delegationCanonicalBytes(input) {
|
|
297
311
|
return new TextEncoder().encode(delegationCanonicalMessage(input));
|
|
298
312
|
}
|
|
@@ -302,6 +316,9 @@ function actionCanonicalBytes(input) {
|
|
|
302
316
|
function revocationCanonicalBytes(input) {
|
|
303
317
|
return new TextEncoder().encode(revocationCanonicalMessage(input));
|
|
304
318
|
}
|
|
319
|
+
function subdelegationCanonicalBytes(input) {
|
|
320
|
+
return new TextEncoder().encode(subdelegationCanonicalMessage(input));
|
|
321
|
+
}
|
|
305
322
|
function computeDelegationId(input) {
|
|
306
323
|
return hexEncode(sha256(delegationCanonicalBytes(input)));
|
|
307
324
|
}
|
|
@@ -311,6 +328,9 @@ function computeActionId(input) {
|
|
|
311
328
|
function computeRevocationId(input) {
|
|
312
329
|
return hexEncode(sha256(revocationCanonicalBytes(input)));
|
|
313
330
|
}
|
|
331
|
+
function computeSubdelegationId(input) {
|
|
332
|
+
return hexEncode(sha256(subdelegationCanonicalBytes(input)));
|
|
333
|
+
}
|
|
314
334
|
function canonicalizeDelegation(env) {
|
|
315
335
|
return canonicalize(env);
|
|
316
336
|
}
|
|
@@ -332,6 +352,51 @@ function canonicalRevocationBytes(env) {
|
|
|
332
352
|
function sha256Hex(bytes) {
|
|
333
353
|
return hexEncode(sha256(bytes));
|
|
334
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
|
|
399
|
+
var DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
335
400
|
var AgentError = class extends Error {
|
|
336
401
|
constructor(code, message) {
|
|
337
402
|
super(message);
|
|
@@ -346,19 +411,63 @@ async function verifyDelegation(input) {
|
|
|
346
411
|
}
|
|
347
412
|
const shape = checkDelegationShape(env);
|
|
348
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
|
+
}
|
|
349
458
|
let canonicalScopes;
|
|
350
459
|
try {
|
|
351
|
-
for (const s of
|
|
352
|
-
canonicalScopes = canonicalizeScopes(
|
|
460
|
+
for (const s of workingScopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
|
|
461
|
+
canonicalScopes = canonicalizeScopes(workingScopes);
|
|
353
462
|
} catch (e) {
|
|
354
463
|
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
355
464
|
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
356
465
|
}
|
|
357
466
|
for (let i = 0; i < canonicalScopes.length; i++) {
|
|
358
|
-
if (
|
|
467
|
+
if (workingScopes[i] !== canonicalScopes[i]) {
|
|
359
468
|
return err(
|
|
360
469
|
"E_BAD_SCOPE_GRAMMAR",
|
|
361
|
-
`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]}`
|
|
362
471
|
);
|
|
363
472
|
}
|
|
364
473
|
}
|
|
@@ -395,9 +504,10 @@ async function verifyDelegation(input) {
|
|
|
395
504
|
if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
|
|
396
505
|
if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
|
|
397
506
|
}
|
|
507
|
+
const returnedEnvelope = env.scopes_encrypted !== void 0 ? { ...env, scopes: canonicalScopes } : env;
|
|
398
508
|
return {
|
|
399
509
|
ok: true,
|
|
400
|
-
envelope:
|
|
510
|
+
envelope: returnedEnvelope,
|
|
401
511
|
canonicalMessage: reconstructedMessage,
|
|
402
512
|
id: env.id
|
|
403
513
|
};
|
|
@@ -405,15 +515,36 @@ async function verifyDelegation(input) {
|
|
|
405
515
|
async function verifyAction(input) {
|
|
406
516
|
const a = input.action;
|
|
407
517
|
const d = input.delegation;
|
|
518
|
+
const chain = input.subdelegationChain ?? [];
|
|
519
|
+
const maxDepth = input.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;
|
|
520
|
+
if (chain.length > maxDepth) {
|
|
521
|
+
return err(
|
|
522
|
+
"E_SUBDELEGATION_DEPTH_EXCEEDED",
|
|
523
|
+
`chain depth ${chain.length} exceeds maximum ${maxDepth}`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
408
526
|
const dr = await verifyDelegation({
|
|
409
527
|
envelope: d,
|
|
410
528
|
verifyBip322: input.verifyBip322,
|
|
411
529
|
skipSignatureVerification: input.skipSignatureVerification,
|
|
412
530
|
scopeMode: input.scopeMode,
|
|
413
|
-
skipTemporalCheck: true
|
|
531
|
+
skipTemporalCheck: true,
|
|
414
532
|
// action window check dominates
|
|
533
|
+
...input.decryptScopesWith ? { decryptScopesWith: input.decryptScopesWith } : {}
|
|
415
534
|
});
|
|
416
535
|
if (!dr.ok) return dr;
|
|
536
|
+
const rootHydrated = dr.envelope;
|
|
537
|
+
let parent = rootHydrated;
|
|
538
|
+
const hydratedChain = [];
|
|
539
|
+
for (const sub of chain) {
|
|
540
|
+
const hydrated = await hydrateSubdelegationScopes(sub, input);
|
|
541
|
+
if (!hydrated.ok) return hydrated;
|
|
542
|
+
const r = await verifyChainLink(hydrated.envelope, parent, input);
|
|
543
|
+
if (!r.ok) return r;
|
|
544
|
+
hydratedChain.push(hydrated.envelope);
|
|
545
|
+
parent = hydrated.envelope;
|
|
546
|
+
}
|
|
547
|
+
const leaf = hydratedChain.length > 0 ? hydratedChain[hydratedChain.length - 1] : rootHydrated;
|
|
417
548
|
if (a.v !== ENVELOPE_VERSION) {
|
|
418
549
|
return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
|
|
419
550
|
}
|
|
@@ -438,25 +569,28 @@ async function verifyAction(input) {
|
|
|
438
569
|
const ok = await input.verifyBip322(a.id, a.sig.value, a.signer.address);
|
|
439
570
|
if (!ok) return err("E_BAD_ACTION_STAMP", "action BIP-322 signature did not verify");
|
|
440
571
|
}
|
|
441
|
-
if (a.delegation_id !==
|
|
442
|
-
return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) !=
|
|
572
|
+
if (a.delegation_id !== leaf.id) {
|
|
573
|
+
return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != leaf.id (${leaf.id})`);
|
|
443
574
|
}
|
|
444
|
-
if (a.signer.address !==
|
|
445
|
-
return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) !=
|
|
575
|
+
if (a.signer.address !== leaf.agent.address) {
|
|
576
|
+
return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != leaf.agent (${leaf.agent.address})`);
|
|
446
577
|
}
|
|
447
|
-
const issued = new Date(
|
|
448
|
-
const expires = new Date(
|
|
578
|
+
const issued = new Date(leaf.issued_at).getTime();
|
|
579
|
+
const expires = new Date(leaf.expires_at).getTime();
|
|
449
580
|
const signed = new Date(a.signed_at).getTime();
|
|
450
581
|
if (Number.isNaN(issued) || Number.isNaN(expires) || Number.isNaN(signed)) {
|
|
451
582
|
return err("E_MALFORMED", "unparseable ISO 8601 timestamp");
|
|
452
583
|
}
|
|
453
584
|
if (signed < issued || signed >= expires) {
|
|
454
|
-
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside
|
|
585
|
+
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
|
|
586
|
+
}
|
|
587
|
+
if (!leaf.scopes) {
|
|
588
|
+
return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
|
|
455
589
|
}
|
|
456
590
|
let exercised, accepted;
|
|
457
591
|
try {
|
|
458
592
|
exercised = canonicalizeScope(parseScope(a.scope_exercised));
|
|
459
|
-
const granted =
|
|
593
|
+
const granted = leaf.scopes.map((s) => parseScope(s));
|
|
460
594
|
const exercisedParsed = parseScope(a.scope_exercised);
|
|
461
595
|
validateScope(exercisedParsed, { mode: input.scopeMode ?? "strict" });
|
|
462
596
|
accepted = granted.some((g) => isSubScope(exercisedParsed, g));
|
|
@@ -466,19 +600,22 @@ async function verifyAction(input) {
|
|
|
466
600
|
}
|
|
467
601
|
if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
|
|
468
602
|
if (input.revocations && input.revocations.length > 0) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
603
|
+
const allLinks = [rootHydrated, ...hydratedChain];
|
|
604
|
+
for (const link of allLinks) {
|
|
605
|
+
for (const rev of input.revocations) {
|
|
606
|
+
if (rev.delegation_id !== link.id) continue;
|
|
607
|
+
const rr = await verifyRevocation({
|
|
608
|
+
envelope: rev,
|
|
609
|
+
delegation: link,
|
|
610
|
+
verifyBip322: input.verifyBip322,
|
|
611
|
+
skipSignatureVerification: input.skipSignatureVerification
|
|
612
|
+
});
|
|
613
|
+
if (!rr.ok) continue;
|
|
614
|
+
const effective = effectiveRevocationTime(rev, input.resolveAnchorBlockHeight);
|
|
615
|
+
const actionTime = actionEffectiveTime(a, input.resolveAnchorBlockHeight);
|
|
616
|
+
if (compareTimes(effective, actionTime) <= 0) {
|
|
617
|
+
return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id} before action was signed`);
|
|
618
|
+
}
|
|
482
619
|
}
|
|
483
620
|
}
|
|
484
621
|
}
|
|
@@ -514,11 +651,56 @@ async function verifyAction(input) {
|
|
|
514
651
|
envelope: a,
|
|
515
652
|
canonicalMessage: reconstructedMessage,
|
|
516
653
|
id: a.id,
|
|
517
|
-
delegation:
|
|
654
|
+
delegation: rootHydrated,
|
|
655
|
+
chain: hydratedChain,
|
|
518
656
|
scopeExercised: exercised,
|
|
519
657
|
anchor
|
|
520
658
|
};
|
|
521
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
|
+
}
|
|
522
704
|
async function verifyRevocation(input) {
|
|
523
705
|
const env = input.envelope;
|
|
524
706
|
const d = input.delegation;
|
|
@@ -560,7 +742,10 @@ function checkDelegationShape(env) {
|
|
|
560
742
|
if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
561
743
|
if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
|
|
562
744
|
if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
|
|
563
|
-
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
|
+
}
|
|
564
749
|
if (env.bond !== null) {
|
|
565
750
|
if (!Number.isInteger(env.bond.sats) || env.bond.sats < 0) return err("E_MALFORMED", "bond.sats must be non-negative integer");
|
|
566
751
|
if (!isHex64(env.bond.attestation_id)) return err("E_MALFORMED", "bond.attestation_id must be 64-hex");
|
|
@@ -598,6 +783,138 @@ function checkRevocationShape(env) {
|
|
|
598
783
|
if (env.sig.pubkey !== env.signer.address) return err("E_MALFORMED", "sig.pubkey must equal signer.address");
|
|
599
784
|
return null;
|
|
600
785
|
}
|
|
786
|
+
function checkSubdelegationShape(env) {
|
|
787
|
+
if (env.kind !== "agent-subdelegation") return err("E_MALFORMED", 'kind must be "agent-subdelegation"');
|
|
788
|
+
if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
789
|
+
if (!isHex64(env.parent_id)) return err("E_MALFORMED", "parent_id must be 64-hex");
|
|
790
|
+
if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
|
|
791
|
+
if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
|
|
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
|
+
}
|
|
796
|
+
if ("bond" in env && env.bond !== void 0) {
|
|
797
|
+
return err("E_MALFORMED", "sub-delegation envelopes MUST NOT carry a bond field");
|
|
798
|
+
}
|
|
799
|
+
if (!isIsoUtc(env.issued_at)) return err("E_MALFORMED", "issued_at must be ISO 8601 UTC");
|
|
800
|
+
if (!isIsoUtc(env.expires_at)) return err("E_MALFORMED", "expires_at must be ISO 8601 UTC");
|
|
801
|
+
if (!/^[0-9a-f]{32}$/.test(env.nonce)) return err("E_MALFORMED", "nonce must be 32 lowercase hex chars");
|
|
802
|
+
if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
|
|
803
|
+
if (env.sig.pubkey !== env.principal.address) return err("E_MALFORMED", "sig.pubkey must equal principal.address");
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
async function verifyChainLink(s, parent, input) {
|
|
807
|
+
if (s.v !== ENVELOPE_VERSION) {
|
|
808
|
+
return err("E_UNSUPPORTED_VERSION", `subdelegation version ${s.v} not supported`);
|
|
809
|
+
}
|
|
810
|
+
const shape = checkSubdelegationShape(s);
|
|
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
|
+
}
|
|
818
|
+
let canonicalScopesList;
|
|
819
|
+
try {
|
|
820
|
+
canonicalScopesList = canonicalizeScopes(s.scopes);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
823
|
+
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
824
|
+
}
|
|
825
|
+
const canonInput = {
|
|
826
|
+
parent_id: s.parent_id,
|
|
827
|
+
principal: s.principal.address,
|
|
828
|
+
agent: s.agent.address,
|
|
829
|
+
scopes: canonicalScopesList,
|
|
830
|
+
issued_at: s.issued_at,
|
|
831
|
+
expires_at: s.expires_at,
|
|
832
|
+
nonce: s.nonce
|
|
833
|
+
};
|
|
834
|
+
const reconstructedId = computeSubdelegationId(canonInput);
|
|
835
|
+
if (reconstructedId !== s.id) {
|
|
836
|
+
return err("E_BAD_ID", `reconstructed subdelegation id (${reconstructedId}) does not match envelope id (${s.id})`);
|
|
837
|
+
}
|
|
838
|
+
let parsedScopes;
|
|
839
|
+
try {
|
|
840
|
+
parsedScopes = s.scopes.map((str) => parseScope(str));
|
|
841
|
+
for (const p of parsedScopes) validateScope(p, { mode: input.scopeMode ?? "strict" });
|
|
842
|
+
} catch (e) {
|
|
843
|
+
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
844
|
+
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
845
|
+
}
|
|
846
|
+
if (!input.skipSignatureVerification) {
|
|
847
|
+
if (!input.verifyBip322) return err("E_BAD_SIG", "no BIP-322 verifier supplied for subdelegation");
|
|
848
|
+
const ok = await input.verifyBip322(s.id, s.sig.value, s.principal.address);
|
|
849
|
+
if (!ok) return err("E_BAD_SIG", "subdelegation BIP-322 signature did not verify");
|
|
850
|
+
}
|
|
851
|
+
if (s.parent_id !== parent.id) {
|
|
852
|
+
return err(
|
|
853
|
+
"E_SUBDELEGATION_PRINCIPAL_MISMATCH",
|
|
854
|
+
`subdelegation.parent_id (${s.parent_id}) does not match parent envelope id (${parent.id})`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (s.principal.address !== parent.agent.address) {
|
|
858
|
+
return err(
|
|
859
|
+
"E_SUBDELEGATION_PRINCIPAL_MISMATCH",
|
|
860
|
+
`subdelegation.principal (${s.principal.address}) does not match parent.agent (${parent.agent.address})`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const sIssued = new Date(s.issued_at).getTime();
|
|
864
|
+
const sExpires = new Date(s.expires_at).getTime();
|
|
865
|
+
const pIssued = new Date(parent.issued_at).getTime();
|
|
866
|
+
const pExpires = new Date(parent.expires_at).getTime();
|
|
867
|
+
if (Number.isNaN(sIssued) || Number.isNaN(sExpires) || Number.isNaN(pIssued) || Number.isNaN(pExpires)) {
|
|
868
|
+
return err("E_MALFORMED", "unparseable ISO 8601 timestamp in chain");
|
|
869
|
+
}
|
|
870
|
+
if (sExpires <= sIssued) {
|
|
871
|
+
return err("E_MALFORMED", "subdelegation expires_at must be > issued_at");
|
|
872
|
+
}
|
|
873
|
+
if (sIssued < pIssued || sExpires > pExpires) {
|
|
874
|
+
return err(
|
|
875
|
+
"E_SUBDELEGATION_EXPIRES_EXTENDED",
|
|
876
|
+
`subdelegation window [${s.issued_at}, ${s.expires_at}) is not contained in parent's [${parent.issued_at}, ${parent.expires_at})`
|
|
877
|
+
);
|
|
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
|
+
}
|
|
885
|
+
const parentScopesParsed = parent.scopes.map((str) => parseScope(str));
|
|
886
|
+
for (let i = 0; i < parsedScopes.length; i++) {
|
|
887
|
+
const childScope = parsedScopes[i];
|
|
888
|
+
const containedBySomeParentScope = parentScopesParsed.some((p) => isSubScope(childScope, p));
|
|
889
|
+
if (!containedBySomeParentScope) {
|
|
890
|
+
return err(
|
|
891
|
+
"E_SUBDELEGATION_SCOPE_ESCALATED",
|
|
892
|
+
`subdelegation scope ${canonicalizeScope(childScope)} is not a sub-scope of any granted scope on the parent`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
ok: true,
|
|
898
|
+
envelope: s,
|
|
899
|
+
canonicalMessage: "",
|
|
900
|
+
// not populated for chain links; computeSubdelegationId is the binding form
|
|
901
|
+
id: s.id
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
async function verifySubdelegation(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);
|
|
908
|
+
if (!r.ok) return r;
|
|
909
|
+
if (!input.skipTemporalCheck) {
|
|
910
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
911
|
+
const issued = new Date(input.envelope.issued_at).getTime();
|
|
912
|
+
const expires = new Date(input.envelope.expires_at).getTime();
|
|
913
|
+
if (now < issued) return err("E_NOT_YET_VALID", `subdelegation issued_at ${input.envelope.issued_at} is in the future`);
|
|
914
|
+
if (now >= expires) return err("E_EXPIRED", `subdelegation expires_at ${input.envelope.expires_at} is past`);
|
|
915
|
+
}
|
|
916
|
+
return r;
|
|
917
|
+
}
|
|
601
918
|
function actionEffectiveTime(a, resolve) {
|
|
602
919
|
if (a.ots?.status === "confirmed") {
|
|
603
920
|
const h = resolve ? resolve(a) : a.ots.block_height;
|
|
@@ -628,6 +945,6 @@ function isIsoUtc(s) {
|
|
|
628
945
|
return typeof s === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(s);
|
|
629
946
|
}
|
|
630
947
|
|
|
631
|
-
export { AgentError, ENVELOPE_VERSION, REGISTERED_SCOPES, ScopeParseError, actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScope, canonicalizeScopeString, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, delegationCanonicalBytes, delegationCanonicalMessage, isSubScope, parseAndCanonicalizeScopes, parseScope, revocationCanonicalBytes, revocationCanonicalMessage, sha256Hex, validateScope, verifyAction, verifyDelegation, verifyRevocation };
|
|
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 };
|
|
632
949
|
//# sourceMappingURL=index.mjs.map
|
|
633
950
|
//# sourceMappingURL=index.mjs.map
|