Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ts/packages/core/src/domain/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
36 changes: 36 additions & 0 deletions ts/packages/core/test/tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down