From 4e0556650ead55a63e11f3b7f8284e7bcce3fddb Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Sun, 13 Sep 2026 06:32:58 +0100 Subject: [PATCH] docs(core): document root-minting authority as separate from delegationsRemaining delegationsRemaining: 0 correctly forbids a token from being re-delegated (it cannot be used as a parent at all), but that has never bounded the issuer's own separate, ordinary authority to mint additional independent root-level grants (no parent named) for other bearers -- there was simply no comment saying so, which is exactly how ExaDev/agent-comms PR #72 arrived at the correct pattern (each room member's own grant minted as its own independent root-level token) as an implementation-time discovery rather than a documented, intentional design. Adds a regression test locking in the pattern: a second, independent root-level grant for a different bearer succeeds even though an earlier root grant from the same issuer has delegationsRemaining: 0, since it names no parent and so has nothing to be checked against. --- ts/packages/core/src/domain/tokens.ts | 3 ++- ts/packages/core/test/tokens.test.ts | 36 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/ts/packages/core/src/domain/tokens.ts b/ts/packages/core/src/domain/tokens.ts index ca6fdf7..2968376 100644 --- a/ts/packages/core/src/domain/tokens.ts +++ b/ts/packages/core/src/domain/tokens.ts @@ -382,8 +382,9 @@ export interface MintCapabilityTokenOptions { scope: TokenClaims["scope"]; expires: number; notBefore?: number; + /** How many further delegation hops the *resulting* token itself permits below it -- unrelated to, and never a bound on, whether `identity` may mint further tokens of its own at the root level for other bearers. Those are two different facts: a token minted with `delegationsRemaining: 0` genuinely cannot itself be re-delegated (correct -- e.g. a room owner's own self-signed root grant, which should never be handed onward), but that same `0` says nothing about the issuer's own separate, ordinary authority to mint additional independent root-level grants (naming no `parent` at all) for other bearers. Root-level minting for a second bearer is never blocked by any existing token's own `delegationsRemaining`, because it uses no `parent` in the first place -- there is no narrowing check to run. Confirmed live in agent-comms' own room-membership implementation (`ExaDev/agent-comms` PR #72): each member's own join/invite grant is minted as its own independent, parent-less, root-level token precisely because the room owner's `delegationsRemaining: 0` self-grant cannot parent anything -- correct by this same reasoning, not a workaround. */ delegationsRemaining?: number; - /** The issuer's own token, when this is a delegation rather than a root grant. Its claims are checked against every narrowing rule below -- mint refuses rather than producing a token verifyCapabilityToken would reject anyway. */ + /** The issuer's own token, when this is a delegation rather than a root grant. Its claims are checked against every narrowing rule below -- mint refuses rather than producing a token verifyCapabilityToken would reject anyway. Omit entirely for a root-level grant (including a second, independent root-level grant for a different bearer under the same capability/scope this issuer already grants elsewhere) -- there is no bound on how many such root grants an issuer may mint, since none of them narrows any other. */ parent?: CapabilityToken; } diff --git a/ts/packages/core/test/tokens.test.ts b/ts/packages/core/test/tokens.test.ts index a403e4c..2da847f 100644 --- a/ts/packages/core/test/tokens.test.ts +++ b/ts/packages/core/test/tokens.test.ts @@ -1373,6 +1373,42 @@ describe("mintCapabilityToken", () => { }); expect(verdict).toEqual({ ok: false, reason: "delegation_exceeds_parent" }); }); + + it("mints a second, independent root-level grant for a different bearer even though an earlier root grant this issuer minted has delegationsRemaining: 0", async () => { + const firstRoot = await mintCapabilityToken({ + identity: issuer, + clock: fixedClock(now), + tokenId: nextTokenId(), + bearer: bearerIdentity.deviceId, + capability: "room:member", + scope: { kind: "room", path: ROOM_MEMBER_ROOM_PATH }, + expires: now + HOUR_MS, + delegationsRemaining: 0, + }); + expect(firstRoot.ok).toBe(true); + + // No `parent` at all -- a second, independent root-level grant for a different bearer, never checked against firstRoot's own (unrelated) delegationsRemaining. + const secondBearer = await generateEs256Identity(); + const secondRoot = await mintCapabilityToken({ + identity: issuer, + clock: fixedClock(now), + tokenId: nextTokenId(), + bearer: secondBearer.deviceId, + capability: "room:member", + scope: { kind: "room", path: ROOM_MEMBER_ROOM_PATH }, + expires: now + HOUR_MS, + delegationsRemaining: 0, + }); + expect(secondRoot.ok).toBe(true); + if (!secondRoot.ok) return; + + const verified = await verifyCapabilityToken(secondRoot.token, { + identity: issuer, + clock: fixedClock(now), + revocation: neverRevoked, + }); + expect(verified.ok).toBe(true); + }); }); describe("canGrant", () => {