@cortexkit/subc-client 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","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,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,4BAA4B,EACjC,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,QAAQ,GACd,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,EACX,KAAK,eAAe,EACpB,KAAK,KAAK,EACV,KAAK,cAAc,GACpB,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,EACnB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,gCAAgC,EACrC,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,0BAA0B,EAC/B,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","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,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,4BAA4B,EACjC,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,+BAA+B,EAC/B,WAAW,EACX,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,QAAQ,GACd,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,EACX,KAAK,eAAe,EACpB,KAAK,KAAK,EACV,KAAK,cAAc,GACpB,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,EACnB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,gCAAgC,EACrC,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,0BAA0B,EAC/B,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,eAAe,GACrB,MAAM,eAAe,CAAC"}
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.1",
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": {
@@ -30,4 +30,4 @@
30
30
  "bun-types": "^1.3.14",
31
31
  "typescript": "^5.6.0"
32
32
  }
33
- }
33
+ }
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 => {
@@ -747,6 +760,20 @@ export class SubcClient {
747
760
  subscription: true,
748
761
  };
749
762
  this.pending.set(key, subscriptionPending);
763
+ // BIT 7 IS NOT EMITTED YET, DELIBERATELY. `SUBSCRIPTION_FLAG` is allocated
764
+ // and the daemon reads it, but every decoder built against subc-protocol
765
+ // <= 0.20.0 treats bit 7 as a RESERVED-BIT TRIPWIRE and REFUSES the frame
766
+ // rather than ignoring a bit it does not know. The splice forwards the
767
+ // flags byte verbatim, so the rejection lands at the far end, inside a
768
+ // module whose author opted into nothing, as a decode error on a frame the
769
+ // daemon considered well-formed.
770
+ //
771
+ // Restoring this line is PHASE 2 of a two-phase fleet operation and must
772
+ // not happen until Phase 1 is done and CENSUSED: every module linking a
773
+ // subc-protocol whose decoder ACCEPTS bit 7. Bit 6 (DAEMON_ORIGIN) was
774
+ // done exactly this way, in separate PRs, for exactly this reason.
775
+ //
776
+ // ... | SUBSCRIPTION_FLAG, <- Phase 2, not before the census
750
777
  const frame = buildFrame(
751
778
  FrameType.Request,
752
779
  buildFlags(false, priority, false, admission),
@@ -857,7 +884,12 @@ export class SubcClient {
857
884
  identity: BindIdentity,
858
885
  opts: ManagedCloseRouteOptions = {},
859
886
  ): Promise<void> {
860
- const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
887
+ const key = routeCacheKey(
888
+ target,
889
+ identity,
890
+ routeOpenConsumerIdentity(opts),
891
+ (opts.reverseRequests ?? new ReverseRequestRegistry()).capabilities(),
892
+ );
861
893
  const cached = this.routes.get(key);
862
894
  if (!cached) return;
863
895
  cached.closed = true;
@@ -1139,7 +1171,9 @@ export class SubcClient {
1139
1171
  { kind: ManagedRouteKind }
1140
1172
  >;
1141
1173
  const consumerIdentity = routeOpenConsumerIdentity(opts);
1142
- const key = routeCacheKey(target, identity, consumerIdentity);
1174
+ const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry();
1175
+ reverseRequests.seal();
1176
+ const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities());
1143
1177
  let cached = this.routes.get(key);
1144
1178
  if (!cached) {
1145
1179
  cached = {
@@ -1148,6 +1182,7 @@ export class SubcClient {
1148
1182
  target,
1149
1183
  identity,
1150
1184
  consumerIdentity,
1185
+ reverseRequests,
1151
1186
  handle: null,
1152
1187
  opening: null,
1153
1188
  };
@@ -1178,6 +1213,7 @@ export class SubcClient {
1178
1213
  try {
1179
1214
  const handle = await this.routeOpen(cached.target, cached.identity, {
1180
1215
  consumerIdentity: cached.consumerIdentity ?? null,
1216
+ reverseRequests: cached.reverseRequests,
1181
1217
  });
1182
1218
  if (cached.closed) {
1183
1219
  this.liveRoutes.delete(handle.channel);
@@ -1401,6 +1437,7 @@ export class SubcClient {
1401
1437
  try {
1402
1438
  const handle = await this.routeOpen(cached.target, cached.identity, {
1403
1439
  consumerIdentity: cached.consumerIdentity ?? null,
1440
+ reverseRequests: cached.reverseRequests,
1404
1441
  });
1405
1442
  if (cached.closed) {
1406
1443
  this.liveRoutes.delete(handle.channel);
@@ -1504,6 +1541,11 @@ export class SubcClient {
1504
1541
  }
1505
1542
  }
1506
1543
 
1544
+ if (frame.header.ty === FrameType.Request && handle) {
1545
+ this.dispatchReverseRequest(frame, handle);
1546
+ return;
1547
+ }
1548
+
1507
1549
  const key = pendingKey(handle, frame.header.corr);
1508
1550
  const pending = this.pending.get(key);
1509
1551
  if (pending) {
@@ -1607,6 +1649,109 @@ export class SubcClient {
1607
1649
  this.settle(key, pending, () => pending.reject(pending.classifyFailure?.(err) ?? err));
1608
1650
  }
1609
1651
 
1652
+ private dispatchReverseRequest(frame: Frame, handle: RouteHandle): void {
1653
+ const method = reverseRequestMethod(frame.body);
1654
+ const methodFamily = method?.split("/", 1)[0];
1655
+ const handler = methodFamily ? reverseRequestHandler(handle, methodFamily) : undefined;
1656
+ const sock = this.sock;
1657
+ const generation = this.generation;
1658
+
1659
+ // ONE CODE, TWO SITES, ON PURPOSE. `reverse_request_unhandled` is emitted
1660
+ // both here (no handler registered) and below (a handler that threw), with
1661
+ // different MESSAGES and the same CODE. That is a ruling, not an oversight:
1662
+ // the distinction between "nobody listening" and "the listener broke" is
1663
+ // ours to log, and every provider's decision is identical for both -- not
1664
+ // granted, retry later. A second code would be a second thing every
1665
+ // consumer's decoder must learn for a branch none of them takes. If one ever
1666
+ // needs it, the message carries it. (Settled with prefrontal, the lane's
1667
+ // first consumer, before anyone pinned on it.)
1668
+ //
1669
+ // What must NOT join them: a handler that RETURNS bytes declining the
1670
+ // request answers with an ordinary Response in the provider's own
1671
+ // vocabulary. Error means "the consumer did not answer", never "the consumer
1672
+ // said no" -- that separation is the reason this lane exists.
1673
+ const reply = async (): Promise<void> => {
1674
+ if (!handler || !method) {
1675
+ await this.sendReverseError(
1676
+ frame,
1677
+ handle,
1678
+ "reverse_request_unhandled",
1679
+ methodFamily
1680
+ ? `no reverse-request handler is registered for ${methodFamily}`
1681
+ : "reverse request has no valid method",
1682
+ sock,
1683
+ generation,
1684
+ );
1685
+ return;
1686
+ }
1687
+
1688
+ let body: Uint8Array;
1689
+ try {
1690
+ body = await handler(frame.body, { corr: frame.header.corr, method });
1691
+ if (!(body instanceof Uint8Array)) {
1692
+ throw new TypeError("reverse-request handler must return a Uint8Array");
1693
+ }
1694
+ } catch (error) {
1695
+ await this.sendReverseError(
1696
+ frame,
1697
+ handle,
1698
+ "reverse_request_unhandled",
1699
+ error instanceof Error ? error.message : String(error),
1700
+ sock,
1701
+ generation,
1702
+ );
1703
+ return;
1704
+ }
1705
+
1706
+ await this.sendReverseReply(frame, handle, FrameType.Response, body, sock, generation);
1707
+ };
1708
+
1709
+ void reply().catch((error) => {
1710
+ if (this.sock === sock && this.generation === generation && !this.closeStarted) {
1711
+ debug("failed to answer reverse request: %s", causeMessage(error));
1712
+ }
1713
+ });
1714
+ }
1715
+
1716
+ private async sendReverseError(
1717
+ request: Frame,
1718
+ handle: RouteHandle,
1719
+ code: string,
1720
+ message: string,
1721
+ sock: SubcSocket,
1722
+ generation: number,
1723
+ ): Promise<void> {
1724
+ await this.sendReverseReply(request, handle, FrameType.Error, this.encode({ code, message }), sock, generation);
1725
+ }
1726
+
1727
+ private async sendReverseReply(
1728
+ request: Frame,
1729
+ handle: RouteHandle,
1730
+ type: FrameType.Response | FrameType.Error,
1731
+ body: Uint8Array,
1732
+ sock: SubcSocket,
1733
+ generation: number,
1734
+ ): Promise<void> {
1735
+ if (
1736
+ this.sock !== sock ||
1737
+ this.generation !== generation ||
1738
+ !this.isLiveHandle(handle) ||
1739
+ this.closeStarted ||
1740
+ this.closedErr
1741
+ ) {
1742
+ return;
1743
+ }
1744
+ const frame = buildFrame(
1745
+ type,
1746
+ buildFlags(false, Priority.Interactive, false),
1747
+ handle.channel,
1748
+ handle.epoch,
1749
+ request.header.corr,
1750
+ body,
1751
+ );
1752
+ await writeBorrowed(sock, encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
1753
+ }
1754
+
1610
1755
  private errorFromFrame(frame: Frame): SubcError {
1611
1756
  try {
1612
1757
  const parsed = JSON.parse(Buffer.from(frame.body).toString("utf8")) as {
@@ -1663,8 +1808,12 @@ export class SubcClient {
1663
1808
  return this.ingressEpochDropCount;
1664
1809
  }
1665
1810
 
1666
- private installRoute(channel: number, epoch: number): RouteHandle {
1667
- const handle = createRouteHandle(channel, epoch, this.connectionToken);
1811
+ private installRoute(
1812
+ channel: number,
1813
+ epoch: number,
1814
+ reverseRequests = new ReverseRequestRegistry(),
1815
+ ): RouteHandle {
1816
+ const handle = createRouteHandle(channel, epoch, this.connectionToken, reverseRequests);
1668
1817
  this.liveRoutes.set(channel, handle);
1669
1818
  return handle;
1670
1819
  }
@@ -1813,12 +1962,14 @@ export function isValidCapabilityIdentifier(identifier: string): boolean {
1813
1962
  function routeCacheKey(
1814
1963
  target: Extract<RouteTarget, { kind: ManagedRouteKind }>,
1815
1964
  identity: BindIdentity,
1816
- consumerIdentity?: ConsumerIdentity,
1965
+ consumerIdentity: ConsumerIdentity | undefined,
1966
+ consumerCapabilities: readonly string[],
1817
1967
  ): string {
1818
1968
  const consumerPart = consumerIdentity
1819
1969
  ? `${consumerIdentity.module_id}\0${consumerIdentity.launch_nonce}`
1820
1970
  : "";
1821
- return `${target.kind}\0${target.module_id}\0${identity.project_root}\0${identity.harness}\0${identity.session}\0${consumerPart}`;
1971
+ const capabilitiesPart = [...consumerCapabilities].sort().join("\0");
1972
+ return `${target.kind}\0${target.module_id}\0${identity.project_root}\0${identity.harness}\0${identity.session}\0${consumerPart}\0${capabilitiesPart}`;
1822
1973
  }
1823
1974
 
1824
1975
  function routeOpenConsumerIdentity(opts: RouteOpenOptions = {}): ConsumerIdentity | undefined {
@@ -1845,3 +1996,12 @@ function causeMessage(cause: unknown): string {
1845
1996
  function pendingKey(handle: RouteHandle | null, corr: bigint): string {
1846
1997
  return handle ? `${handle.channel}:${handle.epoch}:${corr}` : `0:0:${corr}`;
1847
1998
  }
1999
+
2000
+ function reverseRequestMethod(body: Uint8Array): string | undefined {
2001
+ try {
2002
+ const parsed = JSON.parse(Buffer.from(body).toString("utf8")) as { method?: unknown };
2003
+ return typeof parsed.method === "string" && parsed.method.length > 0 ? parsed.method : undefined;
2004
+ } catch {
2005
+ return undefined;
2006
+ }
2007
+ }
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,