wire-mesh-core 1.20.0 → 1.21.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.
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_generated_protocol = require("../generated/protocol.cjs");
3
- const require_tokens = require("../tokens-Dp9BnUTZ.cjs");
3
+ const require_token_scope = require("../token-scope-CxHXTT3u.cjs");
4
+ const require_tokens = require("../tokens-CiNXdKJ8.cjs");
4
5
  //#region src/domain/capability-grant.ts
5
6
  /**
6
7
  * The generic, capability-agnostic capability-grant primitive (wire-mesh#117, spec/management.cddl) -- the unsolicited push counterpart to capability-request.ts's own ask/response primitive. Where capability-request lifts core/room's room.join shape (a pull: the requester asks, the owner mints and returns a grant on the same response) out of that one domain, capability-grant lifts room.invite's own shape (a push: the owner mints unprompted and delivers the grant in the request itself, since there is no approval round-trip to carry it back) the same way. core/room's own room-client.ts is the reference consumer: it builds sendRoomInvite as a thin wrapper over sendCapabilityGrant below, and wires createRoomRouter's dispatch onto createCapabilityGrantHandler for the receiving side.
@@ -68,7 +69,7 @@ function createCapabilityGrantHandler(options) {
68
69
  });
69
70
  return;
70
71
  }
71
- if (!require_tokens.scopeNarrows(verdict.claims.scope, incoming.scope)) {
72
+ if (!require_token_scope.scopeNarrows(verdict.claims.scope, incoming.scope)) {
72
73
  await incoming.respond({
73
74
  result: "error",
74
75
  code: "scope_mismatch"
@@ -1,5 +1,6 @@
1
1
  import { capabilityGrantSchema } from "../generated/protocol.mjs";
2
- import { i as verifyCapabilityToken, o as scopeNarrows } from "../tokens-B5BUgkba.mjs";
2
+ import { n as scopeNarrows } from "../token-scope-Z4bmci4M.mjs";
3
+ import { i as verifyCapabilityToken } from "../tokens-VicRWTj1.mjs";
3
4
  //#region src/domain/capability-grant.ts
4
5
  /**
5
6
  * The generic, capability-agnostic capability-grant primitive (wire-mesh#117, spec/management.cddl) -- the unsolicited push counterpart to capability-request.ts's own ask/response primitive. Where capability-request lifts core/room's room.join shape (a pull: the requester asks, the owner mints and returns a grant on the same response) out of that one domain, capability-grant lifts room.invite's own shape (a push: the owner mints unprompted and delivers the grant in the request itself, since there is no approval round-trip to carry it back) the same way. core/room's own room-client.ts is the reference consumer: it builds sendRoomInvite as a thin wrapper over sendCapabilityGrant below, and wires createRoomRouter's dispatch onto createCapabilityGrantHandler for the receiving side.
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_generated_protocol = require("../generated/protocol.cjs");
3
- const require_tokens = require("../tokens-Dp9BnUTZ.cjs");
3
+ const require_tokens = require("../tokens-CiNXdKJ8.cjs");
4
4
  //#region src/domain/capability-request.ts
5
5
  /**
6
6
  * The generic, capability-agnostic capability-request / capability-grant-ok primitive (wire-mesh#78, spec/management.cddl). Lifts core/room's own room.join shape -- an ungated ask, held open, a human (or any other domain-supplied decision) accepts or refuses, acceptance mints and returns a token on the same response -- out of that one domain so any capability can reuse it, not just room:member. core/room's own room-client.ts is the reference consumer: it rewires requestToJoin/createRoomRouter's room.join handling onto requestCapability/createCapabilityRequestHandler below, supplying room-specific extension fields (its member list) through this module's own extensions mechanism rather than this module knowing anything about rooms.
@@ -1,5 +1,5 @@
1
1
  import { capabilityGrantOkSchema, capabilityRequestSchema } from "../generated/protocol.mjs";
2
- import { n as mintCapabilityToken } from "../tokens-B5BUgkba.mjs";
2
+ import { n as mintCapabilityToken } from "../tokens-VicRWTj1.mjs";
3
3
  //#region src/domain/capability-request.ts
4
4
  /**
5
5
  * The generic, capability-agnostic capability-request / capability-grant-ok primitive (wire-mesh#78, spec/management.cddl). Lifts core/room's own room.join shape -- an ungated ask, held open, a human (or any other domain-supplied decision) accepts or refuses, acceptance mints and returns a token on the same response -- out of that one domain so any capability can reuse it, not just room:member. core/room's own room-client.ts is the reference consumer: it rewires requestToJoin/createRoomRouter's room.join handling onto requestCapability/createCapabilityRequestHandler below, supplying room-specific extension fields (its member list) through this module's own extensions mechanism rather than this module knowing anything about rooms.
@@ -0,0 +1,47 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_generated_protocol = require("../generated/protocol.cjs");
3
+ const require_token_scope = require("../token-scope-CxHXTT3u.cjs");
4
+ let zod = require("zod");
5
+ //#region src/domain/grant-candidates.ts
6
+ /**
7
+ * Gossip-advertised delegable-grant candidates (wire-mesh#87): lets a bearer of a still-delegable held token advertise its own willingness to field a capability-request for a narrower grant, and lets a requester discover the best candidate it can currently see before falling back to a capability's own structural root (that fallback -- e.g. a room's owner device-id -- is the caller's own concern, entirely outside this module; findGrantCandidate only ever returns a nearer-than-root candidate or undefined). Rides peer-advert's existing open extension tail (spec/transport.cddl, wire-mesh#91) under the domain-qualified key "capability-request/candidates" -- no new CDDL, no new wire mechanism, just a structured value on an already-shipped extension point.
8
+ *
9
+ * Honest limit, not closed here: this is only ever as complete as gossip propagation allows. A request can go unanswered by a nearer delegate not because none exists, but because it hasn't been gossiped yet -- the same "detection with propagation delay" honesty this spec already accepts for kick propagation and revocation timing.
10
+ */
11
+ const GOSSIP_CANDIDATES_KEY = "capability-request/candidates";
12
+ const grantCandidateSchema = zod.z.object({
13
+ capability: zod.z.string(),
14
+ scope: require_generated_protocol.capabilityScopeSchema,
15
+ "delegations-remaining": zod.z.number().int().nonnegative().optional()
16
+ });
17
+ const grantCandidateListSchema = zod.z.array(grantCandidateSchema);
18
+ /**
19
+ * Advertises this session's own willingness to field a capability-request for each of candidates, via sendGossipUpdate's own re-advertisement mechanism. Callers own their own re-advertisement cadence (there is no timer here, matching sendGossipUpdate's own design) -- calling this again with a fresh list replaces what was previously advertised under this key, exactly as sendGossipUpdate's own peer-advert re-send already does for any other extension field.
20
+ */
21
+ async function advertiseGrantCandidates(session, candidates) {
22
+ await session.sendGossipUpdate({ [GOSSIP_CANDIDATES_KEY]: candidates });
23
+ }
24
+ /**
25
+ * Scans directory for the best currently-gossiped delegate that could field a capability-request for capability over scope, returning its device-id -- or undefined when nothing closer than the capability's own structural root has been gossiped. A candidate qualifies when its own advertised capability matches exactly and its own advertised scope narrows into the requested scope (scopeNarrows' existing narrowing relation: the candidate's scope is the same as or a broader ancestor of scope), and its own delegations-remaining is either absent (unbounded) or still greater than zero.
26
+ *
27
+ * Selection policy, deliberately simple and explicitly stated rather than left implicit: the first qualifying candidate in directory's own order wins. Gossip carries no distance, latency, or load signal a requester could meaningfully rank candidates by, so no ordering directory provides is more defensible than any other -- "first seen" is exactly as reasonable a tie-break as "highest delegations-remaining" and simplest to implement and reason about.
28
+ *
29
+ * A malformed or non-conforming candidates value (from a misbehaving or out-of-date peer) is silently skipped, not treated as an error: this is a discovery hint over self-asserted, unsigned gossip, not a security check -- the token minted from any candidate this function returns still goes through mintCapabilityToken's own full narrowing checks regardless.
30
+ */
31
+ function findGrantCandidate(directory, capability, scope) {
32
+ for (const entry of directory) {
33
+ const raw = entry.advert[GOSSIP_CANDIDATES_KEY];
34
+ if (raw === void 0) continue;
35
+ const parsed = grantCandidateListSchema.safeParse(raw);
36
+ if (!parsed.success) continue;
37
+ for (const candidate of parsed.data) {
38
+ if (candidate.capability !== capability) continue;
39
+ const remaining = candidate["delegations-remaining"];
40
+ if (remaining !== void 0 && remaining <= 0) continue;
41
+ if (require_token_scope.scopeNarrows(candidate.scope, scope)) return entry.device;
42
+ }
43
+ }
44
+ }
45
+ //#endregion
46
+ exports.advertiseGrantCandidates = advertiseGrantCandidates;
47
+ exports.findGrantCandidate = findGrantCandidate;
@@ -0,0 +1,26 @@
1
+ import { o as CapabilityScope, x as DeviceId } from "../protocol-l9RMR3s5.cjs";
2
+ import { DirectoryEntry, MeshSession } from "./mesh-session.cjs";
3
+ import { z } from "zod";
4
+ //#region src/domain/grant-candidates.d.ts
5
+ declare const grantCandidateSchema: z.ZodObject<{
6
+ capability: z.ZodString;
7
+ scope: z.ZodLazy<z.ZodObject<{
8
+ kind: z.ZodString;
9
+ path: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strip>>;
11
+ "delegations-remaining": z.ZodOptional<z.ZodNumber>;
12
+ }, z.core.$strip>;
13
+ export type GrantCandidate = z.infer<typeof grantCandidateSchema>;
14
+ /**
15
+ * Advertises this session's own willingness to field a capability-request for each of candidates, via sendGossipUpdate's own re-advertisement mechanism. Callers own their own re-advertisement cadence (there is no timer here, matching sendGossipUpdate's own design) -- calling this again with a fresh list replaces what was previously advertised under this key, exactly as sendGossipUpdate's own peer-advert re-send already does for any other extension field.
16
+ */
17
+ export declare function advertiseGrantCandidates(session: Readonly<MeshSession>, candidates: readonly GrantCandidate[]): Promise<void>;
18
+ /**
19
+ * Scans directory for the best currently-gossiped delegate that could field a capability-request for capability over scope, returning its device-id -- or undefined when nothing closer than the capability's own structural root has been gossiped. A candidate qualifies when its own advertised capability matches exactly and its own advertised scope narrows into the requested scope (scopeNarrows' existing narrowing relation: the candidate's scope is the same as or a broader ancestor of scope), and its own delegations-remaining is either absent (unbounded) or still greater than zero.
20
+ *
21
+ * Selection policy, deliberately simple and explicitly stated rather than left implicit: the first qualifying candidate in directory's own order wins. Gossip carries no distance, latency, or load signal a requester could meaningfully rank candidates by, so no ordering directory provides is more defensible than any other -- "first seen" is exactly as reasonable a tie-break as "highest delegations-remaining" and simplest to implement and reason about.
22
+ *
23
+ * A malformed or non-conforming candidates value (from a misbehaving or out-of-date peer) is silently skipped, not treated as an error: this is a discovery hint over self-asserted, unsigned gossip, not a security check -- the token minted from any candidate this function returns still goes through mintCapabilityToken's own full narrowing checks regardless.
24
+ */
25
+ export declare function findGrantCandidate(directory: readonly DirectoryEntry[], capability: string, scope: Readonly<CapabilityScope>): DeviceId | undefined;
26
+ //#endregion
@@ -0,0 +1,26 @@
1
+ import { o as CapabilityScope, x as DeviceId } from "../protocol-l9RMR3s5.mjs";
2
+ import { DirectoryEntry, MeshSession } from "./mesh-session.mjs";
3
+ import { z } from "zod";
4
+ //#region src/domain/grant-candidates.d.ts
5
+ declare const grantCandidateSchema: z.ZodObject<{
6
+ capability: z.ZodString;
7
+ scope: z.ZodLazy<z.ZodObject<{
8
+ kind: z.ZodString;
9
+ path: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strip>>;
11
+ "delegations-remaining": z.ZodOptional<z.ZodNumber>;
12
+ }, z.core.$strip>;
13
+ export type GrantCandidate = z.infer<typeof grantCandidateSchema>;
14
+ /**
15
+ * Advertises this session's own willingness to field a capability-request for each of candidates, via sendGossipUpdate's own re-advertisement mechanism. Callers own their own re-advertisement cadence (there is no timer here, matching sendGossipUpdate's own design) -- calling this again with a fresh list replaces what was previously advertised under this key, exactly as sendGossipUpdate's own peer-advert re-send already does for any other extension field.
16
+ */
17
+ export declare function advertiseGrantCandidates(session: Readonly<MeshSession>, candidates: readonly GrantCandidate[]): Promise<void>;
18
+ /**
19
+ * Scans directory for the best currently-gossiped delegate that could field a capability-request for capability over scope, returning its device-id -- or undefined when nothing closer than the capability's own structural root has been gossiped. A candidate qualifies when its own advertised capability matches exactly and its own advertised scope narrows into the requested scope (scopeNarrows' existing narrowing relation: the candidate's scope is the same as or a broader ancestor of scope), and its own delegations-remaining is either absent (unbounded) or still greater than zero.
20
+ *
21
+ * Selection policy, deliberately simple and explicitly stated rather than left implicit: the first qualifying candidate in directory's own order wins. Gossip carries no distance, latency, or load signal a requester could meaningfully rank candidates by, so no ordering directory provides is more defensible than any other -- "first seen" is exactly as reasonable a tie-break as "highest delegations-remaining" and simplest to implement and reason about.
22
+ *
23
+ * A malformed or non-conforming candidates value (from a misbehaving or out-of-date peer) is silently skipped, not treated as an error: this is a discovery hint over self-asserted, unsigned gossip, not a security check -- the token minted from any candidate this function returns still goes through mintCapabilityToken's own full narrowing checks regardless.
24
+ */
25
+ export declare function findGrantCandidate(directory: readonly DirectoryEntry[], capability: string, scope: Readonly<CapabilityScope>): DeviceId | undefined;
26
+ //#endregion
@@ -0,0 +1,45 @@
1
+ import { capabilityScopeSchema } from "../generated/protocol.mjs";
2
+ import { n as scopeNarrows } from "../token-scope-Z4bmci4M.mjs";
3
+ import { z } from "zod";
4
+ //#region src/domain/grant-candidates.ts
5
+ /**
6
+ * Gossip-advertised delegable-grant candidates (wire-mesh#87): lets a bearer of a still-delegable held token advertise its own willingness to field a capability-request for a narrower grant, and lets a requester discover the best candidate it can currently see before falling back to a capability's own structural root (that fallback -- e.g. a room's owner device-id -- is the caller's own concern, entirely outside this module; findGrantCandidate only ever returns a nearer-than-root candidate or undefined). Rides peer-advert's existing open extension tail (spec/transport.cddl, wire-mesh#91) under the domain-qualified key "capability-request/candidates" -- no new CDDL, no new wire mechanism, just a structured value on an already-shipped extension point.
7
+ *
8
+ * Honest limit, not closed here: this is only ever as complete as gossip propagation allows. A request can go unanswered by a nearer delegate not because none exists, but because it hasn't been gossiped yet -- the same "detection with propagation delay" honesty this spec already accepts for kick propagation and revocation timing.
9
+ */
10
+ const GOSSIP_CANDIDATES_KEY = "capability-request/candidates";
11
+ const grantCandidateSchema = z.object({
12
+ capability: z.string(),
13
+ scope: capabilityScopeSchema,
14
+ "delegations-remaining": z.number().int().nonnegative().optional()
15
+ });
16
+ const grantCandidateListSchema = z.array(grantCandidateSchema);
17
+ /**
18
+ * Advertises this session's own willingness to field a capability-request for each of candidates, via sendGossipUpdate's own re-advertisement mechanism. Callers own their own re-advertisement cadence (there is no timer here, matching sendGossipUpdate's own design) -- calling this again with a fresh list replaces what was previously advertised under this key, exactly as sendGossipUpdate's own peer-advert re-send already does for any other extension field.
19
+ */
20
+ async function advertiseGrantCandidates(session, candidates) {
21
+ await session.sendGossipUpdate({ [GOSSIP_CANDIDATES_KEY]: candidates });
22
+ }
23
+ /**
24
+ * Scans directory for the best currently-gossiped delegate that could field a capability-request for capability over scope, returning its device-id -- or undefined when nothing closer than the capability's own structural root has been gossiped. A candidate qualifies when its own advertised capability matches exactly and its own advertised scope narrows into the requested scope (scopeNarrows' existing narrowing relation: the candidate's scope is the same as or a broader ancestor of scope), and its own delegations-remaining is either absent (unbounded) or still greater than zero.
25
+ *
26
+ * Selection policy, deliberately simple and explicitly stated rather than left implicit: the first qualifying candidate in directory's own order wins. Gossip carries no distance, latency, or load signal a requester could meaningfully rank candidates by, so no ordering directory provides is more defensible than any other -- "first seen" is exactly as reasonable a tie-break as "highest delegations-remaining" and simplest to implement and reason about.
27
+ *
28
+ * A malformed or non-conforming candidates value (from a misbehaving or out-of-date peer) is silently skipped, not treated as an error: this is a discovery hint over self-asserted, unsigned gossip, not a security check -- the token minted from any candidate this function returns still goes through mintCapabilityToken's own full narrowing checks regardless.
29
+ */
30
+ function findGrantCandidate(directory, capability, scope) {
31
+ for (const entry of directory) {
32
+ const raw = entry.advert[GOSSIP_CANDIDATES_KEY];
33
+ if (raw === void 0) continue;
34
+ const parsed = grantCandidateListSchema.safeParse(raw);
35
+ if (!parsed.success) continue;
36
+ for (const candidate of parsed.data) {
37
+ if (candidate.capability !== capability) continue;
38
+ const remaining = candidate["delegations-remaining"];
39
+ if (remaining !== void 0 && remaining <= 0) continue;
40
+ if (scopeNarrows(candidate.scope, scope)) return entry.device;
41
+ }
42
+ }
43
+ }
44
+ //#endregion
45
+ export { advertiseGrantCandidates, findGrantCandidate };
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_tokens = require("../tokens-Dp9BnUTZ.cjs");
2
+ const require_tokens = require("../tokens-CiNXdKJ8.cjs");
3
3
  const require_domain_device_id = require("./device-id.cjs");
4
4
  //#region src/domain/revocation-view.ts
5
5
  function revocationKey(tokenId) {
@@ -1,4 +1,4 @@
1
- import { a as verifyRevocationEntry } from "../tokens-B5BUgkba.mjs";
1
+ import { a as verifyRevocationEntry } from "../tokens-VicRWTj1.mjs";
2
2
  import { bytesToHex } from "./device-id.mjs";
3
3
  //#region src/domain/revocation-view.ts
4
4
  function revocationKey(tokenId) {
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_tokens = require("../tokens-Dp9BnUTZ.cjs");
2
+ const require_tokens = require("../tokens-CiNXdKJ8.cjs");
3
3
  const require_domain_device_id = require("./device-id.cjs");
4
4
  const require_domain_room_path = require("./room-path.cjs");
5
5
  //#region src/domain/room-token-verification.ts
@@ -1,4 +1,4 @@
1
- import { i as verifyCapabilityToken } from "../tokens-B5BUgkba.mjs";
1
+ import { i as verifyCapabilityToken } from "../tokens-VicRWTj1.mjs";
2
2
  import { deviceIdToHex } from "./device-id.mjs";
3
3
  import { parseRoomPath } from "./room-path.mjs";
4
4
  //#region src/domain/room-token-verification.ts
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_tokens = require("../tokens-Dp9BnUTZ.cjs");
2
+ const require_tokens = require("../tokens-CiNXdKJ8.cjs");
3
3
  exports.canGrant = require_tokens.canGrant;
4
4
  exports.mintCapabilityToken = require_tokens.mintCapabilityToken;
5
5
  exports.mintRevocationEntry = require_tokens.mintRevocationEntry;
@@ -1,2 +1,2 @@
1
- import { a as verifyRevocationEntry, i as verifyCapabilityToken, n as mintCapabilityToken, r as mintRevocationEntry, t as canGrant } from "../tokens-B5BUgkba.mjs";
1
+ import { a as verifyRevocationEntry, i as verifyCapabilityToken, n as mintCapabilityToken, r as mintRevocationEntry, t as canGrant } from "../tokens-VicRWTj1.mjs";
2
2
  export { canGrant, mintCapabilityToken, mintRevocationEntry, verifyCapabilityToken, verifyRevocationEntry };
@@ -0,0 +1,43 @@
1
+ //#region src/domain/token-scope.ts
2
+ /**
3
+ * Extracted out of tokens.ts specifically so token-predicates.ts (the generic evaluator's own delegate handlers) and tokens.ts (the chain walk, and its re-export for capability-grant.ts) can both depend on this pure, no-domain-imports module without creating an import cycle between the two -- tokens.ts imports evaluateNarrowing from token-predicates.ts, and token-predicates.ts's own scope-narrows/bearer-is handlers need these exact comparisons, so neither of those two files may import the other.
4
+ */
5
+ function bytesEqual(a, b) {
6
+ if (a.length !== b.length) return false;
7
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
8
+ return true;
9
+ }
10
+ /** True when the path contains a "." or ".." segment. Purely lexical prefix comparison would let "/work/../org" pass under "/work" -- a path that normalises outside the parent -- so any relative segment fails the narrowing comparison wholesale: fail-closed rather than reimplementing path normalisation, consistent with how empty, case-different, and non-boundary-prefixed paths already behave. */
11
+ function hasRelativeSegment(path) {
12
+ return path.split("/").some((segment) => segment === "." || segment === "..");
13
+ }
14
+ /** True when childPath is parentPath or a descendant of it, compared on "/"-segment boundaries: "/work/sub" narrows "/work", but "/workbook" does NOT narrow "/work" despite the string prefix, because "book" continues the same segment. Paths containing "." or ".." segments never narrow anything (see hasRelativeSegment). */
15
+ function pathNarrows(childPath, parentPath) {
16
+ if (hasRelativeSegment(childPath) || hasRelativeSegment(parentPath)) return false;
17
+ if (childPath === parentPath) return true;
18
+ if (!childPath.startsWith(parentPath)) return false;
19
+ if (parentPath.endsWith("/")) return true;
20
+ return childPath.charAt(parentPath.length) === "/";
21
+ }
22
+ /**
23
+ * True when childScope narrows parentScope per tokens.cddl ("each hop can only narrow authority, never widen it"): the kind must be identical (a different kind is a different kind of authority, not a narrower one), and a parent with a path requires the child to carry an equal-or-descendant path -- an absent child path means the kind's whole-scope root, which is wider than any path-narrowed parent. A parent with no path (whole-scope root) lets any child path under the same kind through. Exported for capability-grant.ts's own obligation 4 (an unsolicited push's embedded token must equal-or-root the enclosing request's own scope), which is exactly this same narrowing relation applied outside a delegation chain, and for token-predicates.ts's own `scope-narrows` delegate handler.
24
+ */
25
+ function scopeNarrows(parent, child) {
26
+ if (parent.kind !== child.kind) return false;
27
+ if (parent.path === void 0) return true;
28
+ if (child.path === void 0) return false;
29
+ return pathNarrows(child.path, parent.path);
30
+ }
31
+ //#endregion
32
+ Object.defineProperty(exports, "bytesEqual", {
33
+ enumerable: true,
34
+ get: function() {
35
+ return bytesEqual;
36
+ }
37
+ });
38
+ Object.defineProperty(exports, "scopeNarrows", {
39
+ enumerable: true,
40
+ get: function() {
41
+ return scopeNarrows;
42
+ }
43
+ });
@@ -0,0 +1,32 @@
1
+ //#region src/domain/token-scope.ts
2
+ /**
3
+ * Extracted out of tokens.ts specifically so token-predicates.ts (the generic evaluator's own delegate handlers) and tokens.ts (the chain walk, and its re-export for capability-grant.ts) can both depend on this pure, no-domain-imports module without creating an import cycle between the two -- tokens.ts imports evaluateNarrowing from token-predicates.ts, and token-predicates.ts's own scope-narrows/bearer-is handlers need these exact comparisons, so neither of those two files may import the other.
4
+ */
5
+ function bytesEqual(a, b) {
6
+ if (a.length !== b.length) return false;
7
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
8
+ return true;
9
+ }
10
+ /** True when the path contains a "." or ".." segment. Purely lexical prefix comparison would let "/work/../org" pass under "/work" -- a path that normalises outside the parent -- so any relative segment fails the narrowing comparison wholesale: fail-closed rather than reimplementing path normalisation, consistent with how empty, case-different, and non-boundary-prefixed paths already behave. */
11
+ function hasRelativeSegment(path) {
12
+ return path.split("/").some((segment) => segment === "." || segment === "..");
13
+ }
14
+ /** True when childPath is parentPath or a descendant of it, compared on "/"-segment boundaries: "/work/sub" narrows "/work", but "/workbook" does NOT narrow "/work" despite the string prefix, because "book" continues the same segment. Paths containing "." or ".." segments never narrow anything (see hasRelativeSegment). */
15
+ function pathNarrows(childPath, parentPath) {
16
+ if (hasRelativeSegment(childPath) || hasRelativeSegment(parentPath)) return false;
17
+ if (childPath === parentPath) return true;
18
+ if (!childPath.startsWith(parentPath)) return false;
19
+ if (parentPath.endsWith("/")) return true;
20
+ return childPath.charAt(parentPath.length) === "/";
21
+ }
22
+ /**
23
+ * True when childScope narrows parentScope per tokens.cddl ("each hop can only narrow authority, never widen it"): the kind must be identical (a different kind is a different kind of authority, not a narrower one), and a parent with a path requires the child to carry an equal-or-descendant path -- an absent child path means the kind's whole-scope root, which is wider than any path-narrowed parent. A parent with no path (whole-scope root) lets any child path under the same kind through. Exported for capability-grant.ts's own obligation 4 (an unsolicited push's embedded token must equal-or-root the enclosing request's own scope), which is exactly this same narrowing relation applied outside a delegation chain, and for token-predicates.ts's own `scope-narrows` delegate handler.
24
+ */
25
+ function scopeNarrows(parent, child) {
26
+ if (parent.kind !== child.kind) return false;
27
+ if (parent.path === void 0) return true;
28
+ if (child.path === void 0) return false;
29
+ return pathNarrows(child.path, parent.path);
30
+ }
31
+ //#endregion
32
+ export { scopeNarrows as n, bytesEqual as t };
@@ -1,38 +1,8 @@
1
1
  const require_generated_protocol = require("./generated/protocol.cjs");
2
+ const require_token_scope = require("./token-scope-CxHXTT3u.cjs");
2
3
  let zod = require("zod");
3
4
  let cbor2 = require("cbor2");
4
5
  let trilean = require("trilean");
5
- //#region src/domain/token-scope.ts
6
- /**
7
- * Extracted out of tokens.ts specifically so token-predicates.ts (the generic evaluator's own delegate handlers) and tokens.ts (the chain walk, and its re-export for capability-grant.ts) can both depend on this pure, no-domain-imports module without creating an import cycle between the two -- tokens.ts imports evaluateNarrowing from token-predicates.ts, and token-predicates.ts's own scope-narrows/bearer-is handlers need these exact comparisons, so neither of those two files may import the other.
8
- */
9
- function bytesEqual(a, b) {
10
- if (a.length !== b.length) return false;
11
- for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
12
- return true;
13
- }
14
- /** True when the path contains a "." or ".." segment. Purely lexical prefix comparison would let "/work/../org" pass under "/work" -- a path that normalises outside the parent -- so any relative segment fails the narrowing comparison wholesale: fail-closed rather than reimplementing path normalisation, consistent with how empty, case-different, and non-boundary-prefixed paths already behave. */
15
- function hasRelativeSegment(path) {
16
- return path.split("/").some((segment) => segment === "." || segment === "..");
17
- }
18
- /** True when childPath is parentPath or a descendant of it, compared on "/"-segment boundaries: "/work/sub" narrows "/work", but "/workbook" does NOT narrow "/work" despite the string prefix, because "book" continues the same segment. Paths containing "." or ".." segments never narrow anything (see hasRelativeSegment). */
19
- function pathNarrows(childPath, parentPath) {
20
- if (hasRelativeSegment(childPath) || hasRelativeSegment(parentPath)) return false;
21
- if (childPath === parentPath) return true;
22
- if (!childPath.startsWith(parentPath)) return false;
23
- if (parentPath.endsWith("/")) return true;
24
- return childPath.charAt(parentPath.length) === "/";
25
- }
26
- /**
27
- * True when childScope narrows parentScope per tokens.cddl ("each hop can only narrow authority, never widen it"): the kind must be identical (a different kind is a different kind of authority, not a narrower one), and a parent with a path requires the child to carry an equal-or-descendant path -- an absent child path means the kind's whole-scope root, which is wider than any path-narrowed parent. A parent with no path (whole-scope root) lets any child path under the same kind through. Exported for capability-grant.ts's own obligation 4 (an unsolicited push's embedded token must equal-or-root the enclosing request's own scope), which is exactly this same narrowing relation applied outside a delegation chain, and for token-predicates.ts's own `scope-narrows` delegate handler.
28
- */
29
- function scopeNarrows(parent, child) {
30
- if (parent.kind !== child.kind) return false;
31
- if (parent.path === void 0) return true;
32
- if (child.path === void 0) return false;
33
- return pathNarrows(child.path, parent.path);
34
- }
35
- //#endregion
36
6
  //#region src/domain/token-predicates.ts
37
7
  /** The wire shape of `token-claims.conditions` once CBOR-decoded: trilean's own PredicateNodeSchema is the single source of truth for what a condition entry may contain, re-validated here rather than trusted from a CDDL-generated shadow schema (see tokens.cddl's own comment on why `conditions` is an opaque bstr, not a native CDDL type) -- a token from an untrusted peer must pass trilean's real schema before any of its conditions are evaluated. Explicitly annotated: trilean's PredicateNodeSchema is a deeply recursive z.lazy() type whose inferred shape is too large for tsdown's declaration-file generator to serialise (TS7056) without this. */
38
8
  const conditionsListSchema = zod.z.array(trilean.PredicateNodeSchema);
@@ -67,7 +37,7 @@ function booleanFound(value) {
67
37
  const narrowingHandlers = {
68
38
  [BEARER_IS]: (_payload, context) => {
69
39
  if (context.parentClaims === void 0) return booleanFound(false);
70
- return booleanFound(bytesEqual(context.parentClaims.bearer, context.childIssuer));
40
+ return booleanFound(require_token_scope.bytesEqual(context.parentClaims.bearer, context.childIssuer));
71
41
  },
72
42
  [EXPIRES_AT]: (_payload, context) => {
73
43
  if (context.parentClaims === void 0) return booleanFound(false);
@@ -75,7 +45,7 @@ const narrowingHandlers = {
75
45
  },
76
46
  [SCOPE_NARROWS]: (_payload, context) => {
77
47
  if (context.parentClaims === void 0) return booleanFound(false);
78
- return booleanFound(scopeNarrows(context.parentClaims.scope, context.candidate.scope));
48
+ return booleanFound(require_token_scope.scopeNarrows(context.parentClaims.scope, context.candidate.scope));
79
49
  },
80
50
  [CAPABILITY_IS]: (_payload, context) => {
81
51
  if (context.parentClaims === void 0) return booleanFound(false);
@@ -211,7 +181,7 @@ async function verifyCapabilityToken(token, options) {
211
181
  ...options.extraPredicateResolvers !== void 0 ? { extraPredicateResolvers: options.extraPredicateResolvers } : {}
212
182
  });
213
183
  if (!verdict.ok) return verdict;
214
- if (options.expectedBearer !== void 0 && !bytesEqual(verdict.claims.bearer, options.expectedBearer)) return {
184
+ if (options.expectedBearer !== void 0 && !require_token_scope.bytesEqual(verdict.claims.bearer, options.expectedBearer)) return {
215
185
  ok: false,
216
186
  reason: "bearer_mismatch"
217
187
  };
@@ -221,7 +191,7 @@ async function verifyCapabilityToken(token, options) {
221
191
  * Does one recorded revocation-claims entry actually revoke targetClaims, per management.cddl's own additive obligation? Valid when EITHER the entry's own issuer equals the target token's own issuer (the original, unconditional rule -- only a token's own issuer may revoke it), OR the entry carries an `authorization` that independently verifies as an ordinary capability-token -- with `expectedBearer` set to THIS entry's own `issuer`, proving the authorization was actually granted to the party submitting this revocation, not merely referenced from someone else's -- whose own `capability` is `manage:revoke` (spec/registry/core-capabilities.md) and whose own `scope` narrows targetClaims' scope. An authorization that fails any part of this (wrong capability, scope doesn't narrow, fails ordinary verification -- expired, revoked, bad signature, bearer mismatch) makes the entry no more valid than if `authorization` were absent; it never falls back to weakening the issuer-match rule.
222
192
  */
223
193
  async function revocationEntryGrantsRevoke(entry, targetClaims, options) {
224
- if (bytesEqual(entry.issuer, targetClaims.issuer)) return true;
194
+ if (require_token_scope.bytesEqual(entry.issuer, targetClaims.issuer)) return true;
225
195
  if (entry.authorization === void 0) return false;
226
196
  let decodedAuthorization;
227
197
  try {
@@ -235,7 +205,7 @@ async function revocationEntryGrantsRevoke(entry, targetClaims, options) {
235
205
  ...options,
236
206
  expectedBearer: entry.issuer
237
207
  });
238
- return authorizationVerdict.ok && authorizationVerdict.claims.capability === "manage:revoke" && scopeNarrows(authorizationVerdict.claims.scope, targetClaims.scope);
208
+ return authorizationVerdict.ok && authorizationVerdict.claims.capability === "manage:revoke" && require_token_scope.scopeNarrows(authorizationVerdict.claims.scope, targetClaims.scope);
239
209
  }
240
210
  async function verifyTokenChain(token, options) {
241
211
  const [protectedHeader, , payload, signature] = token;
@@ -262,7 +232,8 @@ async function verifyTokenChain(token, options) {
262
232
  ok: false,
263
233
  reason: "bad_signature"
264
234
  };
265
- if (!bytesEqual(await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]), claims.issuer)) return {
235
+ const derivedIssuerId = await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]);
236
+ if (!require_token_scope.bytesEqual(derivedIssuerId, claims.issuer)) return {
266
237
  ok: false,
267
238
  reason: "wrong_issuer"
268
239
  };
@@ -377,7 +348,8 @@ async function verifyRevocationEntry(entry, options) {
377
348
  ok: false,
378
349
  reason: "bad_signature"
379
350
  };
380
- if (!bytesEqual(await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]), claims.issuer)) return {
351
+ const derivedIssuerId = await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]);
352
+ if (!require_token_scope.bytesEqual(derivedIssuerId, claims.issuer)) return {
381
353
  ok: false,
382
354
  reason: "wrong_issuer"
383
355
  };
@@ -500,12 +472,6 @@ Object.defineProperty(exports, "mintRevocationEntry", {
500
472
  return mintRevocationEntry;
501
473
  }
502
474
  });
503
- Object.defineProperty(exports, "scopeNarrows", {
504
- enumerable: true,
505
- get: function() {
506
- return scopeNarrows;
507
- }
508
- });
509
475
  Object.defineProperty(exports, "verifyCapabilityToken", {
510
476
  enumerable: true,
511
477
  get: function() {
@@ -1,38 +1,8 @@
1
1
  import { capabilityTokenSchema, revocationClaimsSchema, tokenClaimsSchema } from "./generated/protocol.mjs";
2
+ import { n as scopeNarrows, t as bytesEqual } from "./token-scope-Z4bmci4M.mjs";
2
3
  import { z } from "zod";
3
4
  import { cdeDecodeOptions, cdeEncodeOptions, decode, encode } from "cbor2";
4
5
  import { PredicateNodeSchema, evaluatePredicate } from "trilean";
5
- //#region src/domain/token-scope.ts
6
- /**
7
- * Extracted out of tokens.ts specifically so token-predicates.ts (the generic evaluator's own delegate handlers) and tokens.ts (the chain walk, and its re-export for capability-grant.ts) can both depend on this pure, no-domain-imports module without creating an import cycle between the two -- tokens.ts imports evaluateNarrowing from token-predicates.ts, and token-predicates.ts's own scope-narrows/bearer-is handlers need these exact comparisons, so neither of those two files may import the other.
8
- */
9
- function bytesEqual(a, b) {
10
- if (a.length !== b.length) return false;
11
- for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
12
- return true;
13
- }
14
- /** True when the path contains a "." or ".." segment. Purely lexical prefix comparison would let "/work/../org" pass under "/work" -- a path that normalises outside the parent -- so any relative segment fails the narrowing comparison wholesale: fail-closed rather than reimplementing path normalisation, consistent with how empty, case-different, and non-boundary-prefixed paths already behave. */
15
- function hasRelativeSegment(path) {
16
- return path.split("/").some((segment) => segment === "." || segment === "..");
17
- }
18
- /** True when childPath is parentPath or a descendant of it, compared on "/"-segment boundaries: "/work/sub" narrows "/work", but "/workbook" does NOT narrow "/work" despite the string prefix, because "book" continues the same segment. Paths containing "." or ".." segments never narrow anything (see hasRelativeSegment). */
19
- function pathNarrows(childPath, parentPath) {
20
- if (hasRelativeSegment(childPath) || hasRelativeSegment(parentPath)) return false;
21
- if (childPath === parentPath) return true;
22
- if (!childPath.startsWith(parentPath)) return false;
23
- if (parentPath.endsWith("/")) return true;
24
- return childPath.charAt(parentPath.length) === "/";
25
- }
26
- /**
27
- * True when childScope narrows parentScope per tokens.cddl ("each hop can only narrow authority, never widen it"): the kind must be identical (a different kind is a different kind of authority, not a narrower one), and a parent with a path requires the child to carry an equal-or-descendant path -- an absent child path means the kind's whole-scope root, which is wider than any path-narrowed parent. A parent with no path (whole-scope root) lets any child path under the same kind through. Exported for capability-grant.ts's own obligation 4 (an unsolicited push's embedded token must equal-or-root the enclosing request's own scope), which is exactly this same narrowing relation applied outside a delegation chain, and for token-predicates.ts's own `scope-narrows` delegate handler.
28
- */
29
- function scopeNarrows(parent, child) {
30
- if (parent.kind !== child.kind) return false;
31
- if (parent.path === void 0) return true;
32
- if (child.path === void 0) return false;
33
- return pathNarrows(child.path, parent.path);
34
- }
35
- //#endregion
36
6
  //#region src/domain/token-predicates.ts
37
7
  /** The wire shape of `token-claims.conditions` once CBOR-decoded: trilean's own PredicateNodeSchema is the single source of truth for what a condition entry may contain, re-validated here rather than trusted from a CDDL-generated shadow schema (see tokens.cddl's own comment on why `conditions` is an opaque bstr, not a native CDDL type) -- a token from an untrusted peer must pass trilean's real schema before any of its conditions are evaluated. Explicitly annotated: trilean's PredicateNodeSchema is a deeply recursive z.lazy() type whose inferred shape is too large for tsdown's declaration-file generator to serialise (TS7056) without this. */
38
8
  const conditionsListSchema = z.array(PredicateNodeSchema);
@@ -262,7 +232,8 @@ async function verifyTokenChain(token, options) {
262
232
  ok: false,
263
233
  reason: "bad_signature"
264
234
  };
265
- if (!bytesEqual(await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]), claims.issuer)) return {
235
+ const derivedIssuerId = await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]);
236
+ if (!bytesEqual(derivedIssuerId, claims.issuer)) return {
266
237
  ok: false,
267
238
  reason: "wrong_issuer"
268
239
  };
@@ -377,7 +348,8 @@ async function verifyRevocationEntry(entry, options) {
377
348
  ok: false,
378
349
  reason: "bad_signature"
379
350
  };
380
- if (!bytesEqual(await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]), claims.issuer)) return {
351
+ const derivedIssuerId = await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]);
352
+ if (!bytesEqual(derivedIssuerId, claims.issuer)) return {
381
353
  ok: false,
382
354
  reason: "wrong_issuer"
383
355
  };
@@ -482,4 +454,4 @@ async function mintRevocationEntry(options) {
482
454
  ];
483
455
  }
484
456
  //#endregion
485
- export { verifyRevocationEntry as a, verifyCapabilityToken as i, mintCapabilityToken as n, scopeNarrows as o, mintRevocationEntry as r, canGrant as t };
457
+ export { verifyRevocationEntry as a, verifyCapabilityToken as i, mintCapabilityToken as n, mintRevocationEntry as r, canGrant as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
6
6
  "repository": {
@@ -98,6 +98,10 @@
98
98
  "import": "./dist/domain/device-id.mjs",
99
99
  "require": "./dist/domain/device-id.cjs"
100
100
  },
101
+ "./domain/grant-candidates": {
102
+ "import": "./dist/domain/grant-candidates.mjs",
103
+ "require": "./dist/domain/grant-candidates.cjs"
104
+ },
101
105
  "./domain/handshake": {
102
106
  "import": "./dist/domain/handshake.mjs",
103
107
  "require": "./dist/domain/handshake.cjs"