@orangecheck/agent-core 0.2.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/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/types.ts +59 -5
- package/src/verify.ts +234 -18
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult } from './types.mjs';
|
|
1
|
+
import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult, ScopesEncryptedEnvelope } from './types.mjs';
|
|
2
2
|
export { ActionCanonicalInput, ActionContent, ActionOts, ActorRef, DelegationBond, DelegationCanonicalInput, DelegationRevocationRef, ENVELOPE_VERSION, EnvelopeKind, RevocationCanonicalInput, RevocationHolder, Signature, SubdelegationCanonicalInput, VerifyActionOkExtra, VerifyErr, VerifyOk } from './types.mjs';
|
|
3
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.mjs';
|
|
4
4
|
import { ValidationOptions } from './scope.mjs';
|
|
5
5
|
export { REGISTERED_SCOPES, Scope, ScopeConstraint, ScopeOp, ScopeParseError, canonicalizeScope, canonicalizeScopeString, isSubScope, parseScope, validateScope } from './scope.mjs';
|
|
6
|
+
import { SealInput, DeviceRecord, UnsealInput } from '@orangecheck/lock-core';
|
|
6
7
|
export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
|
|
7
8
|
|
|
8
9
|
declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
@@ -19,6 +20,10 @@ interface VerifyDelegationInput extends VerifyBase {
|
|
|
19
20
|
envelope: DelegationEnvelope;
|
|
20
21
|
now?: Date;
|
|
21
22
|
skipTemporalCheck?: boolean;
|
|
23
|
+
decryptScopesWith?: {
|
|
24
|
+
device_id: string;
|
|
25
|
+
secretKey: Uint8Array;
|
|
26
|
+
};
|
|
22
27
|
}
|
|
23
28
|
declare function verifyDelegation(input: VerifyDelegationInput): Promise<VerifyDelegationResult>;
|
|
24
29
|
interface VerifyActionInput extends VerifyBase {
|
|
@@ -30,6 +35,10 @@ interface VerifyActionInput extends VerifyBase {
|
|
|
30
35
|
content?: Uint8Array;
|
|
31
36
|
verifyOtsAnchor?: (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean>;
|
|
32
37
|
resolveAnchorBlockHeight?: (env: ActionEnvelope | RevocationEnvelope) => number | null;
|
|
38
|
+
decryptScopesWith?: {
|
|
39
|
+
device_id: string;
|
|
40
|
+
secretKey: Uint8Array;
|
|
41
|
+
};
|
|
33
42
|
}
|
|
34
43
|
declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
|
|
35
44
|
interface VerifyRevocationInput extends VerifyBase {
|
|
@@ -42,7 +51,43 @@ interface VerifySubdelegationInput extends VerifyBase {
|
|
|
42
51
|
parent: ChainLink;
|
|
43
52
|
skipTemporalCheck?: boolean;
|
|
44
53
|
now?: Date;
|
|
54
|
+
decryptScopesWith?: {
|
|
55
|
+
device_id: string;
|
|
56
|
+
secretKey: Uint8Array;
|
|
57
|
+
};
|
|
45
58
|
}
|
|
46
59
|
declare function verifySubdelegation(input: VerifySubdelegationInput): Promise<VerifySubdelegationResult>;
|
|
47
60
|
|
|
48
|
-
|
|
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
|
+
};
|
|
92
|
+
|
|
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.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult } from './types.js';
|
|
1
|
+
import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult, ScopesEncryptedEnvelope } from './types.js';
|
|
2
2
|
export { ActionCanonicalInput, ActionContent, ActionOts, ActorRef, DelegationBond, DelegationCanonicalInput, DelegationRevocationRef, ENVELOPE_VERSION, EnvelopeKind, RevocationCanonicalInput, RevocationHolder, Signature, SubdelegationCanonicalInput, VerifyActionOkExtra, VerifyErr, VerifyOk } from './types.js';
|
|
3
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
|
|
|
8
9
|
declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
@@ -19,6 +20,10 @@ interface VerifyDelegationInput extends VerifyBase {
|
|
|
19
20
|
envelope: DelegationEnvelope;
|
|
20
21
|
now?: Date;
|
|
21
22
|
skipTemporalCheck?: boolean;
|
|
23
|
+
decryptScopesWith?: {
|
|
24
|
+
device_id: string;
|
|
25
|
+
secretKey: Uint8Array;
|
|
26
|
+
};
|
|
22
27
|
}
|
|
23
28
|
declare function verifyDelegation(input: VerifyDelegationInput): Promise<VerifyDelegationResult>;
|
|
24
29
|
interface VerifyActionInput extends VerifyBase {
|
|
@@ -30,6 +35,10 @@ interface VerifyActionInput extends VerifyBase {
|
|
|
30
35
|
content?: Uint8Array;
|
|
31
36
|
verifyOtsAnchor?: (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean>;
|
|
32
37
|
resolveAnchorBlockHeight?: (env: ActionEnvelope | RevocationEnvelope) => number | null;
|
|
38
|
+
decryptScopesWith?: {
|
|
39
|
+
device_id: string;
|
|
40
|
+
secretKey: Uint8Array;
|
|
41
|
+
};
|
|
33
42
|
}
|
|
34
43
|
declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
|
|
35
44
|
interface VerifyRevocationInput extends VerifyBase {
|
|
@@ -42,7 +51,43 @@ interface VerifySubdelegationInput extends VerifyBase {
|
|
|
42
51
|
parent: ChainLink;
|
|
43
52
|
skipTemporalCheck?: boolean;
|
|
44
53
|
now?: Date;
|
|
54
|
+
decryptScopesWith?: {
|
|
55
|
+
device_id: string;
|
|
56
|
+
secretKey: Uint8Array;
|
|
57
|
+
};
|
|
45
58
|
}
|
|
46
59
|
declare function verifySubdelegation(input: VerifySubdelegationInput): Promise<VerifySubdelegationResult>;
|
|
47
60
|
|
|
48
|
-
|
|
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
|
+
};
|
|
92
|
+
|
|
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;
|
|
@@ -352,6 +353,50 @@ function canonicalRevocationBytes(env) {
|
|
|
352
353
|
function sha256Hex(bytes) {
|
|
353
354
|
return canonical.hexEncode(sha256.sha256(bytes));
|
|
354
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
|
|
355
400
|
var DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
356
401
|
var AgentError = class extends Error {
|
|
357
402
|
constructor(code, message) {
|
|
@@ -367,19 +412,63 @@ async function verifyDelegation(input) {
|
|
|
367
412
|
}
|
|
368
413
|
const shape = checkDelegationShape(env);
|
|
369
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
|
+
}
|
|
370
459
|
let canonicalScopes;
|
|
371
460
|
try {
|
|
372
|
-
for (const s of
|
|
373
|
-
canonicalScopes = canonicalizeScopes(
|
|
461
|
+
for (const s of workingScopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
|
|
462
|
+
canonicalScopes = canonicalizeScopes(workingScopes);
|
|
374
463
|
} catch (e) {
|
|
375
464
|
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
376
465
|
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
377
466
|
}
|
|
378
467
|
for (let i = 0; i < canonicalScopes.length; i++) {
|
|
379
|
-
if (
|
|
468
|
+
if (workingScopes[i] !== canonicalScopes[i]) {
|
|
380
469
|
return err(
|
|
381
470
|
"E_BAD_SCOPE_GRAMMAR",
|
|
382
|
-
`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]}`
|
|
383
472
|
);
|
|
384
473
|
}
|
|
385
474
|
}
|
|
@@ -416,9 +505,10 @@ async function verifyDelegation(input) {
|
|
|
416
505
|
if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
|
|
417
506
|
if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
|
|
418
507
|
}
|
|
508
|
+
const returnedEnvelope = env.scopes_encrypted !== void 0 ? { ...env, scopes: canonicalScopes } : env;
|
|
419
509
|
return {
|
|
420
510
|
ok: true,
|
|
421
|
-
envelope:
|
|
511
|
+
envelope: returnedEnvelope,
|
|
422
512
|
canonicalMessage: reconstructedMessage,
|
|
423
513
|
id: env.id
|
|
424
514
|
};
|
|
@@ -439,17 +529,23 @@ async function verifyAction(input) {
|
|
|
439
529
|
verifyBip322: input.verifyBip322,
|
|
440
530
|
skipSignatureVerification: input.skipSignatureVerification,
|
|
441
531
|
scopeMode: input.scopeMode,
|
|
442
|
-
skipTemporalCheck: true
|
|
532
|
+
skipTemporalCheck: true,
|
|
443
533
|
// action window check dominates
|
|
534
|
+
...input.decryptScopesWith ? { decryptScopesWith: input.decryptScopesWith } : {}
|
|
444
535
|
});
|
|
445
536
|
if (!dr.ok) return dr;
|
|
446
|
-
|
|
537
|
+
const rootHydrated = dr.envelope;
|
|
538
|
+
let parent = rootHydrated;
|
|
539
|
+
const hydratedChain = [];
|
|
447
540
|
for (const sub of chain) {
|
|
448
|
-
const
|
|
541
|
+
const hydrated = await hydrateSubdelegationScopes(sub, input);
|
|
542
|
+
if (!hydrated.ok) return hydrated;
|
|
543
|
+
const r = await verifyChainLink(hydrated.envelope, parent, input);
|
|
449
544
|
if (!r.ok) return r;
|
|
450
|
-
|
|
545
|
+
hydratedChain.push(hydrated.envelope);
|
|
546
|
+
parent = hydrated.envelope;
|
|
451
547
|
}
|
|
452
|
-
const leaf =
|
|
548
|
+
const leaf = hydratedChain.length > 0 ? hydratedChain[hydratedChain.length - 1] : rootHydrated;
|
|
453
549
|
if (a.v !== ENVELOPE_VERSION) {
|
|
454
550
|
return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
|
|
455
551
|
}
|
|
@@ -489,6 +585,9 @@ async function verifyAction(input) {
|
|
|
489
585
|
if (signed < issued || signed >= expires) {
|
|
490
586
|
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
|
|
491
587
|
}
|
|
588
|
+
if (!leaf.scopes) {
|
|
589
|
+
return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
|
|
590
|
+
}
|
|
492
591
|
let exercised, accepted;
|
|
493
592
|
try {
|
|
494
593
|
exercised = canonicalizeScope(parseScope(a.scope_exercised));
|
|
@@ -502,7 +601,7 @@ async function verifyAction(input) {
|
|
|
502
601
|
}
|
|
503
602
|
if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
|
|
504
603
|
if (input.revocations && input.revocations.length > 0) {
|
|
505
|
-
const allLinks = [
|
|
604
|
+
const allLinks = [rootHydrated, ...hydratedChain];
|
|
506
605
|
for (const link of allLinks) {
|
|
507
606
|
for (const rev of input.revocations) {
|
|
508
607
|
if (rev.delegation_id !== link.id) continue;
|
|
@@ -553,12 +652,56 @@ async function verifyAction(input) {
|
|
|
553
652
|
envelope: a,
|
|
554
653
|
canonicalMessage: reconstructedMessage,
|
|
555
654
|
id: a.id,
|
|
556
|
-
delegation:
|
|
557
|
-
chain,
|
|
655
|
+
delegation: rootHydrated,
|
|
656
|
+
chain: hydratedChain,
|
|
558
657
|
scopeExercised: exercised,
|
|
559
658
|
anchor
|
|
560
659
|
};
|
|
561
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
|
+
}
|
|
562
705
|
async function verifyRevocation(input) {
|
|
563
706
|
const env = input.envelope;
|
|
564
707
|
const d = input.delegation;
|
|
@@ -600,7 +743,10 @@ function checkDelegationShape(env) {
|
|
|
600
743
|
if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
601
744
|
if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
|
|
602
745
|
if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
|
|
603
|
-
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
|
+
}
|
|
604
750
|
if (env.bond !== null) {
|
|
605
751
|
if (!Number.isInteger(env.bond.sats) || env.bond.sats < 0) return err("E_MALFORMED", "bond.sats must be non-negative integer");
|
|
606
752
|
if (!isHex64(env.bond.attestation_id)) return err("E_MALFORMED", "bond.attestation_id must be 64-hex");
|
|
@@ -644,7 +790,10 @@ function checkSubdelegationShape(env) {
|
|
|
644
790
|
if (!isHex64(env.parent_id)) return err("E_MALFORMED", "parent_id must be 64-hex");
|
|
645
791
|
if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
|
|
646
792
|
if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
|
|
647
|
-
if (
|
|
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
|
+
}
|
|
648
797
|
if ("bond" in env && env.bond !== void 0) {
|
|
649
798
|
return err("E_MALFORMED", "sub-delegation envelopes MUST NOT carry a bond field");
|
|
650
799
|
}
|
|
@@ -661,6 +810,12 @@ async function verifyChainLink(s, parent, input) {
|
|
|
661
810
|
}
|
|
662
811
|
const shape = checkSubdelegationShape(s);
|
|
663
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
|
+
}
|
|
664
819
|
let canonicalScopesList;
|
|
665
820
|
try {
|
|
666
821
|
canonicalScopesList = canonicalizeScopes(s.scopes);
|
|
@@ -722,6 +877,12 @@ async function verifyChainLink(s, parent, input) {
|
|
|
722
877
|
`subdelegation window [${s.issued_at}, ${s.expires_at}) is not contained in parent's [${parent.issued_at}, ${parent.expires_at})`
|
|
723
878
|
);
|
|
724
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
|
+
}
|
|
725
886
|
const parentScopesParsed = parent.scopes.map((str) => parseScope(str));
|
|
726
887
|
for (let i = 0; i < parsedScopes.length; i++) {
|
|
727
888
|
const childScope = parsedScopes[i];
|
|
@@ -742,7 +903,9 @@ async function verifyChainLink(s, parent, input) {
|
|
|
742
903
|
};
|
|
743
904
|
}
|
|
744
905
|
async function verifySubdelegation(input) {
|
|
745
|
-
const
|
|
906
|
+
const hydrated = await hydrateSubdelegationScopes(input.envelope, input);
|
|
907
|
+
if (!hydrated.ok) return hydrated;
|
|
908
|
+
const r = await verifyChainLink(hydrated.envelope, input.parent, input);
|
|
746
909
|
if (!r.ok) return r;
|
|
747
910
|
if (!input.skipTemporalCheck) {
|
|
748
911
|
const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
@@ -811,16 +974,21 @@ exports.computeActionId = computeActionId;
|
|
|
811
974
|
exports.computeDelegationId = computeDelegationId;
|
|
812
975
|
exports.computeRevocationId = computeRevocationId;
|
|
813
976
|
exports.computeSubdelegationId = computeSubdelegationId;
|
|
977
|
+
exports.decodeScopesPayload = decodeScopesPayload;
|
|
814
978
|
exports.delegationCanonicalBytes = delegationCanonicalBytes;
|
|
815
979
|
exports.delegationCanonicalMessage = delegationCanonicalMessage;
|
|
980
|
+
exports.encodeScopesPayload = encodeScopesPayload;
|
|
981
|
+
exports.hasPrivateScopes = hasPrivateScopes;
|
|
816
982
|
exports.isSubScope = isSubScope;
|
|
817
983
|
exports.parseAndCanonicalizeScopes = parseAndCanonicalizeScopes;
|
|
818
984
|
exports.parseScope = parseScope;
|
|
819
985
|
exports.revocationCanonicalBytes = revocationCanonicalBytes;
|
|
820
986
|
exports.revocationCanonicalMessage = revocationCanonicalMessage;
|
|
987
|
+
exports.sealScopes = sealScopes;
|
|
821
988
|
exports.sha256Hex = sha256Hex;
|
|
822
989
|
exports.subdelegationCanonicalBytes = subdelegationCanonicalBytes;
|
|
823
990
|
exports.subdelegationCanonicalMessage = subdelegationCanonicalMessage;
|
|
991
|
+
exports.unsealScopes = unsealScopes;
|
|
824
992
|
exports.validateScope = validateScope;
|
|
825
993
|
exports.verifyAction = verifyAction;
|
|
826
994
|
exports.verifyDelegation = verifyDelegation;
|