wire-mesh-core 3.7.1 → 3.8.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.
Files changed (107) hide show
  1. package/dist/adapters/byte-stream-connection.d.cts +2 -2
  2. package/dist/adapters/byte-stream-connection.d.mts +2 -2
  3. package/dist/adapters/frame-codec.d.cts +1 -1
  4. package/dist/adapters/frame-codec.d.mts +1 -1
  5. package/dist/adapters/node-identity.d.cts +2 -2
  6. package/dist/adapters/node-identity.d.mts +2 -2
  7. package/dist/adapters/tcp-transport.d.cts +1 -1
  8. package/dist/adapters/tcp-transport.d.mts +1 -1
  9. package/dist/adapters/threshold-identity.d.cts +2 -2
  10. package/dist/adapters/threshold-identity.d.mts +2 -2
  11. package/dist/adapters/threshold-wasm.d.cts +1 -1
  12. package/dist/adapters/threshold-wasm.d.mts +1 -1
  13. package/dist/adapters/tls-transport.d.cts +1 -1
  14. package/dist/adapters/tls-transport.d.mts +1 -1
  15. package/dist/domain/advert-extension-policy.d.cts +1 -1
  16. package/dist/domain/advert-extension-policy.d.mts +1 -1
  17. package/dist/domain/bulk.d.cts +2 -2
  18. package/dist/domain/bulk.d.mts +2 -2
  19. package/dist/domain/capability-grant.cjs +1 -1
  20. package/dist/domain/capability-grant.d.cts +3 -3
  21. package/dist/domain/capability-grant.d.mts +3 -3
  22. package/dist/domain/capability-grant.mjs +1 -1
  23. package/dist/domain/capability-request.cjs +52 -4
  24. package/dist/domain/capability-request.d.cts +14 -3
  25. package/dist/domain/capability-request.d.mts +14 -3
  26. package/dist/domain/capability-request.mjs +52 -5
  27. package/dist/domain/coordinator-election.d.cts +1 -1
  28. package/dist/domain/coordinator-election.d.mts +1 -1
  29. package/dist/domain/data-sync.d.cts +2 -2
  30. package/dist/domain/data-sync.d.mts +2 -2
  31. package/dist/domain/device-id.d.cts +1 -1
  32. package/dist/domain/device-id.d.mts +1 -1
  33. package/dist/domain/direct-manage-request.d.cts +2 -2
  34. package/dist/domain/direct-manage-request.d.mts +2 -2
  35. package/dist/domain/gossip-expansion.d.cts +1 -1
  36. package/dist/domain/gossip-expansion.d.mts +1 -1
  37. package/dist/domain/grant-candidates.d.cts +1 -1
  38. package/dist/domain/grant-candidates.d.mts +1 -1
  39. package/dist/domain/handshake.d.cts +1 -1
  40. package/dist/domain/handshake.d.mts +1 -1
  41. package/dist/domain/hub-mailbox.d.cts +1 -1
  42. package/dist/domain/hub-mailbox.d.mts +1 -1
  43. package/dist/domain/mesh-session.cjs +1 -1
  44. package/dist/domain/mesh-session.d.cts +3 -3
  45. package/dist/domain/mesh-session.d.mts +3 -3
  46. package/dist/domain/mesh-session.mjs +1 -1
  47. package/dist/domain/notice-board.d.cts +3 -3
  48. package/dist/domain/notice-board.d.mts +3 -3
  49. package/dist/domain/path-trace.d.cts +1 -1
  50. package/dist/domain/path-trace.d.mts +1 -1
  51. package/dist/domain/peer-advert.d.cts +2 -2
  52. package/dist/domain/peer-advert.d.mts +2 -2
  53. package/dist/domain/relay-advert.d.cts +1 -1
  54. package/dist/domain/relay-advert.d.mts +1 -1
  55. package/dist/domain/relay-hub.d.cts +2 -2
  56. package/dist/domain/relay-hub.d.mts +2 -2
  57. package/dist/domain/relay-use-gate.d.cts +2 -2
  58. package/dist/domain/relay-use-gate.d.mts +2 -2
  59. package/dist/domain/revocation-view.cjs +1 -1
  60. package/dist/domain/revocation-view.d.cts +2 -2
  61. package/dist/domain/revocation-view.d.mts +2 -2
  62. package/dist/domain/revocation-view.mjs +1 -1
  63. package/dist/domain/room-rekey.d.cts +3 -3
  64. package/dist/domain/room-rekey.d.mts +3 -3
  65. package/dist/domain/room-token-verification.cjs +1 -1
  66. package/dist/domain/room-token-verification.d.cts +1 -1
  67. package/dist/domain/room-token-verification.d.mts +1 -1
  68. package/dist/domain/room-token-verification.mjs +1 -1
  69. package/dist/domain/room.d.cts +4 -4
  70. package/dist/domain/room.d.mts +4 -4
  71. package/dist/domain/shard-manifest.d.cts +1 -1
  72. package/dist/domain/shard-manifest.d.mts +1 -1
  73. package/dist/domain/threshold-subject.cjs +1 -1
  74. package/dist/domain/threshold-subject.mjs +1 -1
  75. package/dist/domain/tokens.cjs +3 -1
  76. package/dist/domain/tokens.d.cts +2 -2
  77. package/dist/domain/tokens.d.mts +2 -2
  78. package/dist/domain/tokens.mjs +2 -2
  79. package/dist/domain/topology-snapshot.d.cts +1 -1
  80. package/dist/domain/topology-snapshot.d.mts +1 -1
  81. package/dist/domain/topology.d.cts +1 -1
  82. package/dist/domain/topology.d.mts +1 -1
  83. package/dist/domain/webrtc-signaling.cjs +1 -1
  84. package/dist/domain/webrtc-signaling.d.cts +2 -2
  85. package/dist/domain/webrtc-signaling.d.mts +2 -2
  86. package/dist/domain/webrtc-signaling.mjs +1 -1
  87. package/dist/generated/protocol.cjs +4 -1
  88. package/dist/generated/protocol.d.cts +1 -1
  89. package/dist/generated/protocol.d.mts +1 -1
  90. package/dist/generated/protocol.mjs +4 -1
  91. package/dist/{identity-DVCNuFLG.d.cts → identity-DJbr96J3.d.cts} +1 -1
  92. package/dist/{identity-DcaYgjXT.d.mts → identity-ViYdZrIU.d.mts} +1 -1
  93. package/dist/ports/identity.d.cts +1 -1
  94. package/dist/ports/identity.d.mts +1 -1
  95. package/dist/ports/transport.d.cts +1 -1
  96. package/dist/ports/transport.d.mts +1 -1
  97. package/dist/{protocol-DCDae3z4.d.cts → protocol-BgR1e_NI.d.cts} +3 -0
  98. package/dist/{protocol-DCDae3z4.d.mts → protocol-BgR1e_NI.d.mts} +3 -0
  99. package/dist/{room-token-verification-CkGnFuQc.d.mts → room-token-verification-7zD4ib-A.d.mts} +2 -2
  100. package/dist/{room-token-verification-PSv4eQ1x.d.cts → room-token-verification-hFtBDZPO.d.cts} +2 -2
  101. package/dist/{tokens-DqFu7iMX.d.cts → tokens-BMWCnQEF.d.cts} +37 -5
  102. package/dist/{tokens-CMdWiRBb.mjs → tokens-D8JlTJNA.mjs} +175 -17
  103. package/dist/{tokens-CBiSYuzp.d.mts → tokens-DRkm_QOR.d.mts} +37 -5
  104. package/dist/{tokens-DSG3SDr8.cjs → tokens-tPJQ7Vzt.cjs} +186 -16
  105. package/dist/{transport-B0GywEzG.d.mts → transport-DP3hdehk.d.cts} +1 -1
  106. package/dist/{transport-DIXjoQKn.d.cts → transport-Dv_LTCsF.d.mts} +1 -1
  107. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- import { n as Listener, r as Transport, t as Connection } from "../transport-B0GywEzG.mjs";
