@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/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) {