wire-mesh-core 1.14.0 → 1.16.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.
- package/dist/adapters/frame-codec.d.cts +1 -1
- package/dist/adapters/frame-codec.d.mts +1 -1
- package/dist/adapters/node-identity.d.cts +2 -2
- package/dist/adapters/node-identity.d.mts +2 -2
- package/dist/adapters/tcp-transport.d.cts +1 -1
- package/dist/adapters/tcp-transport.d.mts +1 -1
- package/dist/adapters/tls-transport.d.cts +1 -1
- package/dist/adapters/tls-transport.d.mts +1 -1
- package/dist/domain/capability-grant.cjs +90 -0
- package/dist/domain/capability-grant.d.cts +44 -0
- package/dist/domain/capability-grant.d.mts +44 -0
- package/dist/domain/capability-grant.mjs +87 -0
- package/dist/domain/capability-request.cjs +118 -0
- package/dist/domain/capability-request.d.cts +56 -0
- package/dist/domain/capability-request.d.mts +56 -0
- package/dist/domain/capability-request.mjs +115 -0
- package/dist/domain/device-id.d.cts +1 -1
- package/dist/domain/device-id.d.mts +1 -1
- package/dist/domain/handshake.d.cts +1 -1
- package/dist/domain/handshake.d.mts +1 -1
- package/dist/domain/mesh-session.d.cts +3 -3
- package/dist/domain/mesh-session.d.mts +3 -3
- package/dist/domain/relay-hub.d.cts +1 -1
- package/dist/domain/relay-hub.d.mts +1 -1
- package/dist/domain/revocation-view.cjs +1 -1
- package/dist/domain/revocation-view.d.cts +1 -1
- package/dist/domain/revocation-view.d.mts +1 -1
- package/dist/domain/revocation-view.mjs +1 -1
- package/dist/domain/room-token-verification.cjs +1 -1
- package/dist/domain/room-token-verification.d.cts +1 -1
- package/dist/domain/room-token-verification.d.mts +1 -1
- package/dist/domain/room-token-verification.mjs +1 -1
- package/dist/domain/tokens.cjs +2 -1
- package/dist/domain/tokens.d.cts +6 -2
- package/dist/domain/tokens.d.mts +6 -2
- package/dist/domain/tokens.mjs +2 -2
- package/dist/generated/protocol.cjs +6 -0
- package/dist/generated/protocol.d.cts +2 -2
- package/dist/generated/protocol.d.mts +2 -2
- package/dist/generated/protocol.mjs +6 -1
- package/dist/{identity-D8iJZRZZ.d.mts → identity-B1hBV3YA.d.mts} +1 -1
- package/dist/{identity-DkEe1465.d.cts → identity-BldV8xFj.d.cts} +1 -1
- package/dist/ports/identity.d.cts +1 -1
- package/dist/ports/identity.d.mts +1 -1
- package/dist/ports/transport.d.cts +2 -29
- package/dist/ports/transport.d.mts +2 -29
- package/dist/{protocol-MnxTzpRP.d.cts → protocol-Dhswk7YD.d.cts} +39 -1
- package/dist/{protocol-MnxTzpRP.d.mts → protocol-Dhswk7YD.d.mts} +39 -1
- package/dist/transport-BNNdUBq-.d.mts +30 -0
- package/dist/transport-hEUxrdhT.d.cts +30 -0
- package/package.json +9 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as Frame } from "../protocol-Dhswk7YD.cjs";
|
|
2
2
|
//#region src/adapters/frame-codec.d.ts
|
|
3
3
|
export declare function messageFromFrame(frame: Frame): Uint8Array<ArrayBuffer>;
|
|
4
4
|
/** A frame that fails schema validation, caught separately from a decode failure so it can be dropped without disconnecting. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as Frame } from "../protocol-Dhswk7YD.mjs";
|
|
2
2
|
//#region src/adapters/frame-codec.d.ts
|
|
3
3
|
export declare function messageFromFrame(frame: Frame): Uint8Array<ArrayBuffer>;
|
|
4
4
|
/** A frame that fails schema validation, caught separately from a decode failure so it can be dropped without disconnecting. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { P as IdentityKey, x as DeviceId } from "../protocol-Dhswk7YD.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-BldV8xFj.cjs";
|
|
3
3
|
import { webcrypto } from "node:crypto";
|
|
4
4
|
//#region src/adapters/node-identity.d.ts
|
|
5
5
|
export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as IdentityPort } from "../identity-
|
|
1
|
+
import { P as IdentityKey, x as DeviceId } from "../protocol-Dhswk7YD.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-B1hBV3YA.mjs";
|
|
3
3
|
import { webcrypto } from "node:crypto";
|
|
4
4
|
//#region src/adapters/node-identity.d.ts
|
|
5
5
|
export declare function deriveDeviceId(publicKey: Uint8Array): Promise<DeviceId>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Transport } from "../
|
|
1
|
+
import { r as Transport } from "../transport-hEUxrdhT.cjs";
|
|
2
2
|
//#region src/adapters/tcp-transport.d.ts
|
|
3
3
|
/** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
|
|
4
4
|
export declare function createTcpTransport(): Transport;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Transport } from "../
|
|
1
|
+
import { r as Transport } from "../transport-BNNdUBq-.mjs";
|
|
2
2
|
//#region src/adapters/tcp-transport.d.ts
|
|
3
3
|
/** A Node net.Socket-based Transport: length-prefixed, CBOR-encoded frames over plain TCP -- matching Cascade's own transport shape, since interop with Cascade nodes is wire-mesh's stated goal. Framing (not TLS) is this adapter's own concern; a TLS-terminated variant is a separate adapter behind the same Transport contract. */
|
|
4
4
|
export declare function createTcpTransport(): Transport;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_generated_protocol = require("../generated/protocol.cjs");
|
|
3
|
+
const require_domain_tokens = require("./tokens.cjs");
|
|
4
|
+
//#region src/domain/capability-grant.ts
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* Two responsibilities, split the same way capability-request.ts already splits them: sendCapabilityGrant is the pushing side (a thin wrapper over MeshSession.sendManageRequest), and createCapabilityGrantHandler is the receiving side (validates the pushed token against all four of management.cddl's own capability-grant obligations, then hands the verified grant to the domain). Unlike capability-request's handler, there is no decide()/timeout mechanism here: management.cddl deliberately specifies no protocol-level approval round trip for this primitive (a receiver's manage-response reports validation success or failure only, never a human decision), so onGrant is a plain notification callback, not an event carrying its own responder.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Builds a capability-grant command per management.cddl. The outer `manage-command.verb` is the capability string itself, the identical convention capability-request.ts's own buildCapabilityRequestCommand already establishes -- a receiver's per-capability handler is how it knows which grant this push is even for. `params.verb` is the fixed "capability.grant" marker. No scope or invitee field: manage-request-frame's own top-level scope already carries the former, and the request's own destination already carries the latter -- naming either a second time inside params could only ever disagree with the fact it duplicates.
|
|
12
|
+
*/
|
|
13
|
+
function buildCapabilityGrantCommand(capability, grantedToken) {
|
|
14
|
+
return {
|
|
15
|
+
verb: capability,
|
|
16
|
+
params: {
|
|
17
|
+
verb: "capability.grant",
|
|
18
|
+
"granted-token": grantedToken
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pushes an already-minted grantedToken to whichever peer this request is addressed to -- the caller mints grantedToken itself beforehand (there is no minting step here, unlike capability-request's own accept path, since this primitive carries a token the sender already decided to hand over unprompted). Resolves with the raw manage-response outcome: `{result:"ok"}` on successful validation, an ordinary manage-error otherwise (per management.cddl's own capability-grant obligations, this is a validation-failure code only, never a human "no" -- an application wanting a human-decision gate applies it on the RECEIVING side's own onGrant callback instead, exactly as core/room's own room.invite→room_invite delivery event already does today in agent-comms).
|
|
24
|
+
*
|
|
25
|
+
* scope and targetDevice forward directly to MeshSession.sendManageRequest's own identically-named parameters (the request's own top-level scope obligation 4 checks the token against, and relay routing respectively).
|
|
26
|
+
*/
|
|
27
|
+
async function sendCapabilityGrant(session, capability, grantedToken, scope, targetDevice) {
|
|
28
|
+
return session.sendManageRequest(buildCapabilityGrantCommand(capability, grantedToken), scope, targetDevice);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Builds a reusable handler for one capability's incoming capability-grants. The returned function checks, in order: the outer verb names this handler's own capability (otherwise `{result:"error", code:"malformed"}`, matching capability-request.ts's identical check); the params payload parses against capability-grant's own CDDL shape (otherwise "malformed"); the embedded token independently passes every ordinary verifyCapabilityToken obligation with `expectedBearer` set to this receiver's OWN identity -- obligations 1 and 3 together, since an unsolicited push must name its actual recipient as bearer and must otherwise be exactly as valid as any other token (a verification failure responds with verifyCapabilityToken's own specific TokenVerdictReason as the error code, e.g. "expired"/"revoked"/"bad_signature", rather than a single generic code, mirroring how capability-request.ts already distinguishes "expired" from "malformed"); the token's own capability claim equals this handler's capability (obligation 2, otherwise "capability_mismatch"); and the token's own scope equals or roots the enclosing request's own top-level scope (obligation 4, via tokens.ts's own scopeNarrows -- otherwise "scope_mismatch"). Only once every obligation passes does it respond `{result:"ok"}` and invoke onGrant -- there is no decide()/timeout mechanism the way createCapabilityRequestHandler has one, since management.cddl specifies no protocol-level approval round trip for this primitive at all.
|
|
32
|
+
*/
|
|
33
|
+
function createCapabilityGrantHandler(options) {
|
|
34
|
+
return async function handleCapabilityGrant(incoming) {
|
|
35
|
+
if (incoming.command.verb !== options.capability) {
|
|
36
|
+
await incoming.respond({
|
|
37
|
+
result: "error",
|
|
38
|
+
code: "malformed"
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const parsed = require_generated_protocol.capabilityGrantSchema.safeParse(incoming.command.params);
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
await incoming.respond({
|
|
45
|
+
result: "error",
|
|
46
|
+
code: "malformed"
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const grantedToken = parsed.data["granted-token"];
|
|
51
|
+
const verdict = await require_domain_tokens.verifyCapabilityToken(grantedToken, {
|
|
52
|
+
identity: options.identity,
|
|
53
|
+
clock: options.clock,
|
|
54
|
+
revocation: options.revocation,
|
|
55
|
+
expectedBearer: options.identity.deviceId
|
|
56
|
+
});
|
|
57
|
+
if (!verdict.ok) {
|
|
58
|
+
await incoming.respond({
|
|
59
|
+
result: "error",
|
|
60
|
+
code: verdict.reason
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (verdict.claims.capability !== options.capability) {
|
|
65
|
+
await incoming.respond({
|
|
66
|
+
result: "error",
|
|
67
|
+
code: "capability_mismatch"
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!require_domain_tokens.scopeNarrows(verdict.claims.scope, incoming.scope)) {
|
|
72
|
+
await incoming.respond({
|
|
73
|
+
result: "error",
|
|
74
|
+
code: "scope_mismatch"
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await incoming.respond({ result: "ok" });
|
|
79
|
+
options.onGrant({
|
|
80
|
+
capability: options.capability,
|
|
81
|
+
grantedToken,
|
|
82
|
+
scope: incoming.scope,
|
|
83
|
+
granterDevice: options.granterDevice
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
exports.buildCapabilityGrantCommand = buildCapabilityGrantCommand;
|
|
89
|
+
exports.createCapabilityGrantHandler = createCapabilityGrantHandler;
|
|
90
|
+
exports.sendCapabilityGrant = sendCapabilityGrant;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { F as ManageCommand, o as CapabilityScope, s as CapabilityToken, x as DeviceId } from "../protocol-Dhswk7YD.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-BldV8xFj.cjs";
|
|
3
|
+
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
|
+
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.cjs";
|
|
5
|
+
import { RevocationCheck } from "./tokens.cjs";
|
|
6
|
+
//#region src/domain/capability-grant.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Builds a capability-grant command per management.cddl. The outer `manage-command.verb` is the capability string itself, the identical convention capability-request.ts's own buildCapabilityRequestCommand already establishes -- a receiver's per-capability handler is how it knows which grant this push is even for. `params.verb` is the fixed "capability.grant" marker. No scope or invitee field: manage-request-frame's own top-level scope already carries the former, and the request's own destination already carries the latter -- naming either a second time inside params could only ever disagree with the fact it duplicates.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildCapabilityGrantCommand(capability: string, grantedToken: CapabilityToken): ManageCommand;
|
|
11
|
+
/**
|
|
12
|
+
* Pushes an already-minted grantedToken to whichever peer this request is addressed to -- the caller mints grantedToken itself beforehand (there is no minting step here, unlike capability-request's own accept path, since this primitive carries a token the sender already decided to hand over unprompted). Resolves with the raw manage-response outcome: `{result:"ok"}` on successful validation, an ordinary manage-error otherwise (per management.cddl's own capability-grant obligations, this is a validation-failure code only, never a human "no" -- an application wanting a human-decision gate applies it on the RECEIVING side's own onGrant callback instead, exactly as core/room's own room.invite→room_invite delivery event already does today in agent-comms).
|
|
13
|
+
*
|
|
14
|
+
* scope and targetDevice forward directly to MeshSession.sendManageRequest's own identically-named parameters (the request's own top-level scope obligation 4 checks the token against, and relay routing respectively).
|
|
15
|
+
*/
|
|
16
|
+
export declare function sendCapabilityGrant(session: Readonly<MeshSession>, capability: string, grantedToken: CapabilityToken, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId): Promise<ManageOutcome>;
|
|
17
|
+
/** One incoming, fully-verified capability-grant, surfaced for the domain to react to (store the token, notify a human, etc.) -- there is no decide() here because management.cddl specifies no protocol-level approval round trip for this primitive; by the time onGrant fires, the wire response has already been sent. */
|
|
18
|
+
export interface CapabilityGrantEvent {
|
|
19
|
+
/** The capability granted -- equals both the outer manage-command.verb and the verified token's own capability claim (obligation 2 already enforced this before onGrant fires). */
|
|
20
|
+
capability: string;
|
|
21
|
+
/** The pushed token, already confirmed to independently pass every ordinary verifyCapabilityToken obligation, name this receiver as its own bearer, and scope-match the enclosing request (obligations 1, 2, 3, and 4 respectively). Ready to use exactly as capability-request.ts's own CapabilityGrantOk["granted-token"] is on the pull side. */
|
|
22
|
+
grantedToken: CapabilityToken;
|
|
23
|
+
/** The scope this grant's own manage-request-frame carried -- e.g. core/room's `{kind:"room", path: roomPath}` -- passed through unchanged so the domain can inspect it (a room handler reads `.path` back out) without this module needing to know its shape. */
|
|
24
|
+
scope: Readonly<CapabilityScope>;
|
|
25
|
+
/** The peer device-id authenticated on the connection this grant arrived on -- the granter, surfaced so the domain can attribute the push (e.g. core/room's own room_invite event names the inviter). Never itself checked against the token's bearer (obligation 1 checks the RECIPIENT's own identity instead -- an unsolicited push names its own recipient by where it was sent, not by who sent it). */
|
|
26
|
+
granterDevice: DeviceId;
|
|
27
|
+
}
|
|
28
|
+
export interface CreateCapabilityGrantHandlerOptions {
|
|
29
|
+
/** The capability this handler accepts pushed grants for. Checked against the incoming command's own outer verb (a mismatch is refused as malformed, mirroring capability-request.ts's identical capability check) and against the verified token's own capability claim (obligation 2); a caller wanting to accept grants for several distinct capabilities over one session constructs one handler per capability, the same way core/room constructs one handler for `room:member` and nothing else. */
|
|
30
|
+
capability: string;
|
|
31
|
+
/** This receiver's own identity -- both the verification primitives every pushed token is checked with, and (via `.deviceId`) the value obligation 1 requires the token's own bearer to equal. There is no separate bearerDevice option the way CreateCapabilityRequestHandlerOptions has one: capability-request's handler mints a NEW token for whichever peer asked, so it needs that peer's device-id as an input; capability-grant's handler verifies an ALREADY-minted token against this side's own identity, which identity.deviceId already provides. */
|
|
32
|
+
identity: IdentityPort;
|
|
33
|
+
clock: Clock;
|
|
34
|
+
revocation: RevocationCheck;
|
|
35
|
+
/** The peer device-id authenticated on this session's own connection -- the granter, surfaced on every CapabilityGrantEvent so the domain can attribute the push. Never used for verification itself (see CapabilityGrantEvent.granterDevice's own doc comment on why obligation 1 checks the recipient's identity instead). */
|
|
36
|
+
granterDevice: DeviceId;
|
|
37
|
+
/** Called once per incoming, fully-verified capability-grant. Fires after the wire response has already been sent (see createCapabilityGrantHandler's own doc comment) -- this is a notification, not a decision point. */
|
|
38
|
+
onGrant: (event: Readonly<CapabilityGrantEvent>) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Builds a reusable handler for one capability's incoming capability-grants. The returned function checks, in order: the outer verb names this handler's own capability (otherwise `{result:"error", code:"malformed"}`, matching capability-request.ts's identical check); the params payload parses against capability-grant's own CDDL shape (otherwise "malformed"); the embedded token independently passes every ordinary verifyCapabilityToken obligation with `expectedBearer` set to this receiver's OWN identity -- obligations 1 and 3 together, since an unsolicited push must name its actual recipient as bearer and must otherwise be exactly as valid as any other token (a verification failure responds with verifyCapabilityToken's own specific TokenVerdictReason as the error code, e.g. "expired"/"revoked"/"bad_signature", rather than a single generic code, mirroring how capability-request.ts already distinguishes "expired" from "malformed"); the token's own capability claim equals this handler's capability (obligation 2, otherwise "capability_mismatch"); and the token's own scope equals or roots the enclosing request's own top-level scope (obligation 4, via tokens.ts's own scopeNarrows -- otherwise "scope_mismatch"). Only once every obligation passes does it respond `{result:"ok"}` and invoke onGrant -- there is no decide()/timeout mechanism the way createCapabilityRequestHandler has one, since management.cddl specifies no protocol-level approval round trip for this primitive at all.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createCapabilityGrantHandler(options: Readonly<CreateCapabilityGrantHandlerOptions>): (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
|
|
44
|
+
//#endregion
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { F as ManageCommand, o as CapabilityScope, s as CapabilityToken, x as DeviceId } from "../protocol-Dhswk7YD.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-B1hBV3YA.mjs";
|
|
3
|
+
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
4
|
+
import { IncomingManageRequest, ManageOutcome, MeshSession } from "./mesh-session.mjs";
|
|
5
|
+
import { RevocationCheck } from "./tokens.mjs";
|
|
6
|
+
//#region src/domain/capability-grant.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Builds a capability-grant command per management.cddl. The outer `manage-command.verb` is the capability string itself, the identical convention capability-request.ts's own buildCapabilityRequestCommand already establishes -- a receiver's per-capability handler is how it knows which grant this push is even for. `params.verb` is the fixed "capability.grant" marker. No scope or invitee field: manage-request-frame's own top-level scope already carries the former, and the request's own destination already carries the latter -- naming either a second time inside params could only ever disagree with the fact it duplicates.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildCapabilityGrantCommand(capability: string, grantedToken: CapabilityToken): ManageCommand;
|
|
11
|
+
/**
|
|
12
|
+
* Pushes an already-minted grantedToken to whichever peer this request is addressed to -- the caller mints grantedToken itself beforehand (there is no minting step here, unlike capability-request's own accept path, since this primitive carries a token the sender already decided to hand over unprompted). Resolves with the raw manage-response outcome: `{result:"ok"}` on successful validation, an ordinary manage-error otherwise (per management.cddl's own capability-grant obligations, this is a validation-failure code only, never a human "no" -- an application wanting a human-decision gate applies it on the RECEIVING side's own onGrant callback instead, exactly as core/room's own room.invite→room_invite delivery event already does today in agent-comms).
|
|
13
|
+
*
|
|
14
|
+
* scope and targetDevice forward directly to MeshSession.sendManageRequest's own identically-named parameters (the request's own top-level scope obligation 4 checks the token against, and relay routing respectively).
|
|
15
|
+
*/
|
|
16
|
+
export declare function sendCapabilityGrant(session: Readonly<MeshSession>, capability: string, grantedToken: CapabilityToken, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId): Promise<ManageOutcome>;
|
|
17
|
+
/** One incoming, fully-verified capability-grant, surfaced for the domain to react to (store the token, notify a human, etc.) -- there is no decide() here because management.cddl specifies no protocol-level approval round trip for this primitive; by the time onGrant fires, the wire response has already been sent. */
|
|
18
|
+
export interface CapabilityGrantEvent {
|
|
19
|
+
/** The capability granted -- equals both the outer manage-command.verb and the verified token's own capability claim (obligation 2 already enforced this before onGrant fires). */
|
|
20
|
+
capability: string;
|
|
21
|
+
/** The pushed token, already confirmed to independently pass every ordinary verifyCapabilityToken obligation, name this receiver as its own bearer, and scope-match the enclosing request (obligations 1, 2, 3, and 4 respectively). Ready to use exactly as capability-request.ts's own CapabilityGrantOk["granted-token"] is on the pull side. */
|
|
22
|
+
grantedToken: CapabilityToken;
|
|
23
|
+
/** The scope this grant's own manage-request-frame carried -- e.g. core/room's `{kind:"room", path: roomPath}` -- passed through unchanged so the domain can inspect it (a room handler reads `.path` back out) without this module needing to know its shape. */
|
|
24
|
+
scope: Readonly<CapabilityScope>;
|
|
25
|
+
/** The peer device-id authenticated on the connection this grant arrived on -- the granter, surfaced so the domain can attribute the push (e.g. core/room's own room_invite event names the inviter). Never itself checked against the token's bearer (obligation 1 checks the RECIPIENT's own identity instead -- an unsolicited push names its own recipient by where it was sent, not by who sent it). */
|
|
26
|
+
granterDevice: DeviceId;
|
|
27
|
+
}
|
|
28
|
+
export interface CreateCapabilityGrantHandlerOptions {
|
|
29
|
+
/** The capability this handler accepts pushed grants for. Checked against the incoming command's own outer verb (a mismatch is refused as malformed, mirroring capability-request.ts's identical capability check) and against the verified token's own capability claim (obligation 2); a caller wanting to accept grants for several distinct capabilities over one session constructs one handler per capability, the same way core/room constructs one handler for `room:member` and nothing else. */
|
|
30
|
+
capability: string;
|
|
31
|
+
/** This receiver's own identity -- both the verification primitives every pushed token is checked with, and (via `.deviceId`) the value obligation 1 requires the token's own bearer to equal. There is no separate bearerDevice option the way CreateCapabilityRequestHandlerOptions has one: capability-request's handler mints a NEW token for whichever peer asked, so it needs that peer's device-id as an input; capability-grant's handler verifies an ALREADY-minted token against this side's own identity, which identity.deviceId already provides. */
|
|
32
|
+
identity: IdentityPort;
|
|
33
|
+
clock: Clock;
|
|
34
|
+
revocation: RevocationCheck;
|
|
35
|
+
/** The peer device-id authenticated on this session's own connection -- the granter, surfaced on every CapabilityGrantEvent so the domain can attribute the push. Never used for verification itself (see CapabilityGrantEvent.granterDevice's own doc comment on why obligation 1 checks the recipient's identity instead). */
|
|
36
|
+
granterDevice: DeviceId;
|
|
37
|
+
/** Called once per incoming, fully-verified capability-grant. Fires after the wire response has already been sent (see createCapabilityGrantHandler's own doc comment) -- this is a notification, not a decision point. */
|
|
38
|
+
onGrant: (event: Readonly<CapabilityGrantEvent>) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Builds a reusable handler for one capability's incoming capability-grants. The returned function checks, in order: the outer verb names this handler's own capability (otherwise `{result:"error", code:"malformed"}`, matching capability-request.ts's identical check); the params payload parses against capability-grant's own CDDL shape (otherwise "malformed"); the embedded token independently passes every ordinary verifyCapabilityToken obligation with `expectedBearer` set to this receiver's OWN identity -- obligations 1 and 3 together, since an unsolicited push must name its actual recipient as bearer and must otherwise be exactly as valid as any other token (a verification failure responds with verifyCapabilityToken's own specific TokenVerdictReason as the error code, e.g. "expired"/"revoked"/"bad_signature", rather than a single generic code, mirroring how capability-request.ts already distinguishes "expired" from "malformed"); the token's own capability claim equals this handler's capability (obligation 2, otherwise "capability_mismatch"); and the token's own scope equals or roots the enclosing request's own top-level scope (obligation 4, via tokens.ts's own scopeNarrows -- otherwise "scope_mismatch"). Only once every obligation passes does it respond `{result:"ok"}` and invoke onGrant -- there is no decide()/timeout mechanism the way createCapabilityRequestHandler has one, since management.cddl specifies no protocol-level approval round trip for this primitive at all.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createCapabilityGrantHandler(options: Readonly<CreateCapabilityGrantHandlerOptions>): (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
|
|
44
|
+
//#endregion
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { capabilityGrantSchema } from "../generated/protocol.mjs";
|
|
2
|
+
import { scopeNarrows, verifyCapabilityToken } from "./tokens.mjs";
|
|
3
|
+
//#region src/domain/capability-grant.ts
|
|
4
|
+
/**
|
|
5
|
+
* 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.
|
|
6
|
+
*
|
|
7
|
+
* Two responsibilities, split the same way capability-request.ts already splits them: sendCapabilityGrant is the pushing side (a thin wrapper over MeshSession.sendManageRequest), and createCapabilityGrantHandler is the receiving side (validates the pushed token against all four of management.cddl's own capability-grant obligations, then hands the verified grant to the domain). Unlike capability-request's handler, there is no decide()/timeout mechanism here: management.cddl deliberately specifies no protocol-level approval round trip for this primitive (a receiver's manage-response reports validation success or failure only, never a human decision), so onGrant is a plain notification callback, not an event carrying its own responder.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Builds a capability-grant command per management.cddl. The outer `manage-command.verb` is the capability string itself, the identical convention capability-request.ts's own buildCapabilityRequestCommand already establishes -- a receiver's per-capability handler is how it knows which grant this push is even for. `params.verb` is the fixed "capability.grant" marker. No scope or invitee field: manage-request-frame's own top-level scope already carries the former, and the request's own destination already carries the latter -- naming either a second time inside params could only ever disagree with the fact it duplicates.
|
|
11
|
+
*/
|
|
12
|
+
function buildCapabilityGrantCommand(capability, grantedToken) {
|
|
13
|
+
return {
|
|
14
|
+
verb: capability,
|
|
15
|
+
params: {
|
|
16
|
+
verb: "capability.grant",
|
|
17
|
+
"granted-token": grantedToken
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pushes an already-minted grantedToken to whichever peer this request is addressed to -- the caller mints grantedToken itself beforehand (there is no minting step here, unlike capability-request's own accept path, since this primitive carries a token the sender already decided to hand over unprompted). Resolves with the raw manage-response outcome: `{result:"ok"}` on successful validation, an ordinary manage-error otherwise (per management.cddl's own capability-grant obligations, this is a validation-failure code only, never a human "no" -- an application wanting a human-decision gate applies it on the RECEIVING side's own onGrant callback instead, exactly as core/room's own room.invite→room_invite delivery event already does today in agent-comms).
|
|
23
|
+
*
|
|
24
|
+
* scope and targetDevice forward directly to MeshSession.sendManageRequest's own identically-named parameters (the request's own top-level scope obligation 4 checks the token against, and relay routing respectively).
|
|
25
|
+
*/
|
|
26
|
+
async function sendCapabilityGrant(session, capability, grantedToken, scope, targetDevice) {
|
|
27
|
+
return session.sendManageRequest(buildCapabilityGrantCommand(capability, grantedToken), scope, targetDevice);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds a reusable handler for one capability's incoming capability-grants. The returned function checks, in order: the outer verb names this handler's own capability (otherwise `{result:"error", code:"malformed"}`, matching capability-request.ts's identical check); the params payload parses against capability-grant's own CDDL shape (otherwise "malformed"); the embedded token independently passes every ordinary verifyCapabilityToken obligation with `expectedBearer` set to this receiver's OWN identity -- obligations 1 and 3 together, since an unsolicited push must name its actual recipient as bearer and must otherwise be exactly as valid as any other token (a verification failure responds with verifyCapabilityToken's own specific TokenVerdictReason as the error code, e.g. "expired"/"revoked"/"bad_signature", rather than a single generic code, mirroring how capability-request.ts already distinguishes "expired" from "malformed"); the token's own capability claim equals this handler's capability (obligation 2, otherwise "capability_mismatch"); and the token's own scope equals or roots the enclosing request's own top-level scope (obligation 4, via tokens.ts's own scopeNarrows -- otherwise "scope_mismatch"). Only once every obligation passes does it respond `{result:"ok"}` and invoke onGrant -- there is no decide()/timeout mechanism the way createCapabilityRequestHandler has one, since management.cddl specifies no protocol-level approval round trip for this primitive at all.
|
|
31
|
+
*/
|
|
32
|
+
function createCapabilityGrantHandler(options) {
|
|
33
|
+
return async function handleCapabilityGrant(incoming) {
|
|
34
|
+
if (incoming.command.verb !== options.capability) {
|
|
35
|
+
await incoming.respond({
|
|
36
|
+
result: "error",
|
|
37
|
+
code: "malformed"
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const parsed = capabilityGrantSchema.safeParse(incoming.command.params);
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
await incoming.respond({
|
|
44
|
+
result: "error",
|
|
45
|
+
code: "malformed"
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const grantedToken = parsed.data["granted-token"];
|
|
50
|
+
const verdict = await verifyCapabilityToken(grantedToken, {
|
|
51
|
+
identity: options.identity,
|
|
52
|
+
clock: options.clock,
|
|
53
|
+
revocation: options.revocation,
|
|
54
|
+
expectedBearer: options.identity.deviceId
|
|
55
|
+
});
|
|
56
|
+
if (!verdict.ok) {
|
|
57
|
+
await incoming.respond({
|
|
58
|
+
result: "error",
|
|
59
|
+
code: verdict.reason
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (verdict.claims.capability !== options.capability) {
|
|
64
|
+
await incoming.respond({
|
|
65
|
+
result: "error",
|
|
66
|
+
code: "capability_mismatch"
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!scopeNarrows(verdict.claims.scope, incoming.scope)) {
|
|
71
|
+
await incoming.respond({
|
|
72
|
+
result: "error",
|
|
73
|
+
code: "scope_mismatch"
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await incoming.respond({ result: "ok" });
|
|
78
|
+
options.onGrant({
|
|
79
|
+
capability: options.capability,
|
|
80
|
+
grantedToken,
|
|
81
|
+
scope: incoming.scope,
|
|
82
|
+
granterDevice: options.granterDevice
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { buildCapabilityGrantCommand, createCapabilityGrantHandler, sendCapabilityGrant };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_generated_protocol = require("../generated/protocol.cjs");
|
|
3
|
+
const require_domain_tokens = require("./tokens.cjs");
|
|
4
|
+
//#region src/domain/capability-request.ts
|
|
5
|
+
/**
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* Two responsibilities live here, split the same way tokens.ts and room-token-verification.ts already split verification concerns: requestCapability is the asking side (a thin wrapper over MeshSession.sendManageRequest, parsing the grant response), and createCapabilityRequestHandler is the receiving side (validates the incoming ask, arms a receiver-side timeout so an unanswered request never hangs a requester forever, and hands the decision to the domain).
|
|
9
|
+
*/
|
|
10
|
+
const TOKEN_ID_BYTE_LENGTH = 16;
|
|
11
|
+
function randomTokenId() {
|
|
12
|
+
const bytes = new Uint8Array(TOKEN_ID_BYTE_LENGTH);
|
|
13
|
+
crypto.getRandomValues(bytes);
|
|
14
|
+
return bytes;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Builds a capability-request command per management.cddl. The outer `manage-command.verb` is the capability string itself -- the same value as this map's own `params.capability` field -- never a generic literal: capability-verb's own CDDL grammar is a closed alternation of colon-delimited `domain:noun` regexes (spec/tokens.cddl), so a bare literal like "capability" could never satisfy it, and routing by capability (the same way room.send/room.join already share the ROOM_MEMBER_CAPABILITY outer verb) is how a receiver's own per-capability handler knows which grant this ask is even for. `params.verb` is the fixed "capability.request" marker that distinguishes this ask from any other verb sharing the same outer capability (mirroring room.send/room.join's own inner `params.verb` split under one shared outer verb). `valid-until`, when given, bounds how long this ask is worth granting (wire-mesh#82) -- a receiver refuses outright once its own clock has passed it, rather than presenting a stale ask to a human for approval.
|
|
18
|
+
*/
|
|
19
|
+
function buildCapabilityRequestCommand(capability, validUntil) {
|
|
20
|
+
return {
|
|
21
|
+
verb: capability,
|
|
22
|
+
params: {
|
|
23
|
+
verb: "capability.request",
|
|
24
|
+
capability,
|
|
25
|
+
...validUntil !== void 0 ? { "valid-until": validUntil } : {}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sends a deliberately ungated capability-request (no token field at all -- per management.cddl's own header comment, this primitive puts access control entirely in the receiving side's own decision, not a capability check on the request itself, the same design room.join already established). Resolves with the freshly granted capability-grant-ok on approval (its own open `* tstr => any` tail may carry domain-specific extension fields, e.g. core/room's member list -- a caller that needs those parses the raw result itself, the same way room-client.ts's own requestToJoin wrapper does); rejects on denial (an ordinary manage-error) or a malformed response.
|
|
31
|
+
*
|
|
32
|
+
* targetDevice and timeoutMs forward directly to MeshSession.sendManageRequest's own identically-named parameters (relay routing and a requester-side give-up bound respectively -- wire-mesh#80 is already implemented there, not duplicated here). validUntil, when given, is attached to the request itself (wire-mesh#82) so a slow-to-answer receiver -- or a relay/facilitator forwarding this request on the caller's behalf -- can refuse or drop a now-stale ask outright rather than holding or forwarding it.
|
|
33
|
+
*/
|
|
34
|
+
async function requestCapability(session, capability, scope, targetDevice, timeoutMs, validUntil) {
|
|
35
|
+
const outcome = await session.sendManageRequest(buildCapabilityRequestCommand(capability, validUntil), scope, targetDevice, void 0, timeoutMs);
|
|
36
|
+
if (outcome.result !== "ok") throw new Error(`capability request for "${capability}" was refused (${outcome.code})`);
|
|
37
|
+
const parsed = require_generated_protocol.capabilityGrantOkSchema.safeParse(outcome);
|
|
38
|
+
if (!parsed.success) throw new Error(`capability request for "${capability}" response was malformed`);
|
|
39
|
+
return parsed.data;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Builds a reusable handler for one capability's incoming capability-requests. The returned function is the generic counterpart to room-client.ts's own (now capability-specific) handleRoomJoin: given one IncomingManageRequest, it refuses a request for the wrong capability or a malformed payload as `{result:"error", code:"malformed"}`, refuses an already-expired request (per its own `valid-until`, wire-mesh#82) as `{result:"error", code:"expired"}` without ever invoking onRequest, otherwise arms the receiver-side timeout described on CreateCapabilityRequestHandlerOptions.timeoutMs and calls onRequest so the domain can accept (minting a fresh token via mintCapabilityToken and responding `{result:"ok", "granted-token": token, ...extensions}`) or reject (an ordinary manage-error) via the event's own decide().
|
|
43
|
+
*/
|
|
44
|
+
function createCapabilityRequestHandler(options) {
|
|
45
|
+
return async function handleCapabilityRequest(incoming) {
|
|
46
|
+
const parsed = require_generated_protocol.capabilityRequestSchema.safeParse(incoming.command.params);
|
|
47
|
+
if (!parsed.success || parsed.data.capability !== options.capability) {
|
|
48
|
+
await incoming.respond({
|
|
49
|
+
result: "error",
|
|
50
|
+
code: "malformed"
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const validUntil = parsed.data["valid-until"];
|
|
55
|
+
if (validUntil !== void 0 && validUntil <= options.clock.now()) {
|
|
56
|
+
await incoming.respond({
|
|
57
|
+
result: "error",
|
|
58
|
+
code: "expired"
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let settled = false;
|
|
63
|
+
const timeoutHandle = setTimeout(() => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
incoming.respond({
|
|
67
|
+
result: "error",
|
|
68
|
+
code: "timeout",
|
|
69
|
+
message: "no decision within the capability-request timeout"
|
|
70
|
+
}).catch(() => void 0);
|
|
71
|
+
}, options.timeoutMs);
|
|
72
|
+
timeoutHandle.unref();
|
|
73
|
+
options.onRequest({
|
|
74
|
+
scope: incoming.scope,
|
|
75
|
+
requesterDevice: options.bearerDevice,
|
|
76
|
+
async decide(decision) {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timeoutHandle);
|
|
80
|
+
if (decision.kind === "reject") {
|
|
81
|
+
await incoming.respond({
|
|
82
|
+
result: "error",
|
|
83
|
+
code: "denied",
|
|
84
|
+
...decision.reason !== void 0 ? { message: decision.reason } : {}
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const verdict = await require_domain_tokens.mintCapabilityToken({
|
|
89
|
+
identity: options.identity,
|
|
90
|
+
clock: options.clock,
|
|
91
|
+
tokenId: randomTokenId(),
|
|
92
|
+
bearer: options.bearerDevice,
|
|
93
|
+
capability: decision.capability,
|
|
94
|
+
scope: incoming.scope,
|
|
95
|
+
expires: decision.expires,
|
|
96
|
+
...decision.delegationsRemaining !== void 0 ? { delegationsRemaining: decision.delegationsRemaining } : {}
|
|
97
|
+
});
|
|
98
|
+
if (!verdict.ok) {
|
|
99
|
+
await incoming.respond({
|
|
100
|
+
result: "error",
|
|
101
|
+
code: "mint_failed"
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const grantedToken = verdict.token;
|
|
106
|
+
await incoming.respond({
|
|
107
|
+
result: "ok",
|
|
108
|
+
"granted-token": grantedToken,
|
|
109
|
+
...decision.extensions ?? {}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
exports.buildCapabilityRequestCommand = buildCapabilityRequestCommand;
|
|
117
|
+
exports.createCapabilityRequestHandler = createCapabilityRequestHandler;
|
|
118
|
+
exports.requestCapability = requestCapability;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { F as ManageCommand, i as CapabilityGrantOk, o as CapabilityScope, x as DeviceId } from "../protocol-Dhswk7YD.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-BldV8xFj.cjs";
|
|
3
|
+
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
|
+
import { IncomingManageRequest, MeshSession } from "./mesh-session.cjs";
|
|
5
|
+
//#region src/domain/capability-request.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Builds a capability-request command per management.cddl. The outer `manage-command.verb` is the capability string itself -- the same value as this map's own `params.capability` field -- never a generic literal: capability-verb's own CDDL grammar is a closed alternation of colon-delimited `domain:noun` regexes (spec/tokens.cddl), so a bare literal like "capability" could never satisfy it, and routing by capability (the same way room.send/room.join already share the ROOM_MEMBER_CAPABILITY outer verb) is how a receiver's own per-capability handler knows which grant this ask is even for. `params.verb` is the fixed "capability.request" marker that distinguishes this ask from any other verb sharing the same outer capability (mirroring room.send/room.join's own inner `params.verb` split under one shared outer verb). `valid-until`, when given, bounds how long this ask is worth granting (wire-mesh#82) -- a receiver refuses outright once its own clock has passed it, rather than presenting a stale ask to a human for approval.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildCapabilityRequestCommand(capability: string, validUntil?: number): ManageCommand;
|
|
10
|
+
/**
|
|
11
|
+
* Sends a deliberately ungated capability-request (no token field at all -- per management.cddl's own header comment, this primitive puts access control entirely in the receiving side's own decision, not a capability check on the request itself, the same design room.join already established). Resolves with the freshly granted capability-grant-ok on approval (its own open `* tstr => any` tail may carry domain-specific extension fields, e.g. core/room's member list -- a caller that needs those parses the raw result itself, the same way room-client.ts's own requestToJoin wrapper does); rejects on denial (an ordinary manage-error) or a malformed response.
|
|
12
|
+
*
|
|
13
|
+
* targetDevice and timeoutMs forward directly to MeshSession.sendManageRequest's own identically-named parameters (relay routing and a requester-side give-up bound respectively -- wire-mesh#80 is already implemented there, not duplicated here). validUntil, when given, is attached to the request itself (wire-mesh#82) so a slow-to-answer receiver -- or a relay/facilitator forwarding this request on the caller's behalf -- can refuse or drop a now-stale ask outright rather than holding or forwarding it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function requestCapability(session: Readonly<MeshSession>, capability: string, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, timeoutMs?: number, validUntil?: number): Promise<CapabilityGrantOk>;
|
|
16
|
+
/**
|
|
17
|
+
* A receiver's decision on one incoming capability-request, mirroring room-client.ts's own (now capability-specific) RoomJoinDecision, generalized to any capability: "accept" mints and returns a fresh token; "reject" sends an ordinary manage-error carrying an optional human-readable reason.
|
|
18
|
+
*/
|
|
19
|
+
export type CapabilityGrantDecision = {
|
|
20
|
+
kind: "accept";
|
|
21
|
+
/** The capability the minted token actually carries -- usually, but not necessarily, the same string the request itself asked for (a receiver is free to grant something narrower). */
|
|
22
|
+
capability: string;
|
|
23
|
+
expires: number;
|
|
24
|
+
delegationsRemaining?: number;
|
|
25
|
+
/** Fields the domain wants merged onto the `{result:"ok", "granted-token": token, ...}` response, riding capability-grant-ok's own open `* tstr => any` tail -- the mechanism core/room's own room-join-ok uses for its `members` field, generalized so this module never needs to know what any particular capability's own grant response should additionally carry. Omit for a capability (e.g. a bare exec grant) whose response needs nothing beyond the token itself. */
|
|
26
|
+
extensions?: Record<string, unknown>;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "reject";
|
|
29
|
+
reason?: string;
|
|
30
|
+
};
|
|
31
|
+
/** One incoming, not-yet-expired capability-request, surfaced for the domain to accept or reject via decide(). */
|
|
32
|
+
export interface CapabilityGrantRequestEvent {
|
|
33
|
+
/** The scope this request's own manage-request-frame carries -- e.g. core/room's `{kind:"room", path: roomPath}` -- passed through unchanged so the domain can inspect it (a room handler reads `.path` back out) without this module needing to know its shape. */
|
|
34
|
+
scope: Readonly<CapabilityScope>;
|
|
35
|
+
/** The peer device-id authenticated on the connection this request arrived over -- the requester, and so the future bearer of any token minted on acceptance. Same role as RoomRouterOptions.peerDevice / RoomJoinRequestEvent.requesterDevice. */
|
|
36
|
+
requesterDevice: DeviceId;
|
|
37
|
+
/** Resolves this request with the domain's decision. Settles the request's response exactly once: whichever of a real decide() call or the handler's own receiver-side timeout comes first wins, and the other is a no-op -- the same single-settle guarantee agent-comms' own PendingConnection timer/accept/reject/disconnect race already established (ported here, not reinvented). */
|
|
38
|
+
decide: (decision: Readonly<CapabilityGrantDecision>) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export interface CreateCapabilityRequestHandlerOptions {
|
|
41
|
+
/** The capability this handler grants. Checked against both the incoming request's own `params.capability` field (a mismatch is refused as malformed rather than trusted -- nothing about manage-command.verb structurally guarantees params.capability agrees with it) and used as the scope this handler is willing to act on at all; a caller wanting to grant several distinct capabilities over one session constructs one handler per capability, the same way core/room constructs one handler for `room:member` and nothing else. */
|
|
42
|
+
capability: string;
|
|
43
|
+
identity: IdentityPort;
|
|
44
|
+
clock: Clock;
|
|
45
|
+
/** The peer device-id authenticated on this session's own connection -- the requester, and so the future bearer of any token this handler mints. One handler is constructed per session for the same reason one RoomRouter is (RoomRouterOptions.peerDevice): MeshSession exposes no way for domain code to learn the authenticated peer independently. */
|
|
46
|
+
bearerDevice: DeviceId;
|
|
47
|
+
/** Receiver-side auto-reject window (wire-mesh#81): how long an incoming request may sit awaiting the domain's decision before this side responds with a real manage-error timeout rather than leaving the requester's own sendManageRequest hanging indefinitely. Ported from agent-comms' PendingConnection pattern (`wire-mesh-transport.ts`'s `pendingConnectionTimeoutMs` / `expirePendingConnection`): an unref'd timer so a stray one can never by itself keep the process alive, cleared the instant decide() is actually called so it can never fire after the request has already been answered. IncomingManageRequest exposes no requester-disconnect signal at this layer (unlike agent-comms' own transport, which owns the raw connection), so only the timeout half of that pattern is implemented here -- there is nothing to detect the requester giving up early honestly, so this module does not pretend to. */
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
/** Called once per incoming, not-yet-expired capability-request, for the domain to accept or reject via the event's own decide(). */
|
|
50
|
+
onRequest: (event: Readonly<CapabilityGrantRequestEvent>) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Builds a reusable handler for one capability's incoming capability-requests. The returned function is the generic counterpart to room-client.ts's own (now capability-specific) handleRoomJoin: given one IncomingManageRequest, it refuses a request for the wrong capability or a malformed payload as `{result:"error", code:"malformed"}`, refuses an already-expired request (per its own `valid-until`, wire-mesh#82) as `{result:"error", code:"expired"}` without ever invoking onRequest, otherwise arms the receiver-side timeout described on CreateCapabilityRequestHandlerOptions.timeoutMs and calls onRequest so the domain can accept (minting a fresh token via mintCapabilityToken and responding `{result:"ok", "granted-token": token, ...extensions}`) or reject (an ordinary manage-error) via the event's own decide().
|
|
54
|
+
*/
|
|
55
|
+
export declare function createCapabilityRequestHandler(options: Readonly<CreateCapabilityRequestHandlerOptions>): (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
|
|
56
|
+
//#endregion
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { F as ManageCommand, i as CapabilityGrantOk, o as CapabilityScope, x as DeviceId } from "../protocol-Dhswk7YD.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-B1hBV3YA.mjs";
|
|
3
|
+
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
4
|
+
import { IncomingManageRequest, MeshSession } from "./mesh-session.mjs";
|
|
5
|
+
//#region src/domain/capability-request.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Builds a capability-request command per management.cddl. The outer `manage-command.verb` is the capability string itself -- the same value as this map's own `params.capability` field -- never a generic literal: capability-verb's own CDDL grammar is a closed alternation of colon-delimited `domain:noun` regexes (spec/tokens.cddl), so a bare literal like "capability" could never satisfy it, and routing by capability (the same way room.send/room.join already share the ROOM_MEMBER_CAPABILITY outer verb) is how a receiver's own per-capability handler knows which grant this ask is even for. `params.verb` is the fixed "capability.request" marker that distinguishes this ask from any other verb sharing the same outer capability (mirroring room.send/room.join's own inner `params.verb` split under one shared outer verb). `valid-until`, when given, bounds how long this ask is worth granting (wire-mesh#82) -- a receiver refuses outright once its own clock has passed it, rather than presenting a stale ask to a human for approval.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildCapabilityRequestCommand(capability: string, validUntil?: number): ManageCommand;
|
|
10
|
+
/**
|
|
11
|
+
* Sends a deliberately ungated capability-request (no token field at all -- per management.cddl's own header comment, this primitive puts access control entirely in the receiving side's own decision, not a capability check on the request itself, the same design room.join already established). Resolves with the freshly granted capability-grant-ok on approval (its own open `* tstr => any` tail may carry domain-specific extension fields, e.g. core/room's member list -- a caller that needs those parses the raw result itself, the same way room-client.ts's own requestToJoin wrapper does); rejects on denial (an ordinary manage-error) or a malformed response.
|
|
12
|
+
*
|
|
13
|
+
* targetDevice and timeoutMs forward directly to MeshSession.sendManageRequest's own identically-named parameters (relay routing and a requester-side give-up bound respectively -- wire-mesh#80 is already implemented there, not duplicated here). validUntil, when given, is attached to the request itself (wire-mesh#82) so a slow-to-answer receiver -- or a relay/facilitator forwarding this request on the caller's behalf -- can refuse or drop a now-stale ask outright rather than holding or forwarding it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function requestCapability(session: Readonly<MeshSession>, capability: string, scope: Readonly<CapabilityScope>, targetDevice?: DeviceId, timeoutMs?: number, validUntil?: number): Promise<CapabilityGrantOk>;
|
|
16
|
+
/**
|
|
17
|
+
* A receiver's decision on one incoming capability-request, mirroring room-client.ts's own (now capability-specific) RoomJoinDecision, generalized to any capability: "accept" mints and returns a fresh token; "reject" sends an ordinary manage-error carrying an optional human-readable reason.
|
|
18
|
+
*/
|
|
19
|
+
export type CapabilityGrantDecision = {
|
|
20
|
+
kind: "accept";
|
|
21
|
+
/** The capability the minted token actually carries -- usually, but not necessarily, the same string the request itself asked for (a receiver is free to grant something narrower). */
|
|
22
|
+
capability: string;
|
|
23
|
+
expires: number;
|
|
24
|
+
delegationsRemaining?: number;
|
|
25
|
+
/** Fields the domain wants merged onto the `{result:"ok", "granted-token": token, ...}` response, riding capability-grant-ok's own open `* tstr => any` tail -- the mechanism core/room's own room-join-ok uses for its `members` field, generalized so this module never needs to know what any particular capability's own grant response should additionally carry. Omit for a capability (e.g. a bare exec grant) whose response needs nothing beyond the token itself. */
|
|
26
|
+
extensions?: Record<string, unknown>;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "reject";
|
|
29
|
+
reason?: string;
|
|
30
|
+
};
|
|
31
|
+
/** One incoming, not-yet-expired capability-request, surfaced for the domain to accept or reject via decide(). */
|
|
32
|
+
export interface CapabilityGrantRequestEvent {
|
|
33
|
+
/** The scope this request's own manage-request-frame carries -- e.g. core/room's `{kind:"room", path: roomPath}` -- passed through unchanged so the domain can inspect it (a room handler reads `.path` back out) without this module needing to know its shape. */
|
|
34
|
+
scope: Readonly<CapabilityScope>;
|
|
35
|
+
/** The peer device-id authenticated on the connection this request arrived over -- the requester, and so the future bearer of any token minted on acceptance. Same role as RoomRouterOptions.peerDevice / RoomJoinRequestEvent.requesterDevice. */
|
|
36
|
+
requesterDevice: DeviceId;
|
|
37
|
+
/** Resolves this request with the domain's decision. Settles the request's response exactly once: whichever of a real decide() call or the handler's own receiver-side timeout comes first wins, and the other is a no-op -- the same single-settle guarantee agent-comms' own PendingConnection timer/accept/reject/disconnect race already established (ported here, not reinvented). */
|
|
38
|
+
decide: (decision: Readonly<CapabilityGrantDecision>) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export interface CreateCapabilityRequestHandlerOptions {
|
|
41
|
+
/** The capability this handler grants. Checked against both the incoming request's own `params.capability` field (a mismatch is refused as malformed rather than trusted -- nothing about manage-command.verb structurally guarantees params.capability agrees with it) and used as the scope this handler is willing to act on at all; a caller wanting to grant several distinct capabilities over one session constructs one handler per capability, the same way core/room constructs one handler for `room:member` and nothing else. */
|
|
42
|
+
capability: string;
|
|
43
|
+
identity: IdentityPort;
|
|
44
|
+
clock: Clock;
|
|
45
|
+
/** The peer device-id authenticated on this session's own connection -- the requester, and so the future bearer of any token this handler mints. One handler is constructed per session for the same reason one RoomRouter is (RoomRouterOptions.peerDevice): MeshSession exposes no way for domain code to learn the authenticated peer independently. */
|
|
46
|
+
bearerDevice: DeviceId;
|
|
47
|
+
/** Receiver-side auto-reject window (wire-mesh#81): how long an incoming request may sit awaiting the domain's decision before this side responds with a real manage-error timeout rather than leaving the requester's own sendManageRequest hanging indefinitely. Ported from agent-comms' PendingConnection pattern (`wire-mesh-transport.ts`'s `pendingConnectionTimeoutMs` / `expirePendingConnection`): an unref'd timer so a stray one can never by itself keep the process alive, cleared the instant decide() is actually called so it can never fire after the request has already been answered. IncomingManageRequest exposes no requester-disconnect signal at this layer (unlike agent-comms' own transport, which owns the raw connection), so only the timeout half of that pattern is implemented here -- there is nothing to detect the requester giving up early honestly, so this module does not pretend to. */
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
/** Called once per incoming, not-yet-expired capability-request, for the domain to accept or reject via the event's own decide(). */
|
|
50
|
+
onRequest: (event: Readonly<CapabilityGrantRequestEvent>) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Builds a reusable handler for one capability's incoming capability-requests. The returned function is the generic counterpart to room-client.ts's own (now capability-specific) handleRoomJoin: given one IncomingManageRequest, it refuses a request for the wrong capability or a malformed payload as `{result:"error", code:"malformed"}`, refuses an already-expired request (per its own `valid-until`, wire-mesh#82) as `{result:"error", code:"expired"}` without ever invoking onRequest, otherwise arms the receiver-side timeout described on CreateCapabilityRequestHandlerOptions.timeoutMs and calls onRequest so the domain can accept (minting a fresh token via mintCapabilityToken and responding `{result:"ok", "granted-token": token, ...extensions}`) or reject (an ordinary manage-error) via the event's own decide().
|
|
54
|
+
*/
|
|
55
|
+
export declare function createCapabilityRequestHandler(options: Readonly<CreateCapabilityRequestHandlerOptions>): (incoming: Readonly<IncomingManageRequest>) => Promise<void>;
|
|
56
|
+
//#endregion
|