1
+ import { n as Listener, r as Transport, t as Connection } from "../transport-Dv_LTCsF.mjs";
2
2
  export { Connection, Listener, Transport };
@@ -1564,9 +1564,12 @@ declare const tokenClaimsSchema: z.ZodLazy<z.ZodObject<{
1564
1564
  expires: z.ZodNumber;
1565
1565
  "not-before": z.ZodOptional<z.ZodNumber>;
1566
1566
  parent: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
1567
+ "authorised-by": z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
1567
1568
  "delegations-remaining": z.ZodOptional<z.ZodNumber>;
1568
1569
  "valid-until": z.ZodOptional<z.ZodNumber>;
1569
1570
  conditions: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
1571
+ "grants-capability": z.ZodOptional<z.ZodLazy<z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>]>>>>;
1572
+ "requests-capability": z.ZodOptional<z.ZodLazy<z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>]>>>>;
1570
1573
  }, z.core.$catchall<z.ZodUnknown>>>;
1571
1574
  declare const pingFrameSchema: z.ZodLazy<z.ZodObject<{
1572
1575
  type: z.ZodLiteral<"ping">;
@@ -1564,9 +1564,12 @@ declare const tokenClaimsSchema: z.ZodLazy<z.ZodObject<{
1564
1564
  expires: z.ZodNumber;
1565
1565
  "not-before": z.ZodOptional<z.ZodNumber>;
1566
1566
  parent: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
1567
+ "authorised-by": z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
1567
1568
  "delegations-remaining": z.ZodOptional<z.ZodNumber>;
1568
1569
  "valid-until": z.ZodOptional<z.ZodNumber>;
1569
1570
  conditions: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
1571
+ "grants-capability": z.ZodOptional<z.ZodLazy<z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>]>>>>;
1572
+ "requests-capability": z.ZodOptional<z.ZodLazy<z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>, z.ZodLazy<z.ZodLazy<z.ZodString>>]>>>>;
1570
1573
  }, z.core.$catchall<z.ZodUnknown>>>;
