@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.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, RevocationEnvelope, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult } from './types.js';
|
|
2
|
-
export { ActionCanonicalInput, ActionContent, ActionOts, ActorRef, DelegationBond, DelegationCanonicalInput, DelegationRevocationRef, ENVELOPE_VERSION, EnvelopeKind, RevocationCanonicalInput, RevocationHolder, Signature, VerifyActionOkExtra, VerifyErr, VerifyOk } from './types.js';
|
|
3
|
-
export { actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, delegationCanonicalBytes, delegationCanonicalMessage, parseAndCanonicalizeScopes, revocationCanonicalBytes, revocationCanonicalMessage, sha256Hex } from './canonical.js';
|
|
1
|
+
import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult, ScopesEncryptedEnvelope } from './types.js';
|
|
2
|
+
export { ActionCanonicalInput, ActionContent, ActionOts, ActorRef, DelegationBond, DelegationCanonicalInput, DelegationRevocationRef, ENVELOPE_VERSION, EnvelopeKind, RevocationCanonicalInput, RevocationHolder, Signature, SubdelegationCanonicalInput, VerifyActionOkExtra, VerifyErr, VerifyOk } from './types.js';
|
|
3
|
+
export { actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, computeSubdelegationId, delegationCanonicalBytes, delegationCanonicalMessage, parseAndCanonicalizeScopes, revocationCanonicalBytes, revocationCanonicalMessage, sha256Hex, subdelegationCanonicalBytes, subdelegationCanonicalMessage } from './canonical.js';
|
|
4
4
|
import { ValidationOptions } from './scope.js';
|
|
5
5
|
export { REGISTERED_SCOPES, Scope, ScopeConstraint, ScopeOp, ScopeParseError, canonicalizeScope, canonicalizeScopeString, isSubScope, parseScope, validateScope } from './scope.js';
|
|
6
|
+
import { SealInput, DeviceRecord, UnsealInput } from '@orangecheck/lock-core';
|
|
6
7
|
export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
|
|
7
8
|
|
|
9
|
+
declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
8
10
|
interface VerifyBase {
|
|
9
11
|
verifyBip322?: (msg: string, signatureB64: string, address: string) => Promise<boolean>;
|
|
10
12
|
skipSignatureVerification?: boolean;
|
|
@@ -18,21 +20,74 @@ interface VerifyDelegationInput extends VerifyBase {
|
|
|
18
20
|
envelope: DelegationEnvelope;
|
|
19
21
|
now?: Date;
|
|
20
22
|
skipTemporalCheck?: boolean;
|
|
23
|
+
decryptScopesWith?: {
|
|
24
|
+
device_id: string;
|
|
25
|
+
secretKey: Uint8Array;
|
|
26
|
+
};
|
|
21
27
|
}
|
|
22
28
|
declare function verifyDelegation(input: VerifyDelegationInput): Promise<VerifyDelegationResult>;
|
|
23
29
|
interface VerifyActionInput extends VerifyBase {
|
|
24
30
|
action: ActionEnvelope;
|
|
25
31
|
delegation: DelegationEnvelope;
|
|
32
|
+
subdelegationChain?: SubdelegationEnvelope[];
|
|
33
|
+
maxChainDepth?: number;
|
|
26
34
|
revocations?: RevocationEnvelope[];
|
|
27
35
|
content?: Uint8Array;
|
|
28
36
|
verifyOtsAnchor?: (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean>;
|
|
29
37
|
resolveAnchorBlockHeight?: (env: ActionEnvelope | RevocationEnvelope) => number | null;
|
|
38
|
+
decryptScopesWith?: {
|
|
39
|
+
device_id: string;
|
|
40
|
+
secretKey: Uint8Array;
|
|
41
|
+
};
|
|
30
42
|
}
|
|
31
43
|
declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
|
|
32
44
|
interface VerifyRevocationInput extends VerifyBase {
|
|
33
45
|
envelope: RevocationEnvelope;
|
|
34
|
-
delegation:
|
|
46
|
+
delegation: ChainLink;
|
|
35
47
|
}
|
|
36
48
|
declare function verifyRevocation(input: VerifyRevocationInput): Promise<VerifyRevocationResult>;
|
|
49
|
+
interface VerifySubdelegationInput extends VerifyBase {
|
|
50
|
+
envelope: SubdelegationEnvelope;
|
|
51
|
+
parent: ChainLink;
|
|
52
|
+
skipTemporalCheck?: boolean;
|
|
53
|
+
now?: Date;
|
|
54
|
+
decryptScopesWith?: {
|
|
55
|
+
device_id: string;
|
|
56
|
+
secretKey: Uint8Array;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
declare function verifySubdelegation(input: VerifySubdelegationInput): Promise<VerifySubdelegationResult>;
|
|
60
|
+
|
|
61
|
+
declare function encodeScopesPayload(scopes: string[]): Uint8Array;
|
|
62
|
+
declare function decodeScopesPayload(bytes: Uint8Array): string[];
|
|
63
|
+
interface SealScopesInput {
|
|
64
|
+
scopes: string[];
|
|
65
|
+
sender: SealInput['sender'];
|
|
66
|
+
recipients: DeviceRecord[];
|
|
67
|
+
hint?: string;
|
|
68
|
+
expiresAt?: Date | null;
|
|
69
|
+
}
|
|
70
|
+
declare function sealScopes(input: SealScopesInput): Promise<ScopesEncryptedEnvelope>;
|
|
71
|
+
interface UnsealScopesInput {
|
|
72
|
+
envelope: ScopesEncryptedEnvelope;
|
|
73
|
+
device: UnsealInput['device'];
|
|
74
|
+
verifyBip322?: UnsealInput['verifyBip322'];
|
|
75
|
+
skipSenderVerification?: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface UnsealedScopes {
|
|
78
|
+
scopes: string[];
|
|
79
|
+
sender: {
|
|
80
|
+
address: string;
|
|
81
|
+
attestation_id?: string;
|
|
82
|
+
};
|
|
83
|
+
matchedDeviceId: string;
|
|
84
|
+
}
|
|
85
|
+
declare function unsealScopes(input: UnsealScopesInput): Promise<UnsealedScopes>;
|
|
86
|
+
declare function hasPrivateScopes<T extends {
|
|
87
|
+
scopes?: string[];
|
|
88
|
+
scopes_encrypted?: ScopesEncryptedEnvelope;
|
|
89
|
+
}>(envelope: T): envelope is T & {
|
|
90
|
+
scopes_encrypted: ScopesEncryptedEnvelope;
|
|
91
|
+
};
|
|
37
92
|
|
|
38
|
-
export { ActionEnvelope, AgentError, AgentErrorCode, DelegationEnvelope, RevocationEnvelope, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyRevocationInput, VerifyRevocationResult, verifyAction, verifyDelegation, verifyRevocation };
|
|
93
|
+
export { ActionEnvelope, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DelegationEnvelope, RevocationEnvelope, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, decodeScopesPayload, encodeScopesPayload, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var sha256 = require('@noble/hashes/sha256');
|
|
4
4
|
var canonical = require('@orangecheck/stamp-core/canonical');
|
|
5
|
+
var lockCore = require('@orangecheck/lock-core');
|
|
5
6
|
|
|
6
7
|
// src/types.ts
|
|
7
8
|
var ENVELOPE_VERSION = 1;
|
|
@@ -294,6 +295,19 @@ function revocationCanonicalMessage(input) {
|
|
|
294
295
|
`signed_at: ${input.signed_at}`
|
|
295
296
|
].join("\n");
|
|
296
297
|
}
|
|
298
|
+
function subdelegationCanonicalMessage(input) {
|
|
299
|
+
const scopeField = input.scopes.join(",");
|
|
300
|
+
return [
|
|
301
|
+
"oc-agent:subdelegation:v1",
|
|
302
|
+
`parent_id: ${input.parent_id}`,
|
|
303
|
+
`principal: ${input.principal}`,
|
|
304
|
+
`agent: ${input.agent}`,
|
|
305
|
+
`scopes: ${scopeField}`,
|
|
306
|
+
`issued_at: ${input.issued_at}`,
|
|
307
|
+
`expires_at: ${input.expires_at}`,
|
|
308
|
+
`nonce: ${input.nonce}`
|
|
309
|
+
].join("\n");
|
|
310
|
+
}
|
|
297
311
|
function delegationCanonicalBytes(input) {
|
|
298
312
|
return new TextEncoder().encode(delegationCanonicalMessage(input));
|
|
299
313
|
}
|
|
@@ -303,6 +317,9 @@ function actionCanonicalBytes(input) {
|
|
|
303
317
|
function revocationCanonicalBytes(input) {
|
|
304
318
|
return new TextEncoder().encode(revocationCanonicalMessage(input));
|
|
305
319
|
}
|
|
320
|
+
function subdelegationCanonicalBytes(input) {
|
|
321
|
+
return new TextEncoder().encode(subdelegationCanonicalMessage(input));
|
|
322
|
+
}
|
|
306
323
|
function computeDelegationId(input) {
|
|
307
324
|
return canonical.hexEncode(sha256.sha256(delegationCanonicalBytes(input)));
|
|
308
325
|
}
|
|
@@ -312,6 +329,9 @@ function computeActionId(input) {
|
|
|
312
329
|
function computeRevocationId(input) {
|
|
313
330
|
return canonical.hexEncode(sha256.sha256(revocationCanonicalBytes(input)));
|
|
314
331
|
}
|
|
332
|
+
function computeSubdelegationId(input) {
|
|
333
|
+
return canonical.hexEncode(sha256.sha256(subdelegationCanonicalBytes(input)));
|
|
334
|
+
}
|
|
315
335
|
function canonicalizeDelegation(env) {
|
|
316
336
|
return canonical.canonicalize(env);
|
|
317
337
|
}
|
|
@@ -333,6 +353,51 @@ function canonicalRevocationBytes(env) {
|
|
|
333
353
|
function sha256Hex(bytes) {
|
|
334
354
|
return canonical.hexEncode(sha256.sha256(bytes));
|
|
335
355
|
}
|
|
356
|
+
function encodeScopesPayload(scopes) {
|
|
357
|
+
const canonical = canonicalizeScopes(scopes);
|
|
358
|
+
const json = JSON.stringify(canonical);
|
|
359
|
+
return new TextEncoder().encode(json);
|
|
360
|
+
}
|
|
361
|
+
function decodeScopesPayload(bytes) {
|
|
362
|
+
const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
363
|
+
const parsed = JSON.parse(json);
|
|
364
|
+
if (!Array.isArray(parsed) || !parsed.every((s) => typeof s === "string")) {
|
|
365
|
+
throw new Error("scopes payload must be a JSON array of strings");
|
|
366
|
+
}
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
async function sealScopes(input) {
|
|
370
|
+
const env = await lockCore.seal({
|
|
371
|
+
kind: "identity",
|
|
372
|
+
payload: encodeScopesPayload(input.scopes),
|
|
373
|
+
sender: input.sender,
|
|
374
|
+
recipients: input.recipients,
|
|
375
|
+
...input.hint !== void 0 && { hint: input.hint },
|
|
376
|
+
...input.expiresAt !== void 0 && { expiresAt: input.expiresAt }
|
|
377
|
+
});
|
|
378
|
+
return env;
|
|
379
|
+
}
|
|
380
|
+
async function unsealScopes(input) {
|
|
381
|
+
const r = await lockCore.unseal({
|
|
382
|
+
envelope: input.envelope,
|
|
383
|
+
device: input.device,
|
|
384
|
+
...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
|
|
385
|
+
...input.skipSenderVerification !== void 0 && {
|
|
386
|
+
skipSenderVerification: input.skipSenderVerification
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
return {
|
|
390
|
+
scopes: decodeScopesPayload(r.payload),
|
|
391
|
+
sender: r.sender,
|
|
392
|
+
matchedDeviceId: r.matchedDeviceId
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function hasPrivateScopes(envelope) {
|
|
396
|
+
return !!envelope.scopes_encrypted;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/verify.ts
|
|
400
|
+
var DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
336
401
|
var AgentError = class extends Error {
|
|
337
402
|
constructor(code, message) {
|
|
338
403
|
super(message);
|
|
@@ -347,19 +412,63 @@ async function verifyDelegation(input) {
|
|
|
347
412
|
}
|
|
348
413
|
const shape = checkDelegationShape(env);
|
|
349
414
|
if (shape) return shape;
|
|
415
|
+
if (env.scopes !== void 0 && env.scopes_encrypted !== void 0) {
|
|
416
|
+
return err(
|
|
417
|
+
"E_SCOPES_BOTH_PROVIDED",
|
|
418
|
+
"envelope carries both scopes and scopes_encrypted; pick one"
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (env.scopes === void 0 && env.scopes_encrypted === void 0) {
|
|
422
|
+
return err(
|
|
423
|
+
"E_SCOPES_NEITHER_PROVIDED",
|
|
424
|
+
"envelope carries neither scopes nor scopes_encrypted"
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
let workingScopes;
|
|
428
|
+
if (env.scopes_encrypted !== void 0) {
|
|
429
|
+
if (env.scopes_encrypted.from?.address !== env.principal.address) {
|
|
430
|
+
return err(
|
|
431
|
+
"E_MALFORMED",
|
|
432
|
+
`scopes_encrypted.from.address (${env.scopes_encrypted.from?.address}) does not match principal.address (${env.principal.address})`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
if (!input.decryptScopesWith) {
|
|
436
|
+
return err(
|
|
437
|
+
"E_SCOPES_UNREADABLE",
|
|
438
|
+
"envelope carries scopes_encrypted; no decryption key provided"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const r = await unsealScopes({
|
|
443
|
+
envelope: env.scopes_encrypted,
|
|
444
|
+
device: input.decryptScopesWith,
|
|
445
|
+
...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
|
|
446
|
+
skipSenderVerification: !!input.skipSignatureVerification
|
|
447
|
+
});
|
|
448
|
+
workingScopes = r.scopes;
|
|
449
|
+
} catch (e) {
|
|
450
|
+
const msg = e.message ?? String(e);
|
|
451
|
+
if (/no matching recipient|no recipient|device_id/i.test(msg)) {
|
|
452
|
+
return err("E_SCOPES_UNREADABLE", msg);
|
|
453
|
+
}
|
|
454
|
+
return err("E_BAD_LOCK_ENVELOPE", msg);
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
workingScopes = env.scopes;
|
|
458
|
+
}
|
|
350
459
|
let canonicalScopes;
|
|
351
460
|
try {
|
|
352
|
-
for (const s of
|
|
353
|
-
canonicalScopes = canonicalizeScopes(
|
|
461
|
+
for (const s of workingScopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
|
|
462
|
+
canonicalScopes = canonicalizeScopes(workingScopes);
|
|
354
463
|
} catch (e) {
|
|
355
464
|
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
356
465
|
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
357
466
|
}
|
|
358
467
|
for (let i = 0; i < canonicalScopes.length; i++) {
|
|
359
|
-
if (
|
|
468
|
+
if (workingScopes[i] !== canonicalScopes[i]) {
|
|
360
469
|
return err(
|
|
361
470
|
"E_BAD_SCOPE_GRAMMAR",
|
|
362
|
-
`scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${
|
|
471
|
+
`scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${workingScopes[i]}`
|
|
363
472
|
);
|
|
364
473
|
}
|
|
365
474
|
}
|
|
@@ -396,9 +505,10 @@ async function verifyDelegation(input) {
|
|
|
396
505
|
if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
|
|
397
506
|
if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
|
|
398
507
|
}
|
|
508
|
+
const returnedEnvelope = env.scopes_encrypted !== void 0 ? { ...env, scopes: canonicalScopes } : env;
|
|
399
509
|
return {
|
|
400
510
|
ok: true,
|
|
401
|
-
envelope:
|
|
511
|
+
envelope: returnedEnvelope,
|
|
402
512
|
canonicalMessage: reconstructedMessage,
|
|
403
513
|
id: env.id
|
|
404
514
|
};
|
|
@@ -406,15 +516,36 @@ async function verifyDelegation(input) {
|
|
|
406
516
|
async function verifyAction(input) {
|
|
407
517
|
const a = input.action;
|
|
408
518
|
const d = input.delegation;
|
|
519
|
+
const chain = input.subdelegationChain ?? [];
|
|
520
|
+
const maxDepth = input.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;
|
|
521
|
+
if (chain.length > maxDepth) {
|
|
522
|
+
return err(
|
|
523
|
+
"E_SUBDELEGATION_DEPTH_EXCEEDED",
|
|
524
|
+
`chain depth ${chain.length} exceeds maximum ${maxDepth}`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
409
527
|
const dr = await verifyDelegation({
|
|
410
528
|
envelope: d,
|
|
411
529
|
verifyBip322: input.verifyBip322,
|
|
412
530
|
skipSignatureVerification: input.skipSignatureVerification,
|
|
413
531
|
scopeMode: input.scopeMode,
|
|
414
|
-
skipTemporalCheck: true
|
|
532
|
+
skipTemporalCheck: true,
|
|
415
533
|
// action window check dominates
|
|
534
|
+
...input.decryptScopesWith ? { decryptScopesWith: input.decryptScopesWith } : {}
|
|
416
535
|
});
|
|
417
536
|
if (!dr.ok) return dr;
|
|
537
|
+
const rootHydrated = dr.envelope;
|
|
538
|
+
let parent = rootHydrated;
|
|
539
|
+
const hydratedChain = [];
|
|
540
|
+
for (const sub of chain) {
|
|
541
|
+
const hydrated = await hydrateSubdelegationScopes(sub, input);
|
|
542
|
+
if (!hydrated.ok) return hydrated;
|
|
543
|
+
const r = await verifyChainLink(hydrated.envelope, parent, input);
|
|
544
|
+
if (!r.ok) return r;
|
|
545
|
+
hydratedChain.push(hydrated.envelope);
|
|
546
|
+
parent = hydrated.envelope;
|
|
547
|
+
}
|
|
548
|
+
const leaf = hydratedChain.length > 0 ? hydratedChain[hydratedChain.length - 1] : rootHydrated;
|
|
418
549
|
if (a.v !== ENVELOPE_VERSION) {
|
|
419
550
|
return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
|
|
420
551
|
}
|
|
@@ -439,25 +570,28 @@ async function verifyAction(input) {
|
|
|
439
570
|
const ok = await input.verifyBip322(a.id, a.sig.value, a.signer.address);
|
|
440
571
|
if (!ok) return err("E_BAD_ACTION_STAMP", "action BIP-322 signature did not verify");
|
|
441
572
|
}
|
|
442
|
-
if (a.delegation_id !==
|
|
443
|
-
return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) !=
|
|
573
|
+
if (a.delegation_id !== leaf.id) {
|
|
574
|
+
return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != leaf.id (${leaf.id})`);
|
|
444
575
|
}
|
|
445
|
-
if (a.signer.address !==
|
|
446
|
-
return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) !=
|
|
576
|
+
if (a.signer.address !== leaf.agent.address) {
|
|
577
|
+
return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != leaf.agent (${leaf.agent.address})`);
|
|
447
578
|
}
|
|
448
|
-
const issued = new Date(
|
|
449
|
-
const expires = new Date(
|
|
579
|
+
const issued = new Date(leaf.issued_at).getTime();
|
|
580
|
+
const expires = new Date(leaf.expires_at).getTime();
|
|
450
581
|
const signed = new Date(a.signed_at).getTime();
|
|
451
582
|
if (Number.isNaN(issued) || Number.isNaN(expires) || Number.isNaN(signed)) {
|
|
452
583
|
return err("E_MALFORMED", "unparseable ISO 8601 timestamp");
|
|
453
584
|
}
|
|
454
585
|
if (signed < issued || signed >= expires) {
|
|
455
|
-
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside
|
|
586
|
+
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
|
|
587
|
+
}
|
|
588
|
+
if (!leaf.scopes) {
|
|
589
|
+
return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
|
|
456
590
|
}
|
|
457
591
|
let exercised, accepted;
|
|
458
592
|
try {
|
|
459
593
|
exercised = canonicalizeScope(parseScope(a.scope_exercised));
|
|
460
|
-
const granted =
|
|
594
|
+
const granted = leaf.scopes.map((s) => parseScope(s));
|
|
461
595
|
const exercisedParsed = parseScope(a.scope_exercised);
|
|
462
596
|
validateScope(exercisedParsed, { mode: input.scopeMode ?? "strict" });
|
|
463
597
|
accepted = granted.some((g) => isSubScope(exercisedParsed, g));
|
|
@@ -467,19 +601,22 @@ async function verifyAction(input) {
|
|
|
467
601
|
}
|
|
468
602
|
if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
|
|
469
603
|
if (input.revocations && input.revocations.length > 0) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
604
|
+
const allLinks = [rootHydrated, ...hydratedChain];
|
|
605
|
+
for (const link of allLinks) {
|
|
606
|
+
for (const rev of input.revocations) {
|
|
607
|
+
if (rev.delegation_id !== link.id) continue;
|
|
608
|
+
const rr = await verifyRevocation({
|
|
609
|
+
envelope: rev,
|
|
610
|
+
delegation: link,
|
|
611
|
+
verifyBip322: input.verifyBip322,
|
|
612
|
+
skipSignatureVerification: input.skipSignatureVerification
|
|
613
|
+
});
|
|
614
|
+
if (!rr.ok) continue;
|
|
615
|
+
const effective = effectiveRevocationTime(rev, input.resolveAnchorBlockHeight);
|
|
616
|
+
const actionTime = actionEffectiveTime(a, input.resolveAnchorBlockHeight);
|
|
617
|
+
if (compareTimes(effective, actionTime) <= 0) {
|
|
618
|
+
return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id} before action was signed`);
|
|
619
|
+
}
|
|
483
620
|
}
|
|
484
621
|
}
|
|
485
622
|
}
|
|
@@ -515,11 +652,56 @@ async function verifyAction(input) {
|
|
|
515
652
|
envelope: a,
|
|
516
653
|
canonicalMessage: reconstructedMessage,
|
|
517
654
|
id: a.id,
|
|
518
|
-
delegation:
|
|
655
|
+
delegation: rootHydrated,
|
|
656
|
+
chain: hydratedChain,
|
|
519
657
|
scopeExercised: exercised,
|
|
520
658
|
anchor
|
|
521
659
|
};
|
|
522
660
|
}
|
|
661
|
+
async function hydrateSubdelegationScopes(sub, input) {
|
|
662
|
+
if (sub.scopes !== void 0 && sub.scopes_encrypted !== void 0) {
|
|
663
|
+
return err(
|
|
664
|
+
"E_SCOPES_BOTH_PROVIDED",
|
|
665
|
+
"subdelegation carries both scopes and scopes_encrypted"
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if (sub.scopes === void 0 && sub.scopes_encrypted === void 0) {
|
|
669
|
+
return err(
|
|
670
|
+
"E_SCOPES_NEITHER_PROVIDED",
|
|
671
|
+
"subdelegation carries neither scopes nor scopes_encrypted"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
if (sub.scopes_encrypted === void 0) {
|
|
675
|
+
return { ok: true, envelope: sub };
|
|
676
|
+
}
|
|
677
|
+
if (sub.scopes_encrypted.from?.address !== sub.principal.address) {
|
|
678
|
+
return err(
|
|
679
|
+
"E_MALFORMED",
|
|
680
|
+
`subdelegation.scopes_encrypted.from.address (${sub.scopes_encrypted.from?.address}) does not match principal.address (${sub.principal.address})`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
if (!input.decryptScopesWith) {
|
|
684
|
+
return err(
|
|
685
|
+
"E_SCOPES_UNREADABLE",
|
|
686
|
+
"subdelegation carries scopes_encrypted; no decryption key provided for the chain"
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const r = await unsealScopes({
|
|
691
|
+
envelope: sub.scopes_encrypted,
|
|
692
|
+
device: input.decryptScopesWith,
|
|
693
|
+
...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
|
|
694
|
+
skipSenderVerification: !!input.skipSignatureVerification
|
|
695
|
+
});
|
|
696
|
+
return { ok: true, envelope: { ...sub, scopes: r.scopes } };
|
|
697
|
+
} catch (e) {
|
|
698
|
+
const msg = e.message ?? String(e);
|
|
699
|
+
if (/no matching recipient|no recipient|device_id/i.test(msg)) {
|
|
700
|
+
return err("E_SCOPES_UNREADABLE", msg);
|
|
701
|
+
}
|
|
702
|
+
return err("E_BAD_LOCK_ENVELOPE", msg);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
523
705
|
async function verifyRevocation(input) {
|
|
524
706
|
const env = input.envelope;
|
|
525
707
|
const d = input.delegation;
|
|
@@ -561,7 +743,10 @@ function checkDelegationShape(env) {
|
|
|
561
743
|
if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
562
744
|
if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
|
|
563
745
|
if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
|
|
564
|
-
if (
|
|
746
|
+
if (env.scopes !== void 0) {
|
|
747
|
+
if (!Array.isArray(env.scopes) || env.scopes.length === 0)
|
|
748
|
+
return err("E_MALFORMED", "scopes must be non-empty array");
|
|
749
|
+
}
|
|
565
750
|
if (env.bond !== null) {
|
|
566
751
|
if (!Number.isInteger(env.bond.sats) || env.bond.sats < 0) return err("E_MALFORMED", "bond.sats must be non-negative integer");
|
|
567
752
|
if (!isHex64(env.bond.attestation_id)) return err("E_MALFORMED", "bond.attestation_id must be 64-hex");
|
|
@@ -599,6 +784,138 @@ function checkRevocationShape(env) {
|
|
|
599
784
|
if (env.sig.pubkey !== env.signer.address) return err("E_MALFORMED", "sig.pubkey must equal signer.address");
|
|
600
785
|
return null;
|
|
601
786
|
}
|
|
787
|
+
function checkSubdelegationShape(env) {
|
|
788
|
+
if (env.kind !== "agent-subdelegation") return err("E_MALFORMED", 'kind must be "agent-subdelegation"');
|
|
789
|
+
if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
790
|
+
if (!isHex64(env.parent_id)) return err("E_MALFORMED", "parent_id must be 64-hex");
|
|
791
|
+
if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
|
|
792
|
+
if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
|
|
793
|
+
if (env.scopes !== void 0) {
|
|
794
|
+
if (!Array.isArray(env.scopes) || env.scopes.length === 0)
|
|
795
|
+
return err("E_MALFORMED", "scopes must be non-empty array");
|
|
796
|
+
}
|
|
797
|
+
if ("bond" in env && env.bond !== void 0) {
|
|
798
|
+
return err("E_MALFORMED", "sub-delegation envelopes MUST NOT carry a bond field");
|
|
799
|
+
}
|
|
800
|
+
if (!isIsoUtc(env.issued_at)) return err("E_MALFORMED", "issued_at must be ISO 8601 UTC");
|
|
801
|
+
if (!isIsoUtc(env.expires_at)) return err("E_MALFORMED", "expires_at must be ISO 8601 UTC");
|
|
802
|
+
if (!/^[0-9a-f]{32}$/.test(env.nonce)) return err("E_MALFORMED", "nonce must be 32 lowercase hex chars");
|
|
803
|
+
if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
|
|
804
|
+
if (env.sig.pubkey !== env.principal.address) return err("E_MALFORMED", "sig.pubkey must equal principal.address");
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
async function verifyChainLink(s, parent, input) {
|
|
808
|
+
if (s.v !== ENVELOPE_VERSION) {
|
|
809
|
+
return err("E_UNSUPPORTED_VERSION", `subdelegation version ${s.v} not supported`);
|
|
810
|
+
}
|
|
811
|
+
const shape = checkSubdelegationShape(s);
|
|
812
|
+
if (shape) return shape;
|
|
813
|
+
if (!s.scopes) {
|
|
814
|
+
return err(
|
|
815
|
+
"E_SCOPES_NEITHER_PROVIDED",
|
|
816
|
+
"subdelegation has no plaintext scopes \u2014 caller must hydrate v1.2 envelopes before invoking verifyChainLink"
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
let canonicalScopesList;
|
|
820
|
+
try {
|
|
821
|
+
canonicalScopesList = canonicalizeScopes(s.scopes);
|
|
822
|
+
} catch (e) {
|
|
823
|
+
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
824
|
+
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
825
|
+
}
|
|
826
|
+
const canonInput = {
|
|
827
|
+
parent_id: s.parent_id,
|
|
828
|
+
principal: s.principal.address,
|
|
829
|
+
agent: s.agent.address,
|
|
830
|
+
scopes: canonicalScopesList,
|
|
831
|
+
issued_at: s.issued_at,
|
|
832
|
+
expires_at: s.expires_at,
|
|
833
|
+
nonce: s.nonce
|
|
834
|
+
};
|
|
835
|
+
const reconstructedId = computeSubdelegationId(canonInput);
|
|
836
|
+
if (reconstructedId !== s.id) {
|
|
837
|
+
return err("E_BAD_ID", `reconstructed subdelegation id (${reconstructedId}) does not match envelope id (${s.id})`);
|
|
838
|
+
}
|
|
839
|
+
let parsedScopes;
|
|
840
|
+
try {
|
|
841
|
+
parsedScopes = s.scopes.map((str) => parseScope(str));
|
|
842
|
+
for (const p of parsedScopes) validateScope(p, { mode: input.scopeMode ?? "strict" });
|
|
843
|
+
} catch (e) {
|
|
844
|
+
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
845
|
+
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
846
|
+
}
|
|
847
|
+
if (!input.skipSignatureVerification) {
|
|
848
|
+
if (!input.verifyBip322) return err("E_BAD_SIG", "no BIP-322 verifier supplied for subdelegation");
|
|
849
|
+
const ok = await input.verifyBip322(s.id, s.sig.value, s.principal.address);
|
|
850
|
+
if (!ok) return err("E_BAD_SIG", "subdelegation BIP-322 signature did not verify");
|
|
851
|
+
}
|
|
852
|
+
if (s.parent_id !== parent.id) {
|
|
853
|
+
return err(
|
|
854
|
+
"E_SUBDELEGATION_PRINCIPAL_MISMATCH",
|
|
855
|
+
`subdelegation.parent_id (${s.parent_id}) does not match parent envelope id (${parent.id})`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (s.principal.address !== parent.agent.address) {
|
|
859
|
+
return err(
|
|
860
|
+
"E_SUBDELEGATION_PRINCIPAL_MISMATCH",
|
|
861
|
+
`subdelegation.principal (${s.principal.address}) does not match parent.agent (${parent.agent.address})`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
const sIssued = new Date(s.issued_at).getTime();
|
|
865
|
+
const sExpires = new Date(s.expires_at).getTime();
|
|
866
|
+
const pIssued = new Date(parent.issued_at).getTime();
|
|
867
|
+
const pExpires = new Date(parent.expires_at).getTime();
|
|
868
|
+
if (Number.isNaN(sIssued) || Number.isNaN(sExpires) || Number.isNaN(pIssued) || Number.isNaN(pExpires)) {
|
|
869
|
+
return err("E_MALFORMED", "unparseable ISO 8601 timestamp in chain");
|
|
870
|
+
}
|
|
871
|
+
if (sExpires <= sIssued) {
|
|
872
|
+
return err("E_MALFORMED", "subdelegation expires_at must be > issued_at");
|
|
873
|
+
}
|
|
874
|
+
if (sIssued < pIssued || sExpires > pExpires) {
|
|
875
|
+
return err(
|
|
876
|
+
"E_SUBDELEGATION_EXPIRES_EXTENDED",
|
|
877
|
+
`subdelegation window [${s.issued_at}, ${s.expires_at}) is not contained in parent's [${parent.issued_at}, ${parent.expires_at})`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
if (!parent.scopes) {
|
|
881
|
+
return err(
|
|
882
|
+
"E_SCOPES_NEITHER_PROVIDED",
|
|
883
|
+
"parent has no plaintext scopes \u2014 caller must hydrate v1.2 parents before invoking verifyChainLink"
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
const parentScopesParsed = parent.scopes.map((str) => parseScope(str));
|
|
887
|
+
for (let i = 0; i < parsedScopes.length; i++) {
|
|
888
|
+
const childScope = parsedScopes[i];
|
|
889
|
+
const containedBySomeParentScope = parentScopesParsed.some((p) => isSubScope(childScope, p));
|
|
890
|
+
if (!containedBySomeParentScope) {
|
|
891
|
+
return err(
|
|
892
|
+
"E_SUBDELEGATION_SCOPE_ESCALATED",
|
|
893
|
+
`subdelegation scope ${canonicalizeScope(childScope)} is not a sub-scope of any granted scope on the parent`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
ok: true,
|
|
899
|
+
envelope: s,
|
|
900
|
+
canonicalMessage: "",
|
|
901
|
+
// not populated for chain links; computeSubdelegationId is the binding form
|
|
902
|
+
id: s.id
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
async function verifySubdelegation(input) {
|
|
906
|
+
const hydrated = await hydrateSubdelegationScopes(input.envelope, input);
|
|
907
|
+
if (!hydrated.ok) return hydrated;
|
|
908
|
+
const r = await verifyChainLink(hydrated.envelope, input.parent, input);
|
|
909
|
+
if (!r.ok) return r;
|
|
910
|
+
if (!input.skipTemporalCheck) {
|
|
911
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
912
|
+
const issued = new Date(input.envelope.issued_at).getTime();
|
|
913
|
+
const expires = new Date(input.envelope.expires_at).getTime();
|
|
914
|
+
if (now < issued) return err("E_NOT_YET_VALID", `subdelegation issued_at ${input.envelope.issued_at} is in the future`);
|
|
915
|
+
if (now >= expires) return err("E_EXPIRED", `subdelegation expires_at ${input.envelope.expires_at} is past`);
|
|
916
|
+
}
|
|
917
|
+
return r;
|
|
918
|
+
}
|
|
602
919
|
function actionEffectiveTime(a, resolve) {
|
|
603
920
|
if (a.ots?.status === "confirmed") {
|
|
604
921
|
const h = resolve ? resolve(a) : a.ots.block_height;
|
|
@@ -638,6 +955,7 @@ Object.defineProperty(exports, "hexEncode", {
|
|
|
638
955
|
get: function () { return canonical.hexEncode; }
|
|
639
956
|
});
|
|
640
957
|
exports.AgentError = AgentError;
|
|
958
|
+
exports.DEFAULT_MAX_CHAIN_DEPTH = DEFAULT_MAX_CHAIN_DEPTH;
|
|
641
959
|
exports.ENVELOPE_VERSION = ENVELOPE_VERSION;
|
|
642
960
|
exports.REGISTERED_SCOPES = REGISTERED_SCOPES;
|
|
643
961
|
exports.ScopeParseError = ScopeParseError;
|
|
@@ -655,17 +973,26 @@ exports.canonicalizeScopes = canonicalizeScopes;
|
|
|
655
973
|
exports.computeActionId = computeActionId;
|
|
656
974
|
exports.computeDelegationId = computeDelegationId;
|
|
657
975
|
exports.computeRevocationId = computeRevocationId;
|
|
976
|
+
exports.computeSubdelegationId = computeSubdelegationId;
|
|
977
|
+
exports.decodeScopesPayload = decodeScopesPayload;
|
|
658
978
|
exports.delegationCanonicalBytes = delegationCanonicalBytes;
|
|
659
979
|
exports.delegationCanonicalMessage = delegationCanonicalMessage;
|
|
980
|
+
exports.encodeScopesPayload = encodeScopesPayload;
|
|
981
|
+
exports.hasPrivateScopes = hasPrivateScopes;
|
|
660
982
|
exports.isSubScope = isSubScope;
|
|
661
983
|
exports.parseAndCanonicalizeScopes = parseAndCanonicalizeScopes;
|
|
662
984
|
exports.parseScope = parseScope;
|
|
663
985
|
exports.revocationCanonicalBytes = revocationCanonicalBytes;
|
|
664
986
|
exports.revocationCanonicalMessage = revocationCanonicalMessage;
|
|
987
|
+
exports.sealScopes = sealScopes;
|
|
665
988
|
exports.sha256Hex = sha256Hex;
|
|
989
|
+
exports.subdelegationCanonicalBytes = subdelegationCanonicalBytes;
|
|
990
|
+
exports.subdelegationCanonicalMessage = subdelegationCanonicalMessage;
|
|
991
|
+
exports.unsealScopes = unsealScopes;
|
|
666
992
|
exports.validateScope = validateScope;
|
|
667
993
|
exports.verifyAction = verifyAction;
|
|
668
994
|
exports.verifyDelegation = verifyDelegation;
|
|
669
995
|
exports.verifyRevocation = verifyRevocation;
|
|
996
|
+
exports.verifySubdelegation = verifySubdelegation;
|
|
670
997
|
//# sourceMappingURL=index.js.map
|
|
671
998
|
//# sourceMappingURL=index.js.map
|