@cortexkit/subc-client 0.12.0 → 0.13.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { SubcClient, SubcError, SubcCallError, DEFAULT_RECONNECT_BACKOFF, SUBC_MODULE_ID_ENV, SUBC_LAUNCH_NONCE_ENV, isConsumerReconnectTransient, parseRouteCloseReason, classifyRouteCloseReason, routeCloseReason, isValidCapabilityIdentifier, connectionFileExists, } from "./client.js";
2
- export { RouteHandle, StaleRouteHandleError } from "./route-handle.js";
2
+ export { ReverseRequestRegistry, ReverseRequestRegistrationError, RouteHandle, StaleRouteHandleError, } from "./route-handle.js";
3
3
  export { readConnectionFile, ConnectionFileError, } from "./connection-file.js";
4
- export { FrameType, Priority, AdmissionClass, DAEMON_ORIGIN_FLAG, PROTOCOL_VERSION, HEADER_LEN, buildFrame, buildFrameWithVersion, buildFlags, encodeFrame, decodeHeader, hasBinary, hasDaemonOrigin, encodeHeader, DecodeError, } from "./envelope.js";
4
+ export { FrameType, Priority, AdmissionClass, DAEMON_ORIGIN_FLAG, SUBSCRIPTION_FLAG, PROTOCOL_VERSION, HEADER_LEN, buildFrame, buildFrameWithVersion, buildFlags, encodeFrame, decodeHeader, hasBinary, hasDaemonOrigin, encodeHeader, DecodeError, } from "./envelope.js";
5
5
  export { authenticateClient, computeProof, AuthError, NONCE_LEN, PROOF_LEN, SERVER_PROOF_DOMAIN, CLIENT_AUTH_DOMAIN, } from "./auth.js";
6
6
  export { SubcSocket, SocketClosedError, SocketTimeoutError } from "./socket.js";
7
7
  export { SubcProvider, SubcProviderError, HELLO_CORR, managementSurfaceManifest, jsonProviderHandler, } from "./provider.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,aAAa,EACb,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,EACrB,4BAA4B,EAC5B,qBAAqB,EACrB,wBAAwB,EACxB,gBAAgB,EAChB,2BAA2B,EAC3B,oBAAoB,GAwBrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,SAAS,EACT,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,qBAAqB,EACrB,UAAU,EACV,WAAW,EACX,YAAY,EACZ,SAAS,EACT,eAAe,EACf,YAAY,EACZ,WAAW,GAIZ,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACT,SAAS,EACT,SAAS,EACT,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACV,yBAAyB,EACzB,mBAAmB,GAmCpB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,aAAa,EACb,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,EACrB,4BAA4B,EAC5B,qBAAqB,EACrB,wBAAwB,EACxB,gBAAgB,EAChB,2BAA2B,EAC3B,oBAAoB,GAwBrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,+BAA+B,EAC/B,WAAW,EACX,qBAAqB,GAGtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GAGpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,SAAS,EACT,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,qBAAqB,EACrB,UAAU,EACV,WAAW,EACX,YAAY,EACZ,SAAS,EACT,eAAe,EACf,YAAY,EACZ,WAAW,GAIZ,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACT,SAAS,EACT,SAAS,EACT,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACV,yBAAyB,EACzB,mBAAmB,GAmCpB,MAAM,eAAe,CAAC"}
@@ -1,3 +1,30 @@
1
+ export interface ReverseRequestContext {
2
+ readonly corr: bigint;
3
+ readonly method: string;
4
+ }
5
+ export type ReverseRequestHandler = (body: Uint8Array, ctx: ReverseRequestContext) => Promise<Uint8Array> | Uint8Array;
6
+ /** A reverse-request registration cannot add a capability after route.open declared its set. */
7
+ export declare class ReverseRequestRegistrationError extends Error {
8
+ readonly methodFamily: string;
9
+ readonly code = "reverse_request_capability_not_declared";
10
+ constructor(methodFamily: string);
11
+ }
12
+ /**
13
+ * Mutable handler registry prepared before route.open. The client derives
14
+ * consumer_capabilities from its method families and seals that set when the
15
+ * route opens; handlers for declared families may still be replaced afterward.
16
+ */
17
+ export declare class ReverseRequestRegistry {
18
+ private readonly handlers;
19
+ private declaredFamilies;
20
+ onRequest(methodFamily: string, handler: ReverseRequestHandler): void;
21
+ /** @internal */
22
+ capabilities(): string[];
23
+ /** @internal */
24
+ handler(methodFamily: string): ReverseRequestHandler | undefined;
25
+ /** @internal */
26
+ seal(): void;
27
+ }
1
28
  /**
2
29
  * Immutable identity for one route binding on one live socket. Only channel and
3
30
  * epoch cross the wire; the connection token remains private to this SDK.
@@ -6,10 +33,14 @@ export declare class RouteHandle {
6
33
  readonly channel: number;
7
34
  readonly epoch: number;
8
35
  private constructor();
36
+ /** Replace the handler for a capability declared when this route opened. */
37
+ onRequest(methodFamily: string, handler: ReverseRequestHandler): void;
9
38
  private static create;
