@orangecheck/agent-core 2.7.0 → 2.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/agent-core",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "description": "OC Agent canonical messages, envelope formats (delegation/action/revocation), scope grammar, and verification. See https://github.com/orangecheck/oc-agent-protocol.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -245,7 +245,7 @@ d('federation delegation revocation check', () => {
245
245
  };
246
246
  }
247
247
 
248
- it('honours an agent self-revocation only when holders include the agent', async () => {
248
+ it('honours the agent’s own revocation whatever holders says', async () => {
249
249
  const dv = await load('v19-federation-delegation-3of5-valid.json');
250
250
  const del = dv.expected.envelope as FederationDelegationEnvelope;
251
251
  const principalOnly = { ...del, revocation: { holders: ['principal' as const], ref: null } };
@@ -253,12 +253,12 @@ d('federation delegation revocation check', () => {
253
253
  ...del,
254
254
  revocation: { holders: ['principal' as const, 'agent' as const], ref: null },
255
255
  };
256
- const ignored = await verifyFederationDelegation({
256
+ const revokedToo = await verifyFederationDelegation({
257
257
  envelope: principalOnly,
258
258
  revocations: [agentRevocation(principalOnly)],
259
259
  ...base,
260
260
  });
261
- expect(ignored.ok).toBe(true);
261
+ expect(revokedToo.ok === false && revokedToo.code).toBe('E_REVOKED');
262
262
  const revoked = await verifyFederationDelegation({
263
263
  envelope: withAgent,
264
264
  revocations: [agentRevocation(withAgent)],
package/src/federation.ts CHANGED
@@ -230,7 +230,7 @@ export interface VerifyFederationDelegationInput extends VerifyFederationBase {
230
230
  envelope: FederationDelegationEnvelope;
231
231
  /**
232
232
  * Revocations to check (kind-30085 by `#t` with the delegation id, SPEC §9.4): federation revocations,
233
- * and agent self-revocations when `revocation.holders` includes "agent".
233
+ * and the agent's own revocations.
234
234
  * Required unless `skipRevocationCheck` states that revocation is not checked.
235
235
  */
236
236
  revocations?: Array<FederationRevocationEnvelope | RevocationEnvelope>;
@@ -316,9 +316,8 @@ export async function verifyFederationDelegation(
316
316
  }
317
317
 
318
318
  /**
319
- * FEDERATION.md §4: the federation (M-of-N) may always revoke; the agent may
320
- * self-revoke with its own BIP-322 signature when `revocation.holders`
321
- * includes "agent".
319
+ * FEDERATION.md §4: the federation (M-of-N) may always revoke, and so may the
320
+ * agent with its own BIP-322 signature, whatever `revocation.holders` says.
322
321
  */
323
322
  async function verifyRevocationOf(
324
323
  d: FederationDelegationEnvelope,
@@ -334,10 +333,6 @@ async function verifyRevocationOf(
334
333
  });
335
334
  }
336
335
  const v1 = rev as RevocationEnvelope;
337
- const holders = d.revocation?.holders ?? ['principal'];
338
- if (!Array.isArray(holders) || !holders.includes('agent')) {
339
- return fail('E_REVOKER_UNAUTHORIZED', 'only the federation may revoke this delegation');
340
- }
341
336
  if (v1.kind !== 'agent-revocation' || v1.signer?.address !== d.agent.address) {
342
337
  return fail('E_REVOKER_UNAUTHORIZED', 'a single-address revocation must be signed by the agent');
343
338
  }
@@ -117,22 +117,24 @@ describe('revocation authority always includes the principal', () => {
117
117
  });
118
118
  }
119
119
 
120
- it('the agent may revoke only when holders lists it', async () => {
121
- const open = delegation(['principal', 'agent']);
122
- const closed = delegation(['principal']);
123
- const okR = await verifyRevocation({
124
- envelope: revocation(open, AGENT, '2026-04-23T00:00:00Z'),
125
- delegation: open,
126
- skipSignatureVerification: true,
127
- });
128
- expect(okR.ok).toBe(true);
129
- const noR = await verifyRevocation({
130
- envelope: revocation(closed, AGENT, '2026-04-23T00:00:00Z'),
131
- delegation: closed,
132
- skipSignatureVerification: true,
120
+ // A copy of the delegation presented with holders ["principal"] has the same
121
+ // id and signature as one with the agent listed, so it must not undo the
122
+ // agent's own revocation (SPEC §9.5).
123
+ for (const holders of [['principal', 'agent'], ['principal'], [], undefined]) {
124
+ it(`the agent's revocation is honoured with holders=${JSON.stringify(holders)}`, async () => {
125
+ const d = delegation(holders);
126
+ const rev = revocation(d, AGENT, '2026-04-23T00:00:00Z');
127
+ const rr = await verifyRevocation({ envelope: rev, delegation: d, skipSignatureVerification: true });
128
+ expect(rr.ok).toBe(true);
129
+ const dr = await verifyDelegation({
130
+ envelope: d,
131
+ revocations: [rev],
132
+ skipSignatureVerification: true,
133
+ now: AT,
134
+ });
135
+ expect(dr.ok === false && dr.code).toBe('E_REVOKED');
133
136
  });
134
- expect(noR.ok === false && noR.code).toBe('E_REVOKER_UNAUTHORIZED');
135
- });
137
+ }
136
138
 
137
139
  it('a third party is never an authorised revoker', async () => {
138
140
  const d = delegation(['principal', 'agent']);
package/src/verify.ts CHANGED
@@ -717,13 +717,14 @@ export async function verifyRevocation(input: VerifyRevocationInput): Promise<Ve
717
717
  return err('E_DELEGATION_MISMATCH', `revocation.delegation_id (${env.delegation_id}) != delegation.id (${d.id})`);
718
718
  }
719
719
 
720
- // The principal may always revoke (SPEC §9.5). `revocation.holders` is not
721
- // covered by the delegation signature, so it may only add the agent.
722
- const holders = d.revocation?.holders ?? ['principal'];
723
- const holderAddrs = new Set<string>([d.principal.address]);
724
- if (Array.isArray(holders) && holders.includes('agent')) holderAddrs.add(d.agent.address);
725
- if (!holderAddrs.has(env.signer.address)) {
726
- return err('E_REVOKER_UNAUTHORIZED', `revocation signer ${env.signer.address} not in delegation holders`);
720
+ // The principal and the agent may always revoke (SPEC §9.5). Revoking only
721
+ // removes authority, and `revocation.holders` is outside the delegation's
722
+ // signature, so no copy of it decides who may.
723
+ if (env.signer.address !== d.principal.address && env.signer.address !== d.agent.address) {
724
+ return err(
725
+ 'E_REVOKER_UNAUTHORIZED',
726
+ `revocation signer ${env.signer.address} is neither the principal nor the agent`
727
+ );
727
728
  }
728
729
 
729
730
  const canonInput = {