@orangecheck/agent-core 1.2.0 → 2.1.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.js.map +1 -1
- package/dist/canonical.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +38 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38 -2
- package/dist/index.mjs.map +1 -1
- package/dist/scope.js +7 -2
- package/dist/scope.js.map +1 -1
- package/dist/scope.mjs +7 -2
- package/dist/scope.mjs.map +1 -1
- package/package.json +4 -4
- package/src/federation.test.ts +1 -0
- package/src/private-scope.test.ts +4 -3
- package/src/revocation-failclosed.test.ts +78 -0
- package/src/scope.test.ts +44 -0
- package/src/scope.ts +17 -2
- package/src/test-vectors.test.ts +5 -0
- package/src/verify.ts +56 -6
package/src/verify.ts
CHANGED
|
@@ -48,6 +48,24 @@ export interface VerifyBase {
|
|
|
48
48
|
verifyBip322?: (msg: string, signatureB64: string, address: string) => Promise<boolean>;
|
|
49
49
|
skipSignatureVerification?: boolean;
|
|
50
50
|
scopeMode?: ValidationOptions['mode'];
|
|
51
|
+
/**
|
|
52
|
+
* Verify WITHOUT consulting revocation feeds. Required to be explicit.
|
|
53
|
+
*
|
|
54
|
+
* SECURITY §7 item 7: "Query revocation feeds (Nostr kind-30085 by
|
|
55
|
+
* `#delegation`) for the cited delegation id before reporting OK, **unless
|
|
56
|
+
* the caller explicitly opts out**." The default was inverted — revocations
|
|
57
|
+
* were checked only when supplied, and omitting them skipped the check
|
|
58
|
+
* silently, so "checked, clean" was indistinguishable from "never checked".
|
|
59
|
+
*
|
|
60
|
+
* This library has no network by design, so the caller fetches the feed and
|
|
61
|
+
* passes `revocations`. Declining to is a decision they must state.
|
|
62
|
+
*/
|
|
63
|
+
skipRevocationCheck?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Revocations to evaluate against the delegation (or, for an action, every
|
|
66
|
+
* link in its chain). Fetch these from kind-30085 by `#delegation`.
|
|
67
|
+
*/
|
|
68
|
+
revocations?: RevocationEnvelope[];
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
export class AgentError extends Error {
|
|
@@ -204,6 +222,31 @@ export async function verifyDelegation(input: VerifyDelegationInput): Promise<Ve
|
|
|
204
222
|
if (!ok) return err('E_BAD_SIG', 'BIP-322 signature did not verify');
|
|
205
223
|
}
|
|
206
224
|
|
|
225
|
+
// Revocation. SECURITY §7 item 7 — check unless the caller states otherwise.
|
|
226
|
+
// Before this, verifyDelegation had NO revocations parameter at all: a
|
|
227
|
+
// revoked delegation returned ok:true and a caller had no way to say
|
|
228
|
+
// otherwise.
|
|
229
|
+
if (!input.skipRevocationCheck && !input.revocations) {
|
|
230
|
+
return err(
|
|
231
|
+
'E_MALFORMED',
|
|
232
|
+
'verifyDelegation requires `revocations` (fetch kind-30085 by #delegation) or an ' +
|
|
233
|
+
'explicit skipRevocationCheck:true to state that revocation is not being checked',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (input.revocations) {
|
|
237
|
+
for (const rev of input.revocations) {
|
|
238
|
+
if (rev.delegation_id !== env.id) continue;
|
|
239
|
+
const rr = await verifyRevocation({
|
|
240
|
+
envelope: rev,
|
|
241
|
+
delegation: env,
|
|
242
|
+
verifyBip322: input.verifyBip322,
|
|
243
|
+
skipSignatureVerification: input.skipSignatureVerification,
|
|
244
|
+
});
|
|
245
|
+
if (!rr.ok) continue; // a malformed revocation revokes nothing
|
|
246
|
+
return err('E_REVOKED', `delegation ${env.id} was revoked by ${rev.id}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
207
250
|
// Temporal.
|
|
208
251
|
if (!input.skipTemporalCheck) {
|
|
209
252
|
const now = input.now ?? new Date();
|
|
@@ -254,12 +297,6 @@ export interface VerifyActionInput extends VerifyBase {
|
|
|
254
297
|
* E_SUBDELEGATION_DEPTH_EXCEEDED before any per-link work is performed.
|
|
255
298
|
*/
|
|
256
299
|
maxChainDepth?: number;
|
|
257
|
-
/**
|
|
258
|
-
* Known revocations targeting any envelope in the chain (root + each
|
|
259
|
-
* subdelegation). The verifier checks every link per SUB-DELEGATION.md §2.2
|
|
260
|
-
* step 5 — a revocation against ANY link invalidates the action.
|
|
261
|
-
*/
|
|
262
|
-
revocations?: RevocationEnvelope[];
|
|
263
300
|
content?: Uint8Array;
|
|
264
301
|
verifyOtsAnchor?: (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean>;
|
|
265
302
|
/** If action and revocation are both OTS-anchored, pass a function that returns the comparable block height of each via proof parsing. Defaults: use envelope.ots.block_height. */
|
|
@@ -301,6 +338,12 @@ export async function verifyAction(input: VerifyActionInput): Promise<VerifyActi
|
|
|
301
338
|
skipSignatureVerification: input.skipSignatureVerification,
|
|
302
339
|
scopeMode: input.scopeMode,
|
|
303
340
|
skipTemporalCheck: true, // action window check dominates
|
|
341
|
+
// Revocation is evaluated ONCE, below, across every link in the chain
|
|
342
|
+
// (root + subdelegations) against the action's effective time — a
|
|
343
|
+
// per-link check here would use the wrong clock and would also
|
|
344
|
+
// double-report. Forward the caller's decision so this inner call does
|
|
345
|
+
// not trip the fail-closed guard.
|
|
346
|
+
skipRevocationCheck: true,
|
|
304
347
|
...(input.decryptScopesWith ? { decryptScopesWith: input.decryptScopesWith } : {}),
|
|
305
348
|
});
|
|
306
349
|
if (!dr.ok) return dr;
|
|
@@ -395,6 +438,13 @@ export async function verifyAction(input: VerifyActionInput): Promise<VerifyActi
|
|
|
395
438
|
// 6. Revocation check — applies per-link to ALL envelopes in the chain
|
|
396
439
|
// (root + every subdelegation). Per SUB-DELEGATION.md §2.2 step 5, a
|
|
397
440
|
// revocation against ANY link invalidates the action.
|
|
441
|
+
if (!input.skipRevocationCheck && !input.revocations) {
|
|
442
|
+
return err(
|
|
443
|
+
'E_MALFORMED',
|
|
444
|
+
'verifyAction requires `revocations` (fetch kind-30085 by #delegation for every link ' +
|
|
445
|
+
'in the chain) or an explicit skipRevocationCheck:true',
|
|
446
|
+
);
|
|
447
|
+
}
|
|
398
448
|
if (input.revocations && input.revocations.length > 0) {
|
|
399
449
|
const allLinks: ChainLink[] = [rootHydrated, ...hydratedChain];
|
|
400
450
|
for (const link of allLinks) {
|