1571
1574
  declare const pingFrameSchema: z.ZodLazy<z.ZodObject<{
1572
1575
  type: z.ZodLiteral<"ping">;
@@ -1,5 +1,5 @@
1
- import { B as IdentityKey, D as DeviceId, en as TokenClaims, p as CapabilityToken } from "./protocol-DCDae3z4.mjs";
2
- import { l as TokenVerdictReason, u as VerifyCapabilityTokenOptions } from "./tokens-CBiSYuzp.mjs";
1
+ import { B as IdentityKey, D as DeviceId, en as TokenClaims, p as CapabilityToken } from "./protocol-BgR1e_NI.mjs";
2
+ import { d as VerifyCapabilityTokenOptions, u as TokenVerdictReason } from "./tokens-DRkm_QOR.mjs";
3
3
  //#region src/domain/room-token-verification.d.ts
4
4
  /** The one capability every core/room verb (room.send/read/leave/members) is gated by, per spec/room.cddl -- room.join/room.invite are deliberately ungated instead and need no token check at all. */
5
5
  declare const ROOM_MEMBER_CAPABILITY = "room:member";
@@ -1,5 +1,5 @@
1
- import { B as IdentityKey, D as DeviceId, en as TokenClaims, p as CapabilityToken } from "./protocol-DCDae3z4.cjs";
2
- import { l as TokenVerdictReason, u as VerifyCapabilityTokenOptions } from "./tokens-DqFu7iMX.cjs";
1
+ import { B as IdentityKey, D as DeviceId, en as TokenClaims, p as CapabilityToken } from "./protocol-BgR1e_NI.cjs";
2
+ import { d as VerifyCapabilityTokenOptions, u as TokenVerdictReason } from "./tokens-BMWCnQEF.cjs";
3
3
  //#region src/domain/room-token-verification.d.ts
4
4
  /** The one capability every core/room verb (room.send/read/leave/members) is gated by, per spec/room.cddl -- room.join/room.invite are deliberately ungated instead and need no token check at all. */
5
5
  declare const ROOM_MEMBER_CAPABILITY = "room:member";
@@ -1,5 +1,5 @@
1
- import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-DCDae3z4.cjs";
2
- import { t as IdentityPort } from "./identity-DVCNuFLG.cjs";
1
+ import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-BgR1e_NI.cjs";
2
+ import { t as IdentityPort } from "./identity-DJbr96J3.cjs";
3
3
  import { t as Clock } from "./clock-DiSx-WKM.cjs";
4
4
  import { z } from "zod";
5
5
  import { JsonValue, PredicateNode, Resolution } from "trilean";
@@ -15,11 +15,23 @@ interface NarrowingCandidate {
15
15
  interface ConditionsContext {
16
16
  readonly clock: Clock;
17
17
  readonly claims: Readonly<TokenClaims>;
18
+ /** The minted child a grant-capability's conditions are being evaluated against (wire-mesh#323 subject mode): present only inside an authorised-by verification walk or a mint-time authorisation check, never when a token's conditions are evaluated against its own claims. */
19
+ readonly subject?: Readonly<GrantSubject>;
20
+ }
21
+ /** The child half of a subject-mode conditions evaluation: the claims of the token a grant-capability authorises, structurally what NarrowingCandidate already is plus the bearer the mint named. Satisfied by a full TokenClaims, so the verifier passes the child's decoded claims directly. */
22
+ interface GrantSubject {
23
+ capability: TokenClaims["capability"];
24
+ scope: TokenClaims["scope"];
25
+ bearer: DeviceId;
26
+ expires: number;
27
+ delegationsRemaining?: number;
18
28
  }
19
29
  /** One delegate system's own handler: given the delegate node's payload and the concrete context it was invoked with, resolves to a boolean-valued Resolution. Handlers never throw for a data-quality problem -- an op that cannot make sense of its own payload/context returns `{found: false}`, which the evaluator turns into a fail-closed `indeterminate`. */
20
30
  type TokenDelegateHandler<TContext> = (payload: JsonValue, context: TContext) => Resolution | Promise<Resolution>;
21
31
  //#endregion
22
32
  //#region src/domain/tokens.d.ts
33
+ /** The capability verb of a grant-capability (wire-mesh#323): authority to mint other capability tokens, the same register manage:revoke already established for authority over tokens. */
34
+ declare const MANAGE_GRANT_CAPABILITY = "manage:grant";
23
35
  /**
24
36
  * The revocation view a verifier consults. Returns every recorded, already-signature-verified revocation-claims for tokenId, across every issuer that has ever submitted one -- unfiltered by the store itself. The actual verifier obligation (an entry counts against a token when its own issuer matches the token's own issuer, OR its optional `authorization` grants delegated revoke authority -- management.cddl) is checked by the caller (verifyTokenChain), not here, since that check needs the target token's own scope plus identity/clock to verify a nested authorization token, none of which a store constructed ahead of time has access to. Implementations ingest gossiped revocation-announce frames via verifyRevocationEntry (which enforces each entry's own signature and self-certification) and key the resulting claims by token-id alone -- a token-id can legitimately carry multiple recorded entries from different issuers.
25
37
  */
@@ -27,6 +39,12 @@ interface RevocationCheck {
27
39
  entriesFor: (tokenId: Uint8Array) => Promise<readonly RevocationClaims[]>;
28
40
  }
29
41
  type TokenVerdictReason = "malformed" | "bad_signature" | "wrong_issuer" | "bearer_mismatch" | "expired" | "not_yet_valid" | "content_expired" | "revoked" | "delegation_exceeds_parent" | "parent_invalid" |
42
+ /** The authorised-by link's own obligations failed: the cited grant-capability did not decode or fully verify, its bearer is not this token's issuer, its capability is not manage:grant, its grants-capability does not match, the scope or expiry widened, the depth bound was exceeded, or its conditions (evaluated against THIS token's claims) did not hold. Collapsed to one reason the same way every narrowing failure already collapses to delegation_exceeds_parent. */
43
+ "authorisation_invalid" |
44
+ /** The parent/authorised-by walk exceeded MAX_CHAIN_DEPTH. */
45
+ "chain_too_deep" |
46
+ /** The walk revisited a payload already on the current path: a cycle two colluding issuers constructed. */
47
+ "chain_cycle" |
30
48
  /** claims.conditions is present but its bstr fails CBOR decode, or decodes to something that is not a JSON array of valid trilean PredicateNodes -- fail-closed per CONVENTIONS.md's verifier-obligations glossary, the same treatment "malformed" already gives an undecodable payload. */
31
49
  "conditions_invalid" |
32
50
  /** claims.conditions decoded and validated, but at least one entry did not evaluate to a definite `true` (indeterminate or false) -- the issuer's own additional restriction was not met. */
@@ -75,7 +93,13 @@ interface VerifyRevocationEntryOptions {
75
93
  * Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified.
76
94
  */
77
95
  declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
78
- type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent";
96
+ type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent" |
97
+ /** parent and authorised-by are mutually exclusive link kinds; a manage:grant capability must be minted through authorised-by only. */
98
+ "links_exclusive" |
99
+ /** The cited grant-capability did not decode, or its own conditions bytes did not. */
100
+ "authorisation_malformed" | "authorisation_parent_forbidden" | "authorisation_bearer_mismatch" | "authorisation_capability_mismatch" | "authorisation_scope_does_not_narrow" | "authorisation_exceeds_authoriser" | "authorisation_delegation_exceeds" |
101
+ /** The authoriser's conditions, evaluated against the candidate being minted, did not all hold (wire-mesh#323 subject mode). */
102
+ "authorisation_conditions_not_satisfied";
79
103
  type MintVerdict = {
80
104
  ok: true;
81
105
  token: CapabilityToken;
@@ -84,13 +108,15 @@ type MintVerdict = {
84
108
  reason: MintRefusalReason;
85
109
  };
86
110
  /**
87
- * A pure query: could deviceId, presenting heldToken as its own delegation authority, successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
111
+ * A pure query: could deviceId, presenting heldToken as its own delegation authority," successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
88
112
  *
89
113
  * Deliberately narrower than a full mint attempt in one respect: this checks only the narrowing rules tokens.cddl's own delegation obligations require (bearer match, expiry, scope, capability, delegations-remaining), the same scope mintCapabilityToken itself checks a *parent* against -- it does not verify heldToken's own signature or revocation status, exactly as mintCapabilityToken never re-verifies its own parent's signature either. A caller that also needs heldToken's cryptographic validity confirmed calls verifyCapabilityToken separately.
90
114
  *
91
115
  * Async as of the predicate-list evaluator (issue #85): checkNarrowing now routes through trilean's own evaluatePredicate, which is asynchronous throughout -- there is no synchronous path through a real evaluator call, so this is a genuine, deliberate breaking change to what was previously a synchronous pure function. No existing consumer of wire-mesh-core calls canGrant (confirmed against agent-comms, the only other repo depending on this package), so there is nothing to migrate.
92
116
  */
93
117
  declare function canGrant(heldToken: CapabilityToken, deviceId: DeviceId, candidate: Readonly<NarrowingCandidate>, now: number): Promise<boolean>;
118
+ /** A pure query: could deviceId, presenting heldGrantCapability as its minting authority, mint a token matching candidate right now (wire-mesh#323) -- canGrant's authorised-by analogue, with the same deliberately-narrower-than-verify caveat canGrant's own doc comment states: the held token's signature and revocation status are not checked here; a caller needing those confirmed calls verifyCapabilityToken separately. The candidate carries a bearer because the authoriser's subject-mode conditions read it (the no-self-grant bar). */
119
+ declare function canGrantVia(heldGrantCapability: CapabilityToken, deviceId: DeviceId, candidate: Readonly<GrantSubject>, now: number): Promise<boolean>;
94
120
  interface MintCapabilityTokenOptions {
95
121
  /** The issuer -- signs the token, and supplies the self-certifying issuer/issuer-key claims. */
96
122
  identity: IdentityPort;
@@ -107,6 +133,12 @@ interface MintCapabilityTokenOptions {
107
133
  parent?: CapabilityToken;
108
134
  /** Additional, issuer-chosen restrictions beyond the five mandatory narrowing checks (issue #85) -- CBOR-encoded into claims.conditions verbatim, evaluated by every verifier via evaluateConditions. Strictly additive: has no bearing on narrowing, which mint enforces separately above regardless of what's given here. Not re-validated against trilean's own schema before encoding -- the TS type already guarantees a well-formed PredicateNode[] at this call site, unlike the bytes a verifier decodes from an untrusted wire token, which always are. */
109
135
  conditions?: PredicateNode[];
136
+ /** The grant-capability authorising this mint (wire-mesh#323): the resulting token carries it as authorised-by, and every link obligation (bearer match, manage:grant verb, grants-capability match, scope and expiry narrowing, depth consumption, and the authoriser's subject-mode conditions against this very candidate) is enforced here, at issuance, rather than left for the far end as a bare authorisation_invalid. Mutually exclusive with parent. */
137
+ authorisedBy?: CapabilityToken;
138
+ /** On a manage:grant token: the single verb it authorises minting. Absent means any verb within scope. */
139
+ grantsCapability?: TokenClaims["capability"];
140
+ /** On a manage:request token (wire-mesh#324): the single verb it authorises requesting. Absent means any verb within scope. */
141
+ requestsCapability?: TokenClaims["capability"];
110
142
  }
111
143
  /**
112
144
  * Mints one capability token: builds token-claims from the given fields, signs it as a COSE_Sign1 under `identity`'s own key, with a protected header matching what the frozen conformance vectors actually encode (`{1: alg, 4: issuer device-id}`, not the empty header a token merely needs to verify against itself).
@@ -123,4 +155,4 @@ interface MintRevocationEntryOptions {
123
155
  /** Mints one revocation-entry (management.cddl): a COSE_Sign1 over revocation-claims, signed the same way mintCapabilityToken signs a token. No narrowing chain to check -- a revocation entry has no parent and cannot fail to be issuable the way a delegated token can, so this returns the entry directly rather than a verdict. */
124
156
  declare function mintRevocationEntry(options: MintRevocationEntryOptions): Promise<RevocationEntry>;
125
157
  //#endregion
126
- export { verifyRevocationEntry as _, RevocationCheck as a, TokenVerdict as c, VerifyRevocationEntryOptions as d, canGrant as f, verifyCapabilityToken as g, sig1ToBeSigned as h, MintVerdict as i, TokenVerdictReason as l, mintRevocationEntry as m, MintRefusalReason as n, RevocationEntryVerdict as o, mintCapabilityToken as p, MintRevocationEntryOptions as r, RevocationEntryVerdictReason as s, MintCapabilityTokenOptions as t, VerifyCapabilityTokenOptions as u };
158
+ export { sig1ToBeSigned as _, MintVerdict as a, ConditionsContext as b, RevocationEntryVerdictReason as c, VerifyCapabilityTokenOptions as d, VerifyRevocationEntryOptions as f, mintRevocationEntry as g, mintCapabilityToken as h, MintRevocationEntryOptions as i, TokenVerdict as l, canGrantVia as m, MintCapabilityTokenOptions as n, RevocationCheck as o, canGrant as p, MintRefusalReason as r, RevocationEntryVerdict as s, MANAGE_GRANT_CAPABILITY as t, TokenVerdictReason as u, verifyCapabilityToken as v, TokenDelegateHandler as x, verifyRevocationEntry as y };
@@ -1,4 +1,5 @@
1
1
  import { capabilityTokenSchema, revocationClaimsSchema, tokenClaimsSchema } from "./generated/protocol.mjs";
2
+ import { bytesToHex, deviceIdToHex } from "./domain/device-id.mjs";
2
3
  import { n as scopeNarrows, t as bytesEqual } from "./token-scope-Z4bmci4M.mjs";
3
4
  import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
4
5
  import { z } from "zod";
@@ -24,6 +25,24 @@ const NARROWING_SYSTEMS = [
24
25
  function isNarrowingSystem(system) {
25
26
  return system === "bearer-is" || system === "expires-at" || system === "scope-narrows" || system === "capability-is" || system === "depth-remaining";
26
27
  }
28
+ /** The two subject-mode delegate systems (wire-mesh#323), registered only when a ConditionsContext carries a subject, and indeterminate (fail-closed) whenever one does not. */
29
+ const GRANTEE_IS = "grantee-is";
30
+ const GRANTED_CAPABILITY_IS = "granted-capability-is";
31
+ function isSubjectSystem(system) {
32
+ return system === "grantee-is" || system === "granted-capability-is";
33
+ }
34
+ const subjectHandlers = {
35
+ [GRANTEE_IS]: (payload, context) => {
36
+ if (context.subject === void 0) return { found: false };
37
+ if (typeof payload !== "string") return { found: false };
38
+ return booleanFound(deviceIdToHex(context.subject.bearer) === payload);
39
+ },
40
+ [GRANTED_CAPABILITY_IS]: (payload, context) => {
41
+ if (context.subject === void 0) return { found: false };
42
+ if (typeof payload !== "string") return { found: false };
43
+ return booleanFound(context.subject.capability === payload);
44
+ }
45
+ };
27
46
  function booleanFound(value) {
28
47
  return {
29
48
  found: true,
@@ -58,15 +77,15 @@ const narrowingHandlers = {
58
77
  return booleanFound(context.candidate.delegationsRemaining !== void 0 && context.candidate.delegationsRemaining < parentRemaining);
59
78
  }
60
79
  };
61
- /** `{kind:"compare", op:"eq", left:{kind:"delegate", system, payload:null}, right:{kind:"booleanLiteral", value:true}}` -- delegate is an ExpressionNode, not itself a PredicateNode (trilean's PredicateNodeSchema has no `delegate` member), so every predicate op is expressed this way: resolve the delegate to a boolean ComputedValue, then compare it against the literal `true`. */
62
- function delegateIsTrue(system) {
80
+ /** `{kind:"compare", op:"eq", left:{kind:"delegate", system, payload}, right:{kind:"booleanLiteral", value:true}}` -- delegate is an ExpressionNode, not itself a PredicateNode (trilean's PredicateNodeSchema has no `delegate` member), so every predicate op is expressed this way: resolve the delegate to a boolean ComputedValue, then compare it against the literal `true`. The narrowing checks pass no payload (every fact they need arrives via context); the subject-mode systems take their comparison value as the payload. */
81
+ function delegateIsTrue(system, payload = null) {
63
82
  return {
64
83
  kind: "compare",
65
84
  op: "eq",
66
85
  left: {
67
86
  kind: "delegate",
68
87
  system,
69
- payload: null
88
+ payload
70
89
  },
71
90
  right: {
72
91
  kind: "booleanLiteral",
@@ -104,16 +123,25 @@ async function evaluateNarrowing(parentClaims, childIssuer, candidate) {
104
123
  /**
105
124
  * Evaluates token-claims' own `conditions` field (issue #85): additional, issuer-chosen predicates strictly additive to the five mandatory narrowing checks above, which are computed structurally (from a delegated token's own claims and its parent's) and never read from this field -- the five built-ins are inherently about a parent/child relationship, so they are not meaningfully reusable inside a bare, parent-less conditions evaluation, unlike a genuine domain-specific condition (a message TTL, #84's future revoke-authorization check) which reasons about the presented token's own claims directly. `extraHandlers` is the registration point for those: none exist yet, so a `conditions` entry naming any system with no registered handler resolves `{found: false}` -- indeterminate, fail-closed. Absent `nodes` (an empty array, the caller's own signal for "no conditions field present") is trivially `{ok: true}` -- no extra restrictions, exactly today's behaviour. Every entry MUST evaluate to a definite `true`; the loop short-circuits on the first indeterminate or false result.
106
125
  */
107
- async function evaluateConditions(nodes, claims, clock, extraHandlers = {}) {
108
- const context = {
126
+ async function evaluateConditions(nodes, claims, clock, extraHandlers = {}, subject) {
127
+ const context = subject === void 0 ? {
109
128
  clock,
110
129
  claims
130
+ } : {
131
+ clock,
132
+ claims,
133
+ subject
111
134
  };
112
135
  const resolvers = {
113
136
  resolveValue: unusedResolveValue,
114
137
  resolveLookup: unusedResolveLookup,
115
138
  resolveCollection: unusedResolveCollection,
116
139
  resolveDelegate: async (system, payload) => {
140
+ if (subject !== void 0 && isSubjectSystem(system)) try {
141
+ return await subjectHandlers[system](payload, context);
142
+ } catch {
143
+ return { found: false };
144
+ }
117
145
  const handler = Object.hasOwn(extraHandlers, system) ? extraHandlers[system] : void 0;
118
146
  if (handler === void 0) return { found: false };
119
147
  try {
@@ -134,6 +162,10 @@ async function evaluateConditions(nodes, claims, clock, extraHandlers = {}) {
134
162
  }
135
163
  //#endregion
136
164
  //#region src/domain/tokens.ts
165
+ /** The capability verb of a grant-capability (wire-mesh#323): authority to mint other capability tokens, the same register manage:revoke already established for authority over tokens. */
166
+ const MANAGE_GRANT_CAPABILITY = "manage:grant";
167
+ /** Both link kinds walk at most this many hops before the verifier gives up, mirroring Rust's own MAX_CHAIN_DEPTH. Two colluding issuers can construct a mutually-bearing parent cycle that would otherwise recurse forever, and the authorised-by link makes the attack cheaper to mount, so TS gains the bound rather than relying on JS recursion limits. */
168
+ const MAX_CHAIN_DEPTH = 64;
137
169
  /** RFC 9052 §4.4 Sig_structure for a COSE_Sign1 with no external AAD: ["Signature1", protected, external_aad, payload]. Exported (not just used internally by verifyTokenChain) so any other self-certifying COSE_Sign1 construction in this codebase -- e.g. threshold-subject.ts's own to-be-signed bytes for a FROST-signed capability-token/revocation-entry/handle-record/room-notice -- reuses this single construction rather than a second hand-rolled one. */
138
170
  function sig1ToBeSigned(protectedHeader, payload) {
139
171
  return encode([
@@ -207,12 +239,22 @@ async function revocationEntryGrantsRevoke(entry, targetClaims, options) {
207
239
  });
208
240
  return authorizationVerdict.ok && authorizationVerdict.claims.capability === "manage:revoke" && scopeNarrows(authorizationVerdict.claims.scope, targetClaims.scope);
209
241
  }
210
- async function verifyTokenChain(token, options) {
242
+ async function verifyTokenChain(token, options, subjectClaims, depth = 0, seen = /* @__PURE__ */ new Set()) {
211
243
  const [protectedHeader, , payload, signature] = token;
212
244
  if (payload === null) return {
213
245
  ok: false,
214
246
  reason: "malformed"
215
247
  };
248
+ if (depth >= MAX_CHAIN_DEPTH) return {
249
+ ok: false,
250
+ reason: "chain_too_deep"
251
+ };
252
+ const payloadHex = bytesToHex(payload);
253
+ if (seen.has(payloadHex)) return {
254
+ ok: false,
255
+ reason: "chain_cycle"
256
+ };
257
+ const deeperSeen = new Set(seen).add(payloadHex);
216
258
  let decodedClaims;
217
259
  try {
218
260
  decodedClaims = decode(payload, cdeDecodeOptions);
@@ -271,15 +313,25 @@ async function verifyTokenChain(token, options) {
271
313
  ok: false,
272
314
  reason: "conditions_invalid"
273
315
  };
274
- if (!(await evaluateConditions(conditionsResult.data, claims, options.clock, options.extraPredicateResolvers)).ok) return {
316
+ if (!(await evaluateConditions(conditionsResult.data, claims, options.clock, options.extraPredicateResolvers, subjectClaims)).ok) return {
275
317
  ok: false,
276
318
  reason: "conditions_not_satisfied"
277
319
  };
278
320
  }
279
- if (claims.parent !== void 0) {
321
+ const parentBytes = claims.parent;
322
+ const authorisationBytes = claims["authorised-by"];
323
+ if (claims.capability === "manage:grant" && parentBytes !== void 0) return {
324
+ ok: false,
325
+ reason: "authorisation_invalid"
326
+ };
327
+ if (parentBytes !== void 0 && authorisationBytes !== void 0) return {
328
+ ok: false,
329
+ reason: "malformed"
330
+ };
331
+ if (parentBytes !== void 0) {
280
332
  let decodedParent;
281
333
  try {
282
- decodedParent = decode(claims.parent, cdeDecodeOptions);
334
+ decodedParent = decode(parentBytes, cdeDecodeOptions);
283
335
  } catch {
284
336
  return {
285
337
  ok: false,
@@ -291,11 +343,17 @@ async function verifyTokenChain(token, options) {
291
343
  ok: false,
292
344
  reason: "parent_invalid"
293
345
  };
294
- const parentVerdict = await verifyTokenChain(parentResult.data, options);
295
- if (!parentVerdict.ok) return {
296
- ok: false,
297
- reason: "parent_invalid"
298
- };
346
+ const parentVerdict = await verifyTokenChain(parentResult.data, options, void 0, depth + 1, deeperSeen);
347
+ if (!parentVerdict.ok) {
348
+ if (parentVerdict.reason === "chain_too_deep" || parentVerdict.reason === "chain_cycle") return {
349
+ ok: false,
350
+ reason: parentVerdict.reason
351
+ };
352
+ return {
353
+ ok: false,
354
+ reason: "parent_invalid"
355
+ };
356
+ }
299
357
  const candidate = {
300
358
  capability: claims.capability,
301
359
  scope: claims.scope,
@@ -314,6 +372,46 @@ async function verifyTokenChain(token, options) {
314
372
  depth: parentVerdict.depth + 1
315
373
  };
316
374
  }
375
+ if (authorisationBytes !== void 0) {
376
+ let decodedAuthoriser;
377
+ try {
378
+ decodedAuthoriser = decode(authorisationBytes, cdeDecodeOptions);
379
+ } catch {
380
+ return {
381
+ ok: false,
382
+ reason: "authorisation_invalid"
383
+ };
384
+ }
385
+ const authoriserResult = capabilityTokenSchema.safeParse(decodedAuthoriser);
386
+ if (!authoriserResult.success) return {
387
+ ok: false,
388
+ reason: "authorisation_invalid"
389
+ };
390
+ const authoriserVerdict = await verifyTokenChain(authoriserResult.data, options, claims, depth + 1, deeperSeen);
391
+ if (!authoriserVerdict.ok) {
392
+ if (authoriserVerdict.reason === "chain_too_deep" || authoriserVerdict.reason === "chain_cycle") return {
393
+ ok: false,
394
+ reason: authoriserVerdict.reason
395
+ };
396
+ return {
397
+ ok: false,
398
+ reason: "authorisation_invalid"
399
+ };
400
+ }
401
+ const authoriser = authoriserVerdict.claims;
402
+ const grantsCapability = authoriser["grants-capability"];
403
+ if (!(bytesEqual(authoriser.bearer, claims.issuer) && authoriser.capability === "manage:grant" && (grantsCapability === void 0 || grantsCapability === claims.capability) && scopeNarrows(authoriser.scope, claims.scope) && claims.expires <= authoriser.expires && (authoriser["delegations-remaining"] === void 0 || claims["delegations-remaining"] !== void 0 && claims["delegations-remaining"] < authoriser["delegations-remaining"]))) return {
404
+ ok: false,
405
+ reason: "authorisation_invalid"
406
+ };
407
+ return {
408
+ ok: true,
409
+ claims,
410
+ rootIssuer: authoriserVerdict.rootIssuer,
411
+ rootIssuerKey: authoriserVerdict.rootIssuerKey,
412
+ depth: authoriserVerdict.depth + 1
413
+ };
414
+ }
317
415
  return {
318
416
  ok: true,
319
417
  claims,
@@ -373,8 +471,30 @@ async function checkNarrowing(parentClaims, granterDeviceId, candidate) {
373
471
  const failedSystem = await evaluateNarrowing(parentClaims, granterDeviceId, candidate);
374
472
  return failedSystem === void 0 ? void 0 : NARROWING_SYSTEM_TO_MINT_REFUSAL[failedSystem];
375
473
  }
474
+ /** The authorised-by link's own arithmetic (wire-mesh#323), the analogue checkNarrowing already is for the parent link: everything tokens.cddl's authorised-by obligations require, checked against a not-yet-minted candidate, shared between mintCapabilityToken and canGrantVia so the two can never drift. The authoriser's conditions are evaluated in subject mode against the candidate, the same evaluation the verifier performs against the minted child. Returns the specific refusal reason, or undefined when the authorisation holds. */
475
+ async function checkAuthorisation(authorisingClaims, minterDeviceId, candidate) {
476
+ if (authorisingClaims.parent !== void 0) return "authorisation_parent_forbidden";
477
+ if (!bytesEqual(authorisingClaims.bearer, minterDeviceId)) return "authorisation_bearer_mismatch";
478
+ const grantsCapability = authorisingClaims["grants-capability"];
479
+ if (authorisingClaims.capability !== "manage:grant" || grantsCapability !== void 0 && grantsCapability !== candidate.capability) return "authorisation_capability_mismatch";
480
+ if (!scopeNarrows(authorisingClaims.scope, candidate.scope)) return "authorisation_scope_does_not_narrow";
481
+ if (candidate.expires > authorisingClaims.expires) return "authorisation_exceeds_authoriser";
482
+ const remaining = authorisingClaims["delegations-remaining"];
483
+ if (remaining !== void 0 && (candidate.delegationsRemaining === void 0 || candidate.delegationsRemaining >= remaining)) return "authorisation_delegation_exceeds";
484
+ if (authorisingClaims.conditions !== void 0) {
485
+ let decodedConditions;
486
+ try {
487
+ decodedConditions = decode(authorisingClaims.conditions, cdeDecodeOptions);
488
+ } catch {
489
+ return "authorisation_malformed";
490
+ }
491
+ const conditionsResult = conditionsListSchema.safeParse(decodedConditions);
492
+ if (!conditionsResult.success) return "authorisation_malformed";
493
+ if (!(await evaluateConditions(conditionsResult.data, authorisingClaims, { now: () => 0 }, {}, candidate)).ok) return "authorisation_conditions_not_satisfied";
494
+ }
495
+ }
376
496
  /**
377
- * A pure query: could deviceId, presenting heldToken as its own delegation authority, successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
497
+ * A pure query: could deviceId, presenting heldToken as its own delegation authority," successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
378
498
  *
379
499
  * Deliberately narrower than a full mint attempt in one respect: this checks only the narrowing rules tokens.cddl's own delegation obligations require (bearer match, expiry, scope, capability, delegations-remaining), the same scope mintCapabilityToken itself checks a *parent* against -- it does not verify heldToken's own signature or revocation status, exactly as mintCapabilityToken never re-verifies its own parent's signature either. A caller that also needs heldToken's cryptographic validity confirmed calls verifyCapabilityToken separately.
380
500
  *
@@ -386,6 +506,13 @@ async function canGrant(heldToken, deviceId, candidate, now) {
386
506
  if (heldClaims === void 0) return false;
387
507
  return await checkNarrowing(heldClaims, deviceId, candidate) === void 0;
388
508
  }
509
+ /** A pure query: could deviceId, presenting heldGrantCapability as its minting authority, mint a token matching candidate right now (wire-mesh#323) -- canGrant's authorised-by analogue, with the same deliberately-narrower-than-verify caveat canGrant's own doc comment states: the held token's signature and revocation status are not checked here; a caller needing those confirmed calls verifyCapabilityToken separately. The candidate carries a bearer because the authoriser's subject-mode conditions read it (the no-self-grant bar). */
510
+ async function canGrantVia(heldGrantCapability, deviceId, candidate, now) {
511
+ if (candidate.expires <= now) return false;
512
+ const heldClaims = decodeTokenClaims(heldGrantCapability);
513
+ if (heldClaims === void 0) return false;
514
+ return await checkAuthorisation(heldClaims, deviceId, candidate) === void 0;
515
+ }
389
516
  /**
390
517
  * Mints one capability token: builds token-claims from the given fields, signs it as a COSE_Sign1 under `identity`'s own key, with a protected header matching what the frozen conformance vectors actually encode (`{1: alg, 4: issuer device-id}`, not the empty header a token merely needs to verify against itself).
391
518
  *
@@ -396,6 +523,14 @@ async function mintCapabilityToken(options) {
396
523
  ok: false,
397
524
  reason: "already_expired"
398
525
  };
526
+ if (options.parent !== void 0 && options.authorisedBy !== void 0) return {
527
+ ok: false,
528
+ reason: "links_exclusive"
529
+ };
530
+ if (options.capability === "manage:grant" && options.parent !== void 0) return {
531
+ ok: false,
532
+ reason: "authorisation_parent_forbidden"
533
+ };
399
534
  let parentBytes;
400
535
  if (options.parent !== void 0) {
401
536
  const parentClaims = decodeTokenClaims(options.parent);
@@ -415,6 +550,26 @@ async function mintCapabilityToken(options) {
415
550
  };
416
551
  parentBytes = encodeBuf(options.parent);
417
552
  }
553
+ let authorisationBytes;
554
+ if (options.authorisedBy !== void 0) {
555
+ const authorisingClaims = decodeTokenClaims(options.authorisedBy);
556
+ if (authorisingClaims === void 0) return {
557
+ ok: false,
558
+ reason: "authorisation_malformed"
559
+ };
560
+ const refusal = await checkAuthorisation(authorisingClaims, options.identity.deviceId, {
561
+ capability: options.capability,
562
+ scope: options.scope,
563
+ bearer: options.bearer,
564
+ expires: options.expires,
565
+ ...options.delegationsRemaining !== void 0 ? { delegationsRemaining: options.delegationsRemaining } : {}
566
+ });
567
+ if (refusal !== void 0) return {
568
+ ok: false,
569
+ reason: refusal
570
+ };
571
+ authorisationBytes = encodeBuf(options.authorisedBy);
572
+ }
418
573
  const payload = encodeBuf({
419
574
  "token-id": options.tokenId,
420
575
  issuer: options.identity.deviceId,
@@ -426,7 +581,10 @@ async function mintCapabilityToken(options) {
426
581
  ...options.notBefore !== void 0 ? { "not-before": options.notBefore } : {},
427
582
  ...parentBytes !== void 0 ? { parent: parentBytes } : {},
428
583
  ...options.delegationsRemaining !== void 0 ? { "delegations-remaining": options.delegationsRemaining } : {},
429
- ...options.conditions !== void 0 ? { conditions: encodeBuf(options.conditions) } : {}
584
+ ...options.conditions !== void 0 ? { conditions: encodeBuf(options.conditions) } : {},
585
+ ...authorisationBytes !== void 0 ? { "authorised-by": authorisationBytes } : {},
586
+ ...options.grantsCapability !== void 0 ? { "grants-capability": options.grantsCapability } : {},
587
+ ...options.requestsCapability !== void 0 ? { "requests-capability": options.requestsCapability } : {}
430
588
  });
431
589
  const protectedHeader = protectedHeaderFor(options.identity);
432
590
  return {
@@ -456,4 +614,4 @@ async function mintRevocationEntry(options) {
456
614
  ];
457
615
  }
458
616
  //#endregion
459
- export { verifyCapabilityToken as a, sig1ToBeSigned as i, mintCapabilityToken as n, verifyRevocationEntry as o, mintRevocationEntry as r, canGrant as t };
617
+ export { mintRevocationEntry as a, verifyRevocationEntry as c, mintCapabilityToken as i, canGrant as n, sig1ToBeSigned as o, canGrantVia as r, verifyCapabilityToken as s, MANAGE_GRANT_CAPABILITY as t };
@@ -1,5 +1,5 @@
1
- import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-DCDae3z4.mjs";
2
- import { t as IdentityPort } from "./identity-DcaYgjXT.mjs";
1
+ import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-BgR1e_NI.mjs";
2
+ import { t as IdentityPort } from "./identity-ViYdZrIU.mjs";
3
3
  import { t as Clock } from "./clock-DiSx-WKM.mjs";
4
4
  import { z } from "zod";
5
5
  import { JsonValue, PredicateNode, Resolution } from "trilean";
@@ -15,11 +15,23 @@ interface NarrowingCandidate {
15
15
  interface ConditionsContext {
16
16
  readonly clock: Clock;
17
17
  readonly claims: Readonly<TokenClaims>;
18
+ /** The minted child a grant-capability's conditions are being evaluated against (wire-mesh#323 subject mode): present only inside an authorised-by verification walk or a mint-time authorisation check, never when a token's conditions are evaluated against its own claims. */
19
+ readonly subject?: Readonly<GrantSubject>;
20
+ }
21
+ /** The child half of a subject-mode conditions evaluation: the claims of the token a grant-capability authorises, structurally what NarrowingCandidate already is plus the bearer the mint named. Satisfied by a full TokenClaims, so the verifier passes the child's decoded claims directly. */
22
+ interface GrantSubject {
23
+ capability: TokenClaims["capability"];
24
+ scope: TokenClaims["scope"];
25
+ bearer: DeviceId;
26
+ expires: number;
27
+ delegationsRemaining?: number;
18
28
  }
19
29
  /** One delegate system's own handler: given the delegate node's payload and the concrete context it was invoked with, resolves to a boolean-valued Resolution. Handlers never throw for a data-quality problem -- an op that cannot make sense of its own payload/context returns `{found: false}`, which the evaluator turns into a fail-closed `indeterminate`. */
20
30
  type TokenDelegateHandler<TContext> = (payload: JsonValue, context: TContext) => Resolution | Promise<Resolution>;
21
31
  //#endregion
22
32
  //#region src/domain/tokens.d.ts
33
+ /** The capability verb of a grant-capability (wire-mesh#323): authority to mint other capability tokens, the same register manage:revoke already established for authority over tokens. */
34
+ declare const MANAGE_GRANT_CAPABILITY = "manage:grant";
23
35
  /**
24
36
  * The revocation view a verifier consults. Returns every recorded, already-signature-verified revocation-claims for tokenId, across every issuer that has ever submitted one -- unfiltered by the store itself. The actual verifier obligation (an entry counts against a token when its own issuer matches the token's own issuer, OR its optional `authorization` grants delegated revoke authority -- management.cddl) is checked by the caller (verifyTokenChain), not here, since that check needs the target token's own scope plus identity/clock to verify a nested authorization token, none of which a store constructed ahead of time has access to. Implementations ingest gossiped revocation-announce frames via verifyRevocationEntry (which enforces each entry's own signature and self-certification) and key the resulting claims by token-id alone -- a token-id can legitimately carry multiple recorded entries from different issuers.
25
37
  */
@@ -27,6 +39,12 @@ interface RevocationCheck {
27
39
  entriesFor: (tokenId: Uint8Array) => Promise<readonly RevocationClaims[]>;
28
40
  }
29
41
  type TokenVerdictReason = "malformed" | "bad_signature" | "wrong_issuer" | "bearer_mismatch" | "expired" | "not_yet_valid" | "content_expired" | "revoked" | "delegation_exceeds_parent" | "parent_invalid" |
42
+ /** The authorised-by link's own obligations failed: the cited grant-capability did not decode or fully verify, its bearer is not this token's issuer, its capability is not manage:grant, its grants-capability does not match, the scope or expiry widened, the depth bound was exceeded, or its conditions (evaluated against THIS token's claims) did not hold. Collapsed to one reason the same way every narrowing failure already collapses to delegation_exceeds_parent. */
43
+ "authorisation_invalid" |
44
+ /** The parent/authorised-by walk exceeded MAX_CHAIN_DEPTH. */
45
+ "chain_too_deep" |
46
+ /** The walk revisited a payload already on the current path: a cycle two colluding issuers constructed. */
47
+ "chain_cycle" |
30
48
  /** claims.conditions is present but its bstr fails CBOR decode, or decodes to something that is not a JSON array of valid trilean PredicateNodes -- fail-closed per CONVENTIONS.md's verifier-obligations glossary, the same treatment "malformed" already gives an undecodable payload. */
31
49
  "conditions_invalid" |
32
50
  /** claims.conditions decoded and validated, but at least one entry did not evaluate to a definite `true` (indeterminate or false) -- the issuer's own additional restriction was not met. */
@@ -75,7 +93,13 @@ interface VerifyRevocationEntryOptions {
75
93
  * Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified.
76
94
  */
77
95
  declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
78
- type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent";
96
+ type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent" |
97
+ /** parent and authorised-by are mutually exclusive link kinds; a manage:grant capability must be minted through authorised-by only. */
98
+ "links_exclusive" |
99
+ /** The cited grant-capability did not decode, or its own conditions bytes did not. */
100
+ "authorisation_malformed" | "authorisation_parent_forbidden" | "authorisation_bearer_mismatch" | "authorisation_capability_mismatch" | "authorisation_scope_does_not_narrow" | "authorisation_exceeds_authoriser" | "authorisation_delegation_exceeds" |
101
+ /** The authoriser's conditions, evaluated against the candidate being minted, did not all hold (wire-mesh#323 subject mode). */
102
+ "authorisation_conditions_not_satisfied";
79
103
  type MintVerdict = {
80
104
  ok: true;
81
105
  token: CapabilityToken;
@@ -84,13 +108,15 @@ type MintVerdict = {
84
108
  reason: MintRefusalReason;
85
109
  };
86
110
  /**
87
- * A pure query: could deviceId, presenting heldToken as its own delegation authority, successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
111
+ * A pure query: could deviceId, presenting heldToken as its own delegation authority," successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
88
112
  *
89
113
  * Deliberately narrower than a full mint attempt in one respect: this checks only the narrowing rules tokens.cddl's own delegation obligations require (bearer match, expiry, scope, capability, delegations-remaining), the same scope mintCapabilityToken itself checks a *parent* against -- it does not verify heldToken's own signature or revocation status, exactly as mintCapabilityToken never re-verifies its own parent's signature either. A caller that also needs heldToken's cryptographic validity confirmed calls verifyCapabilityToken separately.
90
114
  *
91
115
  * Async as of the predicate-list evaluator (issue #85): checkNarrowing now routes through trilean's own evaluatePredicate, which is asynchronous throughout -- there is no synchronous path through a real evaluator call, so this is a genuine, deliberate breaking change to what was previously a synchronous pure function. No existing consumer of wire-mesh-core calls canGrant (confirmed against agent-comms, the only other repo depending on this package), so there is nothing to migrate.
92
116
  */
93
117
  declare function canGrant(heldToken: CapabilityToken, deviceId: DeviceId, candidate: Readonly<NarrowingCandidate>, now: number): Promise<boolean>;
118
+ /** A pure query: could deviceId, presenting heldGrantCapability as its minting authority, mint a token matching candidate right now (wire-mesh#323) -- canGrant's authorised-by analogue, with the same deliberately-narrower-than-verify caveat canGrant's own doc comment states: the held token's signature and revocation status are not checked here; a caller needing those confirmed calls verifyCapabilityToken separately. The candidate carries a bearer because the authoriser's subject-mode conditions read it (the no-self-grant bar). */
119
+ declare function canGrantVia(heldGrantCapability: CapabilityToken, deviceId: DeviceId, candidate: Readonly<GrantSubject>, now: number): Promise<boolean>;
94
120
  interface MintCapabilityTokenOptions {
95
121
  /** The issuer -- signs the token, and supplies the self-certifying issuer/issuer-key claims. */
96
122
  identity: IdentityPort;
@@ -107,6 +133,12 @@ interface MintCapabilityTokenOptions {
107
133
  parent?: CapabilityToken;
108
134
  /** Additional, issuer-chosen restrictions beyond the five mandatory narrowing checks (issue #85) -- CBOR-encoded into claims.conditions verbatim, evaluated by every verifier via evaluateConditions. Strictly additive: has no bearing on narrowing, which mint enforces separately above regardless of what's given here. Not re-validated against trilean's own schema before encoding -- the TS type already guarantees a well-formed PredicateNode[] at this call site, unlike the bytes a verifier decodes from an untrusted wire token, which always are. */
109
135
  conditions?: PredicateNode[];
136
+ /** The grant-capability authorising this mint (wire-mesh#323): the resulting token carries it as authorised-by, and every link obligation (bearer match, manage:grant verb, grants-capability match, scope and expiry narrowing, depth consumption, and the authoriser's subject-mode conditions against this very candidate) is enforced here, at issuance, rather than left for the far end as a bare authorisation_invalid. Mutually exclusive with parent. */
137
+ authorisedBy?: CapabilityToken;
138
+ /** On a manage:grant token: the single verb it authorises minting. Absent means any verb within scope. */
139
+ grantsCapability?: TokenClaims["capability"];
140
+ /** On a manage:request token (wire-mesh#324): the single verb it authorises requesting. Absent means any verb within scope. */
141
+ requestsCapability?: TokenClaims["capability"];
110
142
  }
111
143
  /**
112
144
  * Mints one capability token: builds token-claims from the given fields, signs it as a COSE_Sign1 under `identity`'s own key, with a protected header matching what the frozen conformance vectors actually encode (`{1: alg, 4: issuer device-id}`, not the empty header a token merely needs to verify against itself).
@@ -123,4 +155,4 @@ interface MintRevocationEntryOptions {
123
155
  /** Mints one revocation-entry (management.cddl): a COSE_Sign1 over revocation-claims, signed the same way mintCapabilityToken signs a token. No narrowing chain to check -- a revocation entry has no parent and cannot fail to be issuable the way a delegated token can, so this returns the entry directly rather than a verdict. */
124
156
  declare function mintRevocationEntry(options: MintRevocationEntryOptions): Promise<RevocationEntry>;
125
157
  //#endregion
126
- export { verifyRevocationEntry as _, RevocationCheck as a, TokenVerdict as c, VerifyRevocationEntryOptions as d, canGrant as f, verifyCapabilityToken as g, sig1ToBeSigned as h, MintVerdict as i, TokenVerdictReason as l, mintRevocationEntry as m, MintRefusalReason as n, RevocationEntryVerdict as o, mintCapabilityToken as p, MintRevocationEntryOptions as r, RevocationEntryVerdictReason as s, MintCapabilityTokenOptions as t, VerifyCapabilityTokenOptions as u };
158
+ export { sig1ToBeSigned as _, MintVerdict as a, ConditionsContext as b, RevocationEntryVerdictReason as c, VerifyCapabilityTokenOptions as d, VerifyRevocationEntryOptions as f, mintRevocationEntry as g, mintCapabilityToken as h, MintRevocationEntryOptions as i, TokenVerdict as l, canGrantVia as m, MintCapabilityTokenOptions as n, RevocationCheck as o, canGrant as p, MintRefusalReason as r, RevocationEntryVerdict as s, MANAGE_GRANT_CAPABILITY as t, TokenVerdictReason as u, verifyCapabilityToken as v, TokenDelegateHandler as x, verifyRevocationEntry as y };