iso-ucan 0.5.0 → 0.5.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.
@@ -52,7 +52,7 @@ export function sign(options: EnvelopeSignOptions): Promise<{
52
52
  * @param {EnvelopeEncodeOptions} options
53
53
  *
54
54
  */
55
- export function encode(options: EnvelopeEncodeOptions): ByteView<(Uint8Array<ArrayBufferLike> | SignaturePayload<unknown>)[]>;
55
+ export function encode(options: EnvelopeEncodeOptions): ByteView<(Uint8Array<ArrayBufferLike> | SignaturePayload<unknown>)[], ArrayBufferLike>;
56
56
  /**
57
57
  * Decode a UCAN envelope
58
58
  *
@@ -1 +1 @@
1
- {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../../src/envelope.js"],"names":[],"mappings":"AAYA;;;;;;;;GAQG;AACH,6CALG;IAA6B,IAAI,EAAzB,WAAW;IACM,OAAO,EAAxB,OAAO;IACU,OAAO;IAC8B,aAAa,EAAnE,OAAO,sBAAsB,EAAE,aAAa;CACtD,oBAgBA;AAED;;;;GAIG;AACH,8CAFW,UAAU;;;;EAkCpB;AAED;;;;;GAKG;AACH,6CAHW,OAAO,GACL,OAAO,IAAI,iBAAiB,CAIxC;AAED;;;;;GAKG;AACH,6CAHW,OAAO,GACL,OAAO,IAAI,iBAAiB,CAIxC;AAED;;;GAGG;AACH,8BAFW,mBAAmB;;;GAgB7B;AAED;;;;;;GAMG;AACH,gCAHW,qBAAqB,yEAS/B;AAED;;;;;;;GAOG;AACH,uBAL2B,IAAI,SAAlB,WAAa,WAEf,qBAAqB,GACnB,gBAAgB,IAAI,CAAC,CAgCjC;AAtKD;;;GAGG;AAEH,cAAc;AACd,sBAAuB,YAAY,CAAA;iCALuK,YAAY;6BAAZ,YAAY;sCAAZ,YAAY;uCAAZ,YAAY;uCAAZ,YAAY;yCAAZ,YAAY;2CAAZ,YAAY;8BACzL,cAAc;2CAD+J,YAAY;qCAAZ,YAAY"}
1
+ {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../../src/envelope.js"],"names":[],"mappings":"AAYA;;;;;;;;GAQG;AACH,6CALG;IAA6B,IAAI,EAAzB,WAAW;IACM,OAAO,EAAxB,OAAO;IACU,OAAO;IAC8B,aAAa,EAAnE,OAAO,sBAAsB,EAAE,aAAa;CACtD,oBAgBA;AAED;;;;GAIG;AACH,8CAFW,UAAU;;;;EAkCpB;AAED;;;;;GAKG;AACH,6CAHW,OAAO,GACL,OAAO,IAAI,iBAAiB,CAIxC;AAED;;;;;GAKG;AACH,6CAHW,OAAO,GACL,OAAO,IAAI,iBAAiB,CAIxC;AAED;;;GAGG;AACH,8BAFW,mBAAmB;;;GAgB7B;AAED;;;;;;GAMG;AACH,gCAHW,qBAAqB,0FAS/B;AAED;;;;;;;GAOG;AACH,uBAL2B,IAAI,SAAlB,WAAa,WAEf,qBAAqB,GACnB,gBAAgB,IAAI,CAAC,CAgCjC;AAtKD;;;GAGG;AAEH,cAAc;AACd,sBAAuB,YAAY,CAAA;iCALuK,YAAY;6BAAZ,YAAY;sCAAZ,YAAY;uCAAZ,YAAY;uCAAZ,YAAY;yCAAZ,YAAY;2CAAZ,YAAY;8BACzL,cAAc;2CAD+J,YAAY;qCAAZ,YAAY"}
@@ -1 +1 @@
1
- {"version":3,"file":"invocation.d.ts","sourceRoot":"","sources":["../../src/invocation.js"],"names":[],"mappings":"AAqLA;;;;GAIG;AACH,sCAHW,OAAO,YAAY,EAAE,iBAAiB,UACtC,UAAU,EAAE,WA6DtB;AAtOD;;;GAGG;AAEH;;GAEG;AACH;IA0BE;;;OAGG;IACH,qBAFW,OAAO,YAAY,EAAE,qBAAqB,uBAgDpD;IAED;;OAEG;IACH,uBAFW,OAAO,YAAY,EAAE,iBAAiB,uBA0DhD;IA9HD;;;;;;OAMG;IACH,sBALW,OAAO,YAAY,EAAE,eAAe,CAAC,KAAK,CAAC,SAC3C,UAAU,OACV,GAAG,eACH,UAAU,EAAE,EAQtB;IAvBD;;OAEG;IACH,aAFU,UAAU,EAAE,CAEX;IAEX;;OAEG;IACH,KAFU,GAAG,CAEV;IAUD,sDAAwB;IACxB,gDAA+B;IAC/B,mCAAkB;CAqHrB;gCAjJ4B,iBAAiB;yBACxB,cAAc"}
1
+ {"version":3,"file":"invocation.d.ts","sourceRoot":"","sources":["../../src/invocation.js"],"names":[],"mappings":"AAsLA;;;;GAIG;AACH,sCAHW,OAAO,YAAY,EAAE,iBAAiB,UACtC,UAAU,EAAE,WA6DtB;AAtOD;;;GAGG;AAEH;;GAEG;AACH;IA0BE;;;OAGG;IACH,qBAFW,OAAO,YAAY,EAAE,qBAAqB,uBAgDpD;IAED;;OAEG;IACH,uBAFW,OAAO,YAAY,EAAE,iBAAiB,uBA0DhD;IA9HD;;;;;;OAMG;IACH,sBALW,OAAO,YAAY,EAAE,eAAe,CAAC,KAAK,CAAC,SAC3C,UAAU,OACV,GAAG,eACH,UAAU,EAAE,EAQtB;IAvBD;;OAEG;IACH,aAFU,UAAU,EAAE,CAEX;IAEX;;OAEG;IACH,KAFU,GAAG,CAEV;IAUD,sDAAwB;IACxB,gDAA+B;IAC/B,mCAAkB;CAqHrB;gCAjJ4B,iBAAiB;yBACxB,cAAc"}
@@ -75,7 +75,7 @@ export function receipt<Result extends z.ZodType, Error extends z.ZodType>(resul
75
75
  * produces when serialized through `JSON.stringify`) and normalizes to a
76
76
  * {@link CID}.
77
77
  */
78
- export const cidSchema: z.ZodUnion<readonly [z.ZodCustom<CID<unknown, number, number, import("multiformats").Version>, CID<unknown, number, number, import("multiformats").Version>>, z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodObject<{
78
+ export const cidSchema: z.ZodUnion<readonly [z.ZodInstanceOf<CID<unknown, number, number, import("multiformats").Version>>, z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodObject<{
79
79
  '/': z.ZodString;
80
80
  }, z.core.$strip>, z.ZodTransform<string, {
81
81
  '/': string;
@@ -100,7 +100,7 @@ export const receiptServerError: z.ZodObject<{
100
100
  error: z.ZodObject<{
101
101
  code: z.ZodLiteral<"SERVER_ERROR">;
102
102
  message: z.ZodString;
103
- cid: z.ZodOptional<z.ZodUnion<readonly [z.ZodCustom<CID<unknown, number, number, import("multiformats").Version>, CID<unknown, number, number, import("multiformats").Version>>, z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodObject<{
103
+ cid: z.ZodOptional<z.ZodUnion<readonly [z.ZodInstanceOf<CID<unknown, number, number, import("multiformats").Version>>, z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodPipe<z.ZodObject<{
104
104
  '/': z.ZodString;
105
105
  }, z.core.$strip>, z.ZodTransform<string, {
106
106
  '/': string;
@@ -53,6 +53,21 @@ export function validateIssuerAndSignature(envelope: import("./types.js").Decode
53
53
  * @param {import('iso-did').Resolver} [didResolver]
54
54
  */
55
55
  export function verifySignature(envelope: import("./types.js").DecodedEnvelope<PayloadSpec>, signatureVerifierResolver: import("iso-signatures/verifiers/resolver.js").Resolver, didResolver?: import("iso-did").Resolver): Promise<boolean>;
56
+ /**
57
+ * Check if a command is covered by (equal to or a segment-wise child of) another command.
58
+ *
59
+ * Commands are compared by path segment, not by string prefix: `/crypto` covers
60
+ * `/crypto` and `/crypto/sign` but not `/cryptocurrency`. The command `/` covers
61
+ * every command.
62
+ *
63
+ * Both commands are expected to be valid per {@link assertIsValidCommand}, so
64
+ * `${parent}/` is always a real segment boundary.
65
+ *
66
+ * @param {string} parent - The broader command, e.g. from a delegation.
67
+ * @param {string} child - The command that must be attenuated from `parent`.
68
+ * @returns {boolean}
69
+ */
70
+ export function commandCovers(parent: string, child: string): boolean;
56
71
  /**
57
72
  * Asserts that a UCAN command string is syntactically valid.
58
73
  * If the command is invalid, it throws a descriptive error.
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.js"],"names":[],"mappings":"AASA;;GAEG;AAEH;;;;;GAKG;AACH,2CAHW,OAAO,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC;;;EAe3D;AAED,uCAEC;AAED;;;GAGG;AACH,8BAFW,OAAO,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC,iCAW3D;AAED;;;;;GAKG;AACH,2CAHW,OAAO,eAAe,EAAE,aAAa,WACrC,OAAO,sBAAsB,EAAE,aAAa,WA+BtD;AAED;;;;;GAKG;AACH,sCAHW,MAAM,GAAG,IAAI,QACb,MAAM,QAchB;AAED;;;;;GAKG;AACH,sCAHW,MAAM,QACN,MAAM,QAMhB;AAED;;;;;GAKG;AACH,qDAHW,OAAO,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC,gBACjD,OAAO,SAAS,EAAE,QAAQ,gBAYpC;AAED;;;;;;GAMG;AACH,0CAJW,OAAO,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC,6BACjD,OAAO,sCAAsC,EAAE,QAAQ,gBACvD,OAAO,SAAS,EAAE,QAAQ,oBAwBpC;AAED;;;;;;;;;;;;;GAaG;AACH,8CAJW,MAAM,GAEJ,IAAI,CAuChB;AAED;;;;;;;;;;;;;GAaG;AACH,mCAXW,OAAO,GAEL,IAAI,CAehB;AACD;;;;GAIG;AACH,iCAFW,OAAO,QASjB;AAED;;;;;;GAMG;AACH,iCAJW,OAAO,GAEL,IAAI,CAWhB;AAED;;;;;GAKG;AACH,sCAHW,OAAO,kBAAkB,EAAE,GAAG,gBAC9B,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,iBAQxC;AAED;;;;;;GAMG;AACH,uCAHG;IAAgC,GAAG;IACV,GAAG;CAAC,iBAU/B;AAgED;;GAEG;AACH,qCAFW,OAAO,QASjB;AAED;;;;;;GAMG;AACH,gCAJW,OAAO,GACL,KAAK,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAKjD;AAnFD,iJAeI;AACJ,4KAAyD;AAEzD,oGAGE;AAEF;;GAEG;AAEH;;GAEG;AACH,mKA4BI;AAEJ,kLAA8C;uBApCjC,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC;iCAjUP,YAAY;oBARtB,kBAAkB;oBADlB,SAAS;kBAGX,QAAQ"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.js"],"names":[],"mappings":"AASA;;GAEG;AAEH;;;;;GAKG;AACH,2CAHW,OAAO,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC;;;EAe3D;AAED,uCAEC;AAED;;;GAGG;AACH,8BAFW,OAAO,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC,iCAW3D;AAED;;;;;GAKG;AACH,2CAHW,OAAO,eAAe,EAAE,aAAa,WACrC,OAAO,sBAAsB,EAAE,aAAa,WA+BtD;AAED;;;;;GAKG;AACH,sCAHW,MAAM,GAAG,IAAI,QACb,MAAM,QAchB;AAED;;;;;GAKG;AACH,sCAHW,MAAM,QACN,MAAM,QAMhB;AAED;;;;;GAKG;AACH,qDAHW,OAAO,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC,gBACjD,OAAO,SAAS,EAAE,QAAQ,gBAYpC;AAED;;;;;;GAMG;AACH,0CAJW,OAAO,YAAY,EAAE,eAAe,CAAC,WAAW,CAAC,6BACjD,OAAO,sCAAsC,EAAE,QAAQ,gBACvD,OAAO,SAAS,EAAE,QAAQ,oBAwBpC;AAED;;;;;;;;;;;;;GAaG;AACH,sCAJW,MAAM,SACN,MAAM,GACJ,OAAO,CAKnB;AAED;;;;;;;;;;;;;GAaG;AACH,8CAJW,MAAM,GAEJ,IAAI,CAuChB;AAED;;;;;;;;;;;;;GAaG;AACH,mCAXW,OAAO,GAEL,IAAI,CAehB;AACD;;;;GAIG;AACH,iCAFW,OAAO,QASjB;AAED;;;;;;GAMG;AACH,iCAJW,OAAO,GAEL,IAAI,CAWhB;AAED;;;;;GAKG;AACH,sCAHW,OAAO,kBAAkB,EAAE,GAAG,gBAC9B,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,iBAQxC;AAED;;;;;;GAMG;AACH,uCAHG;IAAgC,GAAG;IACV,GAAG;CAAC,iBAU/B;AAgED;;GAEG;AACH,qCAFW,OAAO,QASjB;AAED;;;;;;GAMG;AACH,gCAJW,OAAO,GACL,KAAK,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAKjD;AAnFD,iJAeI;AACJ,4KAAyD;AAEzD,oGAGE;AAEF;;GAEG;AAEH;;GAEG;AACH,mKA4BI;AAEJ,kLAA8C;uBApCjC,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC;iCApVP,YAAY;oBARtB,kBAAkB;oBADlB,SAAS;kBAGX,QAAQ"}
package/license ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) Hugo Dias <hugomrdias@gmail.com> (hugodias.me)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iso-ucan",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "description": "Isomorphic UCAN",
6
6
  "author": "Hugo Dias <hugomrdias@gmail.com> (hugodias.me)",
7
7
  "license": "MIT",
@@ -95,24 +95,24 @@
95
95
  "dist/src/**/*.d.ts.map"
96
96
  ],
97
97
  "dependencies": {
98
- "@ipld/dag-cbor": "^9.2.5",
98
+ "@ipld/dag-cbor": "^10.0.2",
99
99
  "@standard-schema/spec": "^1.1.0",
100
100
  "@standard-schema/utils": "^0.3.0",
101
101
  "iobuffer": "^6.0.1",
102
- "iso-kv": "^3.1.1",
103
- "iso-web": "^2.1.0",
104
- "it-merge": "^3.0.12",
105
- "multiformats": "^13.4.1",
106
- "zod": "^4.3.6",
107
- "iso-base": "^4.3.1",
108
- "iso-did": "^2.1.1",
109
- "iso-signatures": "^0.5.1"
102
+ "iso-base": "^4.4.0",
103
+ "iso-did": "^3.0.0",
104
+ "iso-kv": "^3.2.0",
105
+ "iso-signatures": "^0.5.2",
106
+ "iso-web": "^4.0.0",
107
+ "it-merge": "^3.0.15",
108
+ "multiformats": "^14.0.5",
109
+ "zod": "^4.6.5"
110
110
  },
111
111
  "devDependencies": {
112
- "@types/node": "^25.6.0",
112
+ "@types/node": "^26.6.2",
113
113
  "delay": "^7.0.0",
114
- "playwright-test": "^14.1.13",
115
- "typescript": "5.9.3"
114
+ "playwright-test": "^15.0.1",
115
+ "typescript": "^6.0.3"
116
116
  },
117
117
  "publishConfig": {
118
118
  "provenance": true
package/readme.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # iso-ucan [![NPM Version](https://img.shields.io/npm/v/iso-ucan.svg)](https://www.npmjs.com/package/iso-ucan) [![License](https://img.shields.io/npm/l/iso-ucan.svg)](https://github.com/hugomrdias/iso-repo/blob/main/license) [![iso-ucan](https://github.com/hugomrdias/iso-repo/actions/workflows/iso-ucan.yml/badge.svg)](https://github.com/hugomrdias/iso-repo/actions/workflows/iso-ucan.yml)
2
2
 
3
- > Isomorphic UCAN
3
+ > Isomorphic UCAN primitives for delegations, invocations, proof storage, and
4
+ > typed RPC.
5
+
6
+ ## Overview
7
+
8
+ UCAN is a local-first authorization model where permissions are
9
+ cryptographically signed, delegated, and verified without relying on a central
10
+ authority. See [ucan.xyz](https://ucan.xyz/) for more background.
11
+
12
+ `iso-ucan` provides:
13
+
14
+ - Typed capabilities backed by schema validation.
15
+ - Delegation and invocation builders for UCAN authorization flows.
16
+ - Proof storage with pluggable `iso-kv` drivers.
17
+ - Signer and verifier support through `iso-signatures`.
18
+ - EIP-191 wallet signing support through `iso-signatures` `EIP191Signer`.
19
+ - Filsnap-powered UCAN signature insights in MetaMask signature popups.
20
+ - A typed RPC layer for request/response APIs built on UCAN invocations.
4
21
 
5
22
  ## Install
6
23
 
@@ -43,39 +60,59 @@ const invoker = await EdDSASigner.generate()
43
60
  const nowInSeconds = Math.floor(Date.now() / 1000)
44
61
 
45
62
  const ownerDelegation = await AccountCap.delegate({
46
- iss: owner,
47
- aud: bob,
48
- sub: owner,
49
- pol: [],
50
- exp: nowInSeconds + 1000,
63
+ iss: owner,
64
+ aud: bob,
65
+ sub: owner,
66
+ pol: [],
67
+ exp: nowInSeconds + 1000,
51
68
  })
52
69
 
53
70
  await store.set(ownerDelegation)
54
71
 
55
72
  const bobDelegation = await AccountCap.delegate({
56
- iss: bob,
57
- aud: invoker,
58
- sub: owner,
59
- pol: [],
60
- exp: nowInSeconds + 1000,
73
+ iss: bob,
74
+ aud: invoker,
75
+ sub: owner,
76
+ pol: [],
77
+ exp: nowInSeconds + 1000,
61
78
  })
62
79
 
63
80
  await store.set(bobDelegation)
64
81
 
65
82
  const invocation = await AccountCreateCap.invoke({
66
- iss: invoker,
67
- sub: owner,
68
- args: {
83
+ iss: invoker,
84
+ sub: owner,
85
+ args: {
69
86
  type: 'account',
70
87
  properties: {
71
- name: 'John Doe',
88
+ name: 'John Doe',
72
89
  },
73
- },
74
- store,
75
- exp: nowInSeconds + 1000,
90
+ },
91
+ store,
92
+ exp: nowInSeconds + 1000,
76
93
  })
77
94
  ```
78
95
 
96
+ ## EIP-191 wallet signing
97
+
98
+ `iso-ucan` can use EIP-191 wallet signatures through `EIP191Signer` from
99
+ `iso-signatures`. That lets UCAN delegations and invocations be issued by an
100
+ Ethereum account exposed through an EIP-1193 provider, such as an injected
101
+ MetaMask wallet, while verification is handled by the matching EIP-191 verifier
102
+ from `iso-signatures`.
103
+
104
+ [`examples/eip191`](../../examples/eip191) shows this flow in a browser web
105
+ app. It uses `wagmi` to connect the wallet, wraps the provider in
106
+ `EIP191Signer`, then creates and verifies UCAN delegations and invocations
107
+ directly in the browser.
108
+
109
+ The example uses `filsnap-adapter` to install or enable
110
+ [Filsnap](https://github.com/filecoin-project/filsnap), the Filecoin Wallet
111
+ MetaMask Snap. Filsnap supports UCAN signature insights for EIP-191 signers, so
112
+ when the app asks MetaMask to sign a UCAN delegation, the MetaMask signature
113
+ popup can show a readable summary of the issuer, audience, subject, command,
114
+ expiration, and raw payload.
115
+
79
116
  ## RPC
80
117
 
81
118
  `iso-ucan/rpc` is a small request/response layer on top of UCAN
package/src/invocation.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  assertMeta,
10
10
  assertNonce,
11
11
  cid,
12
+ commandCovers,
12
13
  expOrTtl,
13
14
  verifySignature,
14
15
  } from './utils.js'
@@ -235,9 +236,9 @@ export function assertProofs(payload, proofs) {
235
236
  )
236
237
  }
237
238
 
238
- if (!next.cmd.startsWith(current.cmd)) {
239
+ if (!commandCovers(current.cmd, next.cmd)) {
239
240
  throw new Error(
240
- `UCAN Invocation command mismatch, expected ${current.cmd} to be a broader than ${next.cmd}`
241
+ `UCAN Invocation command mismatch, expected ${current.cmd} to cover ${next.cmd}`
241
242
  )
242
243
  }
243
244
  }
package/src/utils.js CHANGED
@@ -168,6 +168,25 @@ export async function verifySignature(
168
168
  return true
169
169
  }
170
170
 
171
+ /**
172
+ * Check if a command is covered by (equal to or a segment-wise child of) another command.
173
+ *
174
+ * Commands are compared by path segment, not by string prefix: `/crypto` covers
175
+ * `/crypto` and `/crypto/sign` but not `/cryptocurrency`. The command `/` covers
176
+ * every command.
177
+ *
178
+ * Both commands are expected to be valid per {@link assertIsValidCommand}, so
179
+ * `${parent}/` is always a real segment boundary.
180
+ *
181
+ * @param {string} parent - The broader command, e.g. from a delegation.
182
+ * @param {string} child - The command that must be attenuated from `parent`.
183
+ * @returns {boolean}
184
+ */
185
+ export function commandCovers(parent, child) {
186
+ if (parent === '/') return child.startsWith('/')
187
+ return child === parent || child.startsWith(`${parent}/`)
188
+ }
189
+
171
190
  /**
172
191
  * Asserts that a UCAN command string is syntactically valid.
173
192
  * If the command is invalid, it throws a descriptive error.