10
39
  }
11
40
  /** @internal SDK factory; not re-exported from the package surface. */
12
- export declare function createRouteHandle(channel: number, epoch: number, token: object): RouteHandle;
41
+ export declare function createRouteHandle(channel: number, epoch: number, token: object, registry?: ReverseRequestRegistry): RouteHandle;
42
+ /** @internal */
43
+ export declare function reverseRequestHandler(handle: RouteHandle, methodFamily: string): ReverseRequestHandler | undefined;
13
44
  /** A route handle belongs to another connection or is no longer installed. */
14
45
  export declare class StaleRouteHandleError extends Error {
15
46
  readonly handle: RouteHandle;
@@ -1 +1 @@
1
- {"version":3,"file":"route-handle.d.ts","sourceRoot":"","sources":["../src/route-handle.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,OAAO;IAaP,OAAO,CAAC,MAAM,CAAC,MAAM;CAGtB;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,CAK5F;AAED,8EAA8E;AAC9E,qBAAa,qBAAsB,SAAQ,KAAK;IAGlC,QAAQ,CAAC,MAAM,EAAE,WAAW;IAFxC,QAAQ,CAAC,IAAI,wBAAwB;gBAEhB,MAAM,EAAE,WAAW;CAIzC;AAED,gBAAgB;AAChB,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,gBAAgB;AAChB,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/E;AAED,gBAAgB;AAChB,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAE9E"}
1
+ {"version":3,"file":"route-handle.d.ts","sourceRoot":"","sources":["../src/route-handle.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,qBAAqB,KACvB,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC;AAEtC,gGAAgG;AAChG,qBAAa,+BAAgC,SAAQ,KAAK;IAG5C,QAAQ,CAAC,YAAY,EAAE,MAAM;IAFzC,QAAQ,CAAC,IAAI,6CAA6C;gBAErC,YAAY,EAAE,MAAM;CAI1C;AAED;;;;GAIG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;IACrE,OAAO,CAAC,gBAAgB,CAAoC;IAE5D,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAWrE,gBAAgB;IAChB,YAAY,IAAI,MAAM,EAAE;IAIxB,gBAAgB;IAChB,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS;IAIhE,gBAAgB;IAChB,IAAI,IAAI,IAAI;CAGb;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,OAAO;IAeP,4EAA4E;IAC5E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI;IAIrE,OAAO,CAAC,MAAM,CAAC,MAAM;CAQtB;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,QAAQ,yBAA+B,GACtC,WAAW,CAKb;AAED,gBAAgB;AAChB,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAElH;AAMD,8EAA8E;AAC9E,qBAAa,qBAAsB,SAAQ,KAAK;IAGlC,QAAQ,CAAC,MAAM,EAAE,WAAW;IAFxC,QAAQ,CAAC,IAAI,wBAAwB;gBAEhB,MAAM,EAAE,WAAW;CAIzC;AAED,gBAAgB;AAChB,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,gBAAgB;AAChB,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/E;AAED,gBAAgB;AAChB,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAE9E"}
@@ -1,4 +1,47 @@
1
1
  const connectionToken = new WeakMap();
2
+ const reverseRequests = new WeakMap();
3
+ /** A reverse-request registration cannot add a capability after route.open declared its set. */
4
+ export class ReverseRequestRegistrationError extends Error {
5
+ methodFamily;
6
+ code = "reverse_request_capability_not_declared";
7
+ constructor(methodFamily) {
8
+ super(`reverse-request capability ${JSON.stringify(methodFamily)} was not registered before route.open`);
9
+ this.methodFamily = methodFamily;
10
+ this.name = "ReverseRequestRegistrationError";
11
+ }
12
+ }
13
+ /**
14
+ * Mutable handler registry prepared before route.open. The client derives
15
+ * consumer_capabilities from its method families and seals that set when the
16
+ * route opens; handlers for declared families may still be replaced afterward.
17
+ */
18
+ export class ReverseRequestRegistry {
19
+ handlers = new Map();
20
+ declaredFamilies = null;
21
+ onRequest(methodFamily, handler) {
22
+ if (!isMethodFamily(methodFamily)) {
23
+ throw new TypeError(`reverse-request method family must be a capability identifier, got ${JSON.stringify(methodFamily)}`);
24
+ }
25
+ if (typeof handler !== "function")
26
+ throw new TypeError("reverse-request handler must be a function");
27
+ if (this.declaredFamilies && !this.declaredFamilies.has(methodFamily)) {
28
+ throw new ReverseRequestRegistrationError(methodFamily);
29
+ }
30
+ this.handlers.set(methodFamily, handler);
31
+ }
32
+ /** @internal */
33
+ capabilities() {
34
+ return [...this.handlers.keys()].sort();
35
+ }
36
+ /** @internal */
37
+ handler(methodFamily) {
38
+ return this.handlers.get(methodFamily);
39
+ }
40
+ /** @internal */
41
+ seal() {
42
+ this.declaredFamilies ??= new Set(this.handlers.keys());
43
+ }
44
+ }
2
45
  /**
3
46
  * Immutable identity for one route binding on one live socket. Only channel and
4
47
  * epoch cross the wire; the connection token remains private to this SDK.
@@ -6,7 +49,7 @@ const connectionToken = new WeakMap();
6
49
  export class RouteHandle {
7
50
  channel;
8
51
  epoch;
9
- constructor(channel, epoch, token) {
52
+ constructor(channel, epoch, token, registry) {
10
53
  if (!Number.isInteger(channel) || channel <= 0 || channel > 0xffff) {
11
54
  throw new RangeError(`route channel must be an integer in 1..65535, got ${channel}`);
12
55
  }
@@ -15,17 +58,30 @@ export class RouteHandle {
15
58
  }
16
59
  this.channel = channel;
17
60
  this.epoch = epoch;
61
+ registry.seal();
18
62
  connectionToken.set(this, token);
63
+ reverseRequests.set(this, registry);
19
64
  Object.freeze(this);
20
65
  }
21
- static create(channel, epoch, token) {
22
- return new RouteHandle(channel, epoch, token);
66
+ /** Replace the handler for a capability declared when this route opened. */
67
+ onRequest(methodFamily, handler) {
68
+ reverseRequests.get(this).onRequest(methodFamily, handler);
69
+ }
70
+ static create(channel, epoch, token, registry) {
71
+ return new RouteHandle(channel, epoch, token, registry);
23
72
  }
24
73
  }
25
74
  /** @internal SDK factory; not re-exported from the package surface. */
26
- export function createRouteHandle(channel, epoch, token) {
75
+ export function createRouteHandle(channel, epoch, token, registry = new ReverseRequestRegistry()) {
27
76
  const factory = RouteHandle;
28
- return factory.create(channel, epoch, token);
77
+ return factory.create(channel, epoch, token, registry);
78
+ }
79
+ /** @internal */
80
+ export function reverseRequestHandler(handle, methodFamily) {
81
+ return reverseRequests.get(handle)?.handler(methodFamily);
82
+ }
83
+ function isMethodFamily(value) {
84
+ return /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(value);
29
85
  }
30
86
  /** A route handle belongs to another connection or is no longer installed. */
31
87
  export class StaleRouteHandleError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"route-handle.js","sourceRoot":"","sources":["../src/route-handle.ts"],"names":[],"mappings":"AAAA,MAAM,eAAe,GAAG,IAAI,OAAO,EAAuB,CAAC;AAE3D;;;GAGG;AACH,MAAM,OAAO,WAAW;IACb,OAAO,CAAS;IAChB,KAAK,CAAS;IAEvB,YAAoB,OAAe,EAAE,KAAa,EAAE,KAAa;QAC/D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,OAAO,GAAG,MAAM,EAAE,CAAC;YACnE,MAAM,IAAI,UAAU,CAAC,qDAAqD,OAAO,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,WAAW,EAAE,CAAC;YAClE,MAAM,IAAI,UAAU,CAAC,wDAAwD,KAAK,EAAE,CAAC,CAAC;QACxF,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;IAEO,MAAM,CAAC,MAAM,CAAC,OAAe,EAAE,KAAa,EAAE,KAAa;QACjE,OAAO,IAAI,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAChD,CAAC;CACF;AAED,uEAAuE;AACvE,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,KAAa,EAAE,KAAa;IAC7E,MAAM,OAAO,GAAG,WAEf,CAAC;IACF,OAAO,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AAC/C,CAAC;AAED,8EAA8E;AAC9E,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAGzB;IAFZ,IAAI,GAAG,oBAAoB,CAAC;IAErC,YAAqB,MAAmB;QACtC,KAAK,CAAC,iBAAiB,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC,KAAK,yCAAyC,CAAC,CAAC;QAD9E,WAAM,GAAN,MAAM,CAAa;QAEtC,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,gBAAgB;AAChB,MAAM,UAAU,kBAAkB;IAChC,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,gBAAgB;AAChB,MAAM,UAAU,mBAAmB,CAAC,MAAmB,EAAE,KAAa;IACpE,OAAO,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC;AAC/C,CAAC;AAED,gBAAgB;AAChB,MAAM,UAAU,eAAe,CAAC,IAAiB,EAAE,KAAkB;IACnE,OAAO,IAAI,KAAK,KAAK,CAAC;AACxB,CAAC"}
1
+ {"version":3,"file":"route-handle.js","sourceRoot":"","sources":["../src/route-handle.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/subc-client",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "TypeScript client for the subc daemon. Wire-compatible (byte-for-byte) with the Rust subc-transport handshake and subc-protocol envelope.",
5
5
  "type": "module",
6
6
  "exports": {
package/src/client.ts CHANGED
@@ -22,12 +22,15 @@ import {
22
22
  FrameType,
23
23
  hasBinary,
24
24
  Priority,
25
+ SUBSCRIPTION_FLAG,
25
26
  type Frame,
26
27
  } from "./envelope.js";
27
28
  import {
28
29
  belongsToConnection,
29
30
  createRouteHandle,
30
31
  newConnectionToken,
32
+ reverseRequestHandler,
33
+ ReverseRequestRegistry,
31
34
  RouteHandle,
32
35
  sameRouteHandle,
33
36
  StaleRouteHandleError,
@@ -109,8 +112,11 @@ export interface ConsumerIdentity {
109
112
  export interface RouteOpenOptions {
110
113
  /** Optional override for the consumer identity; by default the SUBC_MODULE_ID and SUBC_LAUNCH_NONCE environment variables are used when both are non-empty. Set null to send route.open without consumer_identity. */
111
114
  consumerIdentity?: ConsumerIdentity | null;
112
- /** Optional consumer-declared reverse-request capabilities for this route.open. This is a declaration, not a verified privilege; providers must treat an omitted field as no reverse-request capability. Known MCP method-family values today are "elicitation", "sampling", and "roots". */
113
- consumerCapabilities?: string[];
115
+ /**
116
+ * Handlers prepared before route.open. The client derives consumer_capabilities
117
+ * from this registry; callers cannot declare a capability without a handler.
118
+ */
119
+ reverseRequests?: ReverseRequestRegistry;
114
120
  }
115
121
 
116
122
  export interface CatalogCapabilityRequirement {
@@ -193,6 +199,8 @@ export interface ManagedCallOptions extends RequestOptions {
193
199
  targetKind?: ManagedRouteKind;
194
200
  /** Optional override for the consumer identity; by default the SUBC_MODULE_ID and SUBC_LAUNCH_NONCE environment variables are used when both are non-empty. Set null to send route.open without consumer_identity. */
195
201
  consumerIdentity?: ConsumerIdentity | null;
202
+ /** Reverse-request handlers used by the managed route and all of its reconnect reopens. */
203
+ reverseRequests?: ReverseRequestRegistry;
196
204
  }
197
205
 
198
206
  export interface SubscribeOptions {
@@ -221,6 +229,8 @@ export interface CloseRouteOptions {
221
229
  export interface ManagedCloseRouteOptions extends CloseRouteOptions {
222
230
  /** Consumer identity used by the cached managed route. */
223
231
  consumerIdentity?: ConsumerIdentity | null;
232
+ /** The registry whose derived capability set identifies the cached route. */
233
+ reverseRequests?: ReverseRequestRegistry;
224
234
  }
225
235
 
226
236
  /**
@@ -468,6 +478,7 @@ interface CachedRoute {
468
478
  target: Extract<RouteTarget, { kind: ManagedRouteKind }>;
469
479
  identity: BindIdentity;
470
480
  consumerIdentity?: ConsumerIdentity;
481
+ reverseRequests: ReverseRequestRegistry;
471
482
  handle: RouteHandle | null;
472
483
  opening: Promise<RouteHandle> | null;
473
484
  /**
@@ -566,13 +577,15 @@ export class SubcClient {
566
577
  /** Open a route and return its connection-bound immutable handle. */
567
578
  async routeOpen(target: RouteTarget, identity: BindIdentity, opts: RouteOpenOptions = {}): Promise<RouteHandle> {
568
579
  const consumerIdentity = routeOpenConsumerIdentity(opts);
569
- const consumerCapabilities = opts.consumerCapabilities;
580
+ const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry();
581
+ reverseRequests.seal();
582
+ const consumerCapabilities = reverseRequests.capabilities();
570
583
  const body = this.encode({
571
584
  op: "route.open",
572
585
  target,
573
586
  identity,
574
587
  ...(consumerIdentity ? { consumer_identity: consumerIdentity } : {}),
575
- ...(consumerCapabilities !== undefined ? { consumer_capabilities: consumerCapabilities } : {}),
588
+ ...(consumerCapabilities.length > 0 ? { consumer_capabilities: consumerCapabilities } : {}),
576
589
  });
577
590
 
578
591
  let installed: RouteHandle | null = null;
@@ -582,7 +595,7 @@ export class SubcClient {
582
595
  if (typeof parsed.route_channel !== "number" || typeof parsed.route_epoch !== "number") {
583
596
  throw new SubcError(`route.open returned no route handle: ${JSON.stringify(parsed)}`);
584
597
  }
585
- installed = this.installRoute(parsed.route_channel, parsed.route_epoch);
598
+ installed = this.installRoute(parsed.route_channel, parsed.route_epoch, reverseRequests);
586
599
  return true;
587
600
  };
588
601
  const closeLateRoute = (frame: Frame): void => {
@@ -749,7 +762,7 @@ export class SubcClient {
749
762
  this.pending.set(key, subscriptionPending);
750
763
  const frame = buildFrame(
751
764
  FrameType.Request,
752
- buildFlags(false, priority, false, admission),
765
+ buildFlags(false, priority, false, admission) | SUBSCRIPTION_FLAG,
753
766
  handle.channel,
754
767
  handle.epoch,
755
768
  corr,
@@ -857,7 +870,12 @@ export class SubcClient {
857
870
  identity: BindIdentity,
858
871
  opts: ManagedCloseRouteOptions = {},
859
872
  ): Promise<void> {
860
- const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
873
+ const key = routeCacheKey(
874
+ target,
875
+ identity,
876
+ routeOpenConsumerIdentity(opts),
877
+ (opts.reverseRequests ?? new ReverseRequestRegistry()).capabilities(),
878
+ );
861
879
  const cached = this.routes.get(key);
862
880
  if (!cached) return;
863
881
  cached.closed = true;
@@ -1139,7 +1157,9 @@ export class SubcClient {
1139
1157
  { kind: ManagedRouteKind }
1140
1158
  >;
1141
1159
  const consumerIdentity = routeOpenConsumerIdentity(opts);
1142
- const key = routeCacheKey(target, identity, consumerIdentity);
1160
+ const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry();
1161
+ reverseRequests.seal();
1162
+ const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities());
1143
1163
  let cached = this.routes.get(key);
1144
1164
  if (!cached) {
1145
1165
  cached = {
@@ -1148,6 +1168,7 @@ export class SubcClient {
1148
1168
  target,
1149
1169
  identity,
1150
1170
  consumerIdentity,
1171
+ reverseRequests,
1151
1172
  handle: null,
1152
1173
  opening: null,
1153
1174
  };
@@ -1178,6 +1199,7 @@ export class SubcClient {
1178
1199
  try {
1179
1200
  const handle = await this.routeOpen(cached.target, cached.identity, {
1180
1201
  consumerIdentity: cached.consumerIdentity ?? null,
1202
+ reverseRequests: cached.reverseRequests,
1181
1203
  });
1182
1204
  if (cached.closed) {
1183
1205
  this.liveRoutes.delete(handle.channel);
@@ -1401,6 +1423,7 @@ export class SubcClient {
1401
1423
  try {
1402
1424
  const handle = await this.routeOpen(cached.target, cached.identity, {
1403
1425
  consumerIdentity: cached.consumerIdentity ?? null,
1426
+ reverseRequests: cached.reverseRequests,
1404
1427
  });
1405
1428
  if (cached.closed) {
1406
1429
  this.liveRoutes.delete(handle.channel);
@@ -1504,6 +1527,11 @@ export class SubcClient {
1504
1527
  }
1505
1528
  }
1506
1529
 
1530
+ if (frame.header.ty === FrameType.Request && handle) {
1531
+ this.dispatchReverseRequest(frame, handle);
1532
+ return;
1533
+ }
1534
+
1507
1535
  const key = pendingKey(handle, frame.header.corr);
1508
1536
  const pending = this.pending.get(key);
1509
1537
  if (pending) {
@@ -1607,6 +1635,109 @@ export class SubcClient {
1607
1635
  this.settle(key, pending, () => pending.reject(pending.classifyFailure?.(err) ?? err));
1608
1636
  }
1609
1637
 
1638
+ private dispatchReverseRequest(frame: Frame, handle: RouteHandle): void {
1639
+ const method = reverseRequestMethod(frame.body);
1640
+ const methodFamily = method?.split("/", 1)[0];
1641
+ const handler = methodFamily ? reverseRequestHandler(handle, methodFamily) : undefined;
1642
+ const sock = this.sock;
1643
+ const generation = this.generation;
1644
+
1645
+ // ONE CODE, TWO SITES, ON PURPOSE. `reverse_request_unhandled` is emitted
1646
+ // both here (no handler registered) and below (a handler that threw), with
1647
+ // different MESSAGES and the same CODE. That is a ruling, not an oversight:
1648
+ // the distinction between "nobody listening" and "the listener broke" is
1649
+ // ours to log, and every provider's decision is identical for both -- not
1650
+ // granted, retry later. A second code would be a second thing every
1651
+ // consumer's decoder must learn for a branch none of them takes. If one ever
1652
+ // needs it, the message carries it. (Settled with prefrontal, the lane's
1653
+ // first consumer, before anyone pinned on it.)
1654
+ //
1655
+ // What must NOT join them: a handler that RETURNS bytes declining the
1656
+ // request answers with an ordinary Response in the provider's own
1657
+ // vocabulary. Error means "the consumer did not answer", never "the consumer
1658
+ // said no" -- that separation is the reason this lane exists.
1659
+ const reply = async (): Promise<void> => {
1660
+ if (!handler || !method) {
1661
+ await this.sendReverseError(
1662
+ frame,
1663
+ handle,
1664
+ "reverse_request_unhandled",
1665
+ methodFamily
1666
+ ? `no reverse-request handler is registered for ${methodFamily}`
1667
+ : "reverse request has no valid method",
1668
+ sock,
1669
+ generation,
1670
+ );
1671
+ return;
1672
+ }
1673
+
1674
+ let body: Uint8Array;
1675
+ try {
1676
+ body = await handler(frame.body, { corr: frame.header.corr, method });
1677
+ if (!(body instanceof Uint8Array)) {
1678
+ throw new TypeError("reverse-request handler must return a Uint8Array");
1679
+ }
1680
+ } catch (error) {
1681
+ await this.sendReverseError(
1682
+ frame,
1683
+ handle,
1684
+ "reverse_request_unhandled",
1685
+ error instanceof Error ? error.message : String(error),
1686
+ sock,
1687
+ generation,
1688
+ );
1689
+ return;
1690
+ }
1691
+
1692
+ await this.sendReverseReply(frame, handle, FrameType.Response, body, sock, generation);
1693
+ };
1694
+
1695
+ void reply().catch((error) => {
1696
+ if (this.sock === sock && this.generation === generation && !this.closeStarted) {
1697
+ debug("failed to answer reverse request: %s", causeMessage(error));
1698
+ }
1699
+ });
1700
+ }
1701
+
1702
+ private async sendReverseError(
1703
+ request: Frame,
1704
+ handle: RouteHandle,
1705
+ code: string,
1706
+ message: string,
1707
+ sock: SubcSocket,
1708
+ generation: number,
1709
+ ): Promise<void> {
1710
+ await this.sendReverseReply(request, handle, FrameType.Error, this.encode({ code, message }), sock, generation);
1711
+ }
1712
+
1713
+ private async sendReverseReply(
1714
+ request: Frame,
1715
+ handle: RouteHandle,
1716
+ type: FrameType.Response | FrameType.Error,
1717
+ body: Uint8Array,
1718
+ sock: SubcSocket,
1719
+ generation: number,
1720
+ ): Promise<void> {
1721
+ if (
1722
+ this.sock !== sock ||
1723
+ this.generation !== generation ||
1724
+ !this.isLiveHandle(handle) ||
1725
+ this.closeStarted ||
1726
+ this.closedErr
1727
+ ) {
1728
+ return;
1729
+ }
1730
+ const frame = buildFrame(
1731
+ type,
1732
+ buildFlags(false, Priority.Interactive, false),
1733
+ handle.channel,
1734
+ handle.epoch,
1735
+ request.header.corr,
1736
+ body,
1737
+ );
1738
+ await writeBorrowed(sock, encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
1739
+ }
1740
+
1610
1741
  private errorFromFrame(frame: Frame): SubcError {
1611
1742
  try {
1612
1743
  const parsed = JSON.parse(Buffer.from(frame.body).toString("utf8")) as {
@@ -1663,8 +1794,12 @@ export class SubcClient {
1663
1794
  return this.ingressEpochDropCount;
1664
1795
  }
1665
1796
 
1666
- private installRoute(channel: number, epoch: number): RouteHandle {
1667
- const handle = createRouteHandle(channel, epoch, this.connectionToken);
1797
+ private installRoute(
1798
+ channel: number,
1799
+ epoch: number,
1800
+ reverseRequests = new ReverseRequestRegistry(),
1801
+ ): RouteHandle {
1802
+ const handle = createRouteHandle(channel, epoch, this.connectionToken, reverseRequests);
1668
1803
  this.liveRoutes.set(channel, handle);
1669
1804
  return handle;
1670
1805
  }
@@ -1813,12 +1948,14 @@ export function isValidCapabilityIdentifier(identifier: string): boolean {
1813
1948
  function routeCacheKey(
1814
1949
  target: Extract<RouteTarget, { kind: ManagedRouteKind }>,
1815
1950
  identity: BindIdentity,
1816
- consumerIdentity?: ConsumerIdentity,
1951
+ consumerIdentity: ConsumerIdentity | undefined,
1952
+ consumerCapabilities: readonly string[],
1817
1953
  ): string {
1818
1954
  const consumerPart = consumerIdentity
1819
1955
  ? `${consumerIdentity.module_id}\0${consumerIdentity.launch_nonce}`
1820
1956
  : "";
1821
- return `${target.kind}\0${target.module_id}\0${identity.project_root}\0${identity.harness}\0${identity.session}\0${consumerPart}`;
1957
+ const capabilitiesPart = [...consumerCapabilities].sort().join("\0");
1958
+ return `${target.kind}\0${target.module_id}\0${identity.project_root}\0${identity.harness}\0${identity.session}\0${consumerPart}\0${capabilitiesPart}`;
1822
1959
  }
1823
1960
 
1824
1961
  function routeOpenConsumerIdentity(opts: RouteOpenOptions = {}): ConsumerIdentity | undefined {
@@ -1845,3 +1982,12 @@ function causeMessage(cause: unknown): string {
1845
1982
  function pendingKey(handle: RouteHandle | null, corr: bigint): string {
1846
1983
  return handle ? `${handle.channel}:${handle.epoch}:${corr}` : `0:0:${corr}`;
1847
1984
  }
1985
+
1986
+ function reverseRequestMethod(body: Uint8Array): string | undefined {
1987
+ try {
1988
+ const parsed = JSON.parse(Buffer.from(body).toString("utf8")) as { method?: unknown };
1989
+ return typeof parsed.method === "string" && parsed.method.length > 0 ? parsed.method : undefined;
1990
+ } catch {
1991
+ return undefined;
1992
+ }
1993
+ }
package/src/envelope.ts CHANGED
@@ -51,7 +51,8 @@ const FLAG_LAST = 0b0000_1000;
51
51
  const FLAG_ADMISSION_MASK = 0b0011_0000;
52
52
  const FLAG_ADMISSION_SHIFT = 4;
53
53
  export const DAEMON_ORIGIN_FLAG = 0x40;
54
- const FLAG_RESERVED_MASK = 0b1000_0000;
54
+ export const SUBSCRIPTION_FLAG = 0x80;
55
+ const FLAG_RESERVED_MASK = 0;
55
56
 
56
57
  /** Build flags from typed components. Admission defaults to NORMAL. */
57
58
  export function buildFlags(
package/src/index.ts CHANGED
@@ -35,7 +35,14 @@ export {
35
35
  type RoutePollKind,
36
36
  type RoutePollResult,
37
37
  } from "./client.js";
38
- export { RouteHandle, StaleRouteHandleError } from "./route-handle.js";
38
+ export {
39
+ ReverseRequestRegistry,
40
+ ReverseRequestRegistrationError,
41
+ RouteHandle,
42
+ StaleRouteHandleError,
43
+ type ReverseRequestContext,
44
+ type ReverseRequestHandler,
45
+ } from "./route-handle.js";
39
46
  export {
40
47
  readConnectionFile,
41
48
  ConnectionFileError,
@@ -47,6 +54,7 @@ export {
47
54
  Priority,
48
55
  AdmissionClass,
49
56
  DAEMON_ORIGIN_FLAG,
57
+ SUBSCRIPTION_FLAG,
50
58
  PROTOCOL_VERSION,
51
59
  HEADER_LEN,
52
60
  buildFrame,
@@ -1,4 +1,61 @@
1
1
  const connectionToken = new WeakMap<RouteHandle, object>();
2
+ const reverseRequests = new WeakMap<RouteHandle, ReverseRequestRegistry>();
3
+
4
+ export interface ReverseRequestContext {
5
+ readonly corr: bigint;
6
+ readonly method: string;
7
+ }
8
+
9
+ export type ReverseRequestHandler = (
10
+ body: Uint8Array,
11
+ ctx: ReverseRequestContext,
12
+ ) => Promise<Uint8Array> | Uint8Array;
13
+
14
+ /** A reverse-request registration cannot add a capability after route.open declared its set. */
15
+ export class ReverseRequestRegistrationError extends Error {
16
+ readonly code = "reverse_request_capability_not_declared";
17
+
18
+ constructor(readonly methodFamily: string) {
19
+ super(`reverse-request capability ${JSON.stringify(methodFamily)} was not registered before route.open`);
20
+ this.name = "ReverseRequestRegistrationError";
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Mutable handler registry prepared before route.open. The client derives
26
+ * consumer_capabilities from its method families and seals that set when the
27
+ * route opens; handlers for declared families may still be replaced afterward.
28
+ */
29
+ export class ReverseRequestRegistry {
30
+ private readonly handlers = new Map<string, ReverseRequestHandler>();
31
+ private declaredFamilies: ReadonlySet<string> | null = null;
32
+
33
+ onRequest(methodFamily: string, handler: ReverseRequestHandler): void {
34
+ if (!isMethodFamily(methodFamily)) {
35
+ throw new TypeError(`reverse-request method family must be a capability identifier, got ${JSON.stringify(methodFamily)}`);
36
+ }
37
+ if (typeof handler !== "function") throw new TypeError("reverse-request handler must be a function");
38
+ if (this.declaredFamilies && !this.declaredFamilies.has(methodFamily)) {
39
+ throw new ReverseRequestRegistrationError(methodFamily);
40
+ }
41
+ this.handlers.set(methodFamily, handler);
42
+ }
43
+
44
+ /** @internal */
45
+ capabilities(): string[] {
46
+ return [...this.handlers.keys()].sort();
47
+ }
48
+
49
+ /** @internal */
50
+ handler(methodFamily: string): ReverseRequestHandler | undefined {
51
+ return this.handlers.get(methodFamily);
52
+ }
53
+
54
+ /** @internal */
55
+ seal(): void {
56
+ this.declaredFamilies ??= new Set(this.handlers.keys());
57
+ }
58
+ }
2
59
 
3
60
  /**
4
61
  * Immutable identity for one route binding on one live socket. Only channel and
@@ -8,7 +65,7 @@ export class RouteHandle {
8
65
  readonly channel: number;
9
66
  readonly epoch: number;
10
67
 
11
- private constructor(channel: number, epoch: number, token: object) {
68
+ private constructor(channel: number, epoch: number, token: object, registry: ReverseRequestRegistry) {
12
69
  if (!Number.isInteger(channel) || channel <= 0 || channel > 0xffff) {
13
70
  throw new RangeError(`route channel must be an integer in 1..65535, got ${channel}`);
14
71
  }
@@ -17,21 +74,47 @@ export class RouteHandle {
17
74
  }
18
75
  this.channel = channel;
19
76
  this.epoch = epoch;
77
+ registry.seal();
20
78
  connectionToken.set(this, token);
79
+ reverseRequests.set(this, registry);
21
80
  Object.freeze(this);
22
81
  }
23
82
 
24
- private static create(channel: number, epoch: number, token: object): RouteHandle {
25
- return new RouteHandle(channel, epoch, token);
83
+ /** Replace the handler for a capability declared when this route opened. */
84
+ onRequest(methodFamily: string, handler: ReverseRequestHandler): void {
85
+ reverseRequests.get(this)!.onRequest(methodFamily, handler);
86
+ }
87
+
88
+ private static create(
89
+ channel: number,
90
+ epoch: number,
91
+ token: object,
92
+ registry: ReverseRequestRegistry,
93
+ ): RouteHandle {
94
+ return new RouteHandle(channel, epoch, token, registry);
26
95
  }
27
96
  }
28
97
 
29
98
  /** @internal SDK factory; not re-exported from the package surface. */
30
- export function createRouteHandle(channel: number, epoch: number, token: object): RouteHandle {
99
+ export function createRouteHandle(
100
+ channel: number,
101
+ epoch: number,
102
+ token: object,
103
+ registry = new ReverseRequestRegistry(),
104
+ ): RouteHandle {
31
105
  const factory = RouteHandle as unknown as {
32
- create(channel: number, epoch: number, token: object): RouteHandle;
106
+ create(channel: number, epoch: number, token: object, registry: ReverseRequestRegistry): RouteHandle;
33
107
  };
34
- return factory.create(channel, epoch, token);
108
+ return factory.create(channel, epoch, token, registry);
109
+ }
110
+
111
+ /** @internal */
112
+ export function reverseRequestHandler(handle: RouteHandle, methodFamily: string): ReverseRequestHandler | undefined {
113
+ return reverseRequests.get(handle)?.handler(methodFamily);
114
+ }
115
+
116
+ function isMethodFamily(value: string): boolean {
117
+ return /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(value);
35
118
  }
36
119
 
37
120
  /** A route handle belongs to another connection or is no longer installed. */