@cortexkit/subc-client 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/subc-client",
3
- "version": "0.1.0",
3
+ "version": "0.2.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
@@ -38,6 +38,8 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
38
38
  const BODY_READ_TIMEOUT_MS = 30_000;
39
39
  const EMPTY_BODY = new Uint8Array(0);
40
40
  const DEFAULT_MANAGED_TARGET_KIND: ManagedRouteKind = "management_surface";
41
+ export const SUBC_MODULE_ID_ENV = "SUBC_MODULE_ID";
42
+ export const SUBC_LAUNCH_NONCE_ENV = "SUBC_LAUNCH_NONCE";
41
43
 
42
44
  export interface BindIdentity {
43
45
  project_root: string;
@@ -52,6 +54,16 @@ export type RouteTarget =
52
54
 
53
55
  export type ManagedRouteKind = "management_surface" | "tool_provider";
54
56
 
57
+ export interface ConsumerIdentity {
58
+ module_id: string;
59
+ launch_nonce: string;
60
+ }
61
+
62
+ export interface RouteOpenOptions {
63
+ /** 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. */
64
+ consumerIdentity?: ConsumerIdentity | null;
65
+ }
66
+
55
67
  export interface CatalogEntry {
56
68
  module_id: string;
57
69
  roles: unknown[];
@@ -70,6 +82,8 @@ export interface ManagedCallOptions extends RequestOptions {
70
82
  identity?: BindIdentity;
71
83
  /** Defaults to management_surface, matching the store/host management APIs. */
72
84
  targetKind?: ManagedRouteKind;
85
+ /** 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. */
86
+ consumerIdentity?: ConsumerIdentity | null;
73
87
  }
74
88
 
75
89
  export interface SubscribeOptions {
@@ -83,6 +97,8 @@ export interface CloseRouteOptions {
83
97
  * Defaults to false: close immediately, aborting everything in flight.
84
98
  */
85
99
  drain?: boolean;
100
+ /** Consumer identity to use when looking up the route to close; only needed for routes opened with a non-default consumer identity. */
101
+ consumerIdentity?: ConsumerIdentity | null;
86
102
  }
87
103
 
88
104
  /**
@@ -199,6 +215,7 @@ interface CachedRoute {
199
215
  moduleId: string;
200
216
  target: Extract<RouteTarget, { kind: ManagedRouteKind }>;
201
217
  identity: BindIdentity;
218
+ consumerIdentity?: ConsumerIdentity;
202
219
  channel: number | null;
203
220
  generation: number;
204
221
  opening: Promise<number> | null;
@@ -252,8 +269,14 @@ export class SubcClient {
252
269
  }
253
270
 
254
271
  /** Open a route to a provider (channel-0 route.open); returns the route channel. */
255
- async routeOpen(target: RouteTarget, identity: BindIdentity): Promise<number> {
256
- const body = this.encode({ op: "route.open", target, identity });
272
+ async routeOpen(target: RouteTarget, identity: BindIdentity, opts: RouteOpenOptions = {}): Promise<number> {
273
+ const consumerIdentity = routeOpenConsumerIdentity(opts);
274
+ const body = this.encode({
275
+ op: "route.open",
276
+ target,
277
+ identity,
278
+ ...(consumerIdentity ? { consumer_identity: consumerIdentity } : {}),
279
+ });
257
280
  const reply = await this.controlRpc(body);
258
281
  const parsed = this.parseJson(reply) as { op: string; route_channel?: number };
259
282
  if (typeof parsed.route_channel !== "number") {
@@ -380,7 +403,7 @@ export class SubcClient {
380
403
  identity: BindIdentity,
381
404
  opts: CloseRouteOptions = {},
382
405
  ): Promise<void> {
383
- const key = routeCacheKey(target, identity);
406
+ const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
384
407
  const cached = this.routes.get(key);
385
408
  if (!cached) return; // never opened / already closed — idempotent no-op.
386
409
  // Generation guard: an in-flight openCachedRoute holds this same object and
@@ -590,7 +613,8 @@ export class SubcClient {
590
613
  RouteTarget,
591
614
  { kind: ManagedRouteKind }
592
615
  >;
593
- const key = routeCacheKey(target, identity);
616
+ const consumerIdentity = routeOpenConsumerIdentity(opts);
617
+ const key = routeCacheKey(target, identity, consumerIdentity);
594
618
  let cached = this.routes.get(key);
595
619
  if (!cached) {
596
620
  cached = {
@@ -598,6 +622,7 @@ export class SubcClient {
598
622
  moduleId,
599
623
  target,
600
624
  identity,
625
+ consumerIdentity,
601
626
  channel: null,
602
627
  generation: 0,
603
628
  opening: null,
@@ -630,7 +655,9 @@ export class SubcClient {
630
655
  }
631
656
 
632
657
  try {
633
- const channel = await this.routeOpen(cached.target, cached.identity);
658
+ const channel = await this.routeOpen(cached.target, cached.identity, {
659
+ consumerIdentity: cached.consumerIdentity ?? null,
660
+ });
634
661
  // Generation guard: a closeRoute may have flipped the tombstone WHILE this
635
662
  // route.open was in flight. If so, close wins — do NOT install the channel
636
663
  // into the (already-removed) cache entry; GOODBYE the channel we just opened
@@ -885,8 +912,23 @@ function normalizeConnectOptions(opts: ConnectOptions): NormalizedConnectOptions
885
912
  };
886
913
  }
887
914
 
888
- function routeCacheKey(target: Extract<RouteTarget, { kind: ManagedRouteKind }>, identity: BindIdentity): string {
889
- return `${target.kind}\0${target.module_id}\0${identity.project_root}\0${identity.harness}\0${identity.session}`;
915
+ function routeCacheKey(
916
+ target: Extract<RouteTarget, { kind: ManagedRouteKind }>,
917
+ identity: BindIdentity,
918
+ consumerIdentity?: ConsumerIdentity,
919
+ ): string {
920
+ const consumerPart = consumerIdentity
921
+ ? `${consumerIdentity.module_id}\0${consumerIdentity.launch_nonce}`
922
+ : "";
923
+ return `${target.kind}\0${target.module_id}\0${identity.project_root}\0${identity.harness}\0${identity.session}\0${consumerPart}`;
924
+ }
925
+
926
+ function routeOpenConsumerIdentity(opts: RouteOpenOptions = {}): ConsumerIdentity | undefined {
927
+ if (opts.consumerIdentity !== undefined) return opts.consumerIdentity ?? undefined;
928
+ const moduleId = process.env[SUBC_MODULE_ID_ENV];
929
+ const launchNonce = process.env[SUBC_LAUNCH_NONCE_ENV];
930
+ if (!moduleId || !launchNonce) return undefined;
931
+ return { module_id: moduleId, launch_nonce: launchNonce };
890
932
  }
891
933
 
892
934
  function errorCode(err: unknown): string | undefined {
package/src/index.ts CHANGED
@@ -3,11 +3,15 @@ export {
3
3
  SubcError,
4
4
  SubcCallError,
5
5
  DEFAULT_RECONNECT_BACKOFF,
6
+ SUBC_MODULE_ID_ENV,
7
+ SUBC_LAUNCH_NONCE_ENV,
6
8
  isConsumerReconnectTransient,
7
9
  connectionFileExists,
8
10
  type BindIdentity,
9
11
  type RouteTarget,
10
12
  type ManagedRouteKind,
13
+ type ConsumerIdentity,
14
+ type RouteOpenOptions,
11
15
  type CatalogEntry,
12
16
  type RequestOptions,
13
17
  type ManagedCallOptions,
@@ -57,6 +61,7 @@ export {
57
61
  jsonProviderHandler,
58
62
  type ProviderRequestContext,
59
63
  type BindDecision,
64
+ type Principal,
60
65
  type BindingsInput,
61
66
  type CircuitBreakerInput,
62
67
  type Concurrency,
package/src/provider.ts CHANGED
@@ -202,10 +202,16 @@ export type ProviderHandler = (
202
202
  ctx: ProviderRequestContext,
203
203
  ) => Promise<Uint8Array | void> | Uint8Array | void;
204
204
 
205
+ export type Principal =
206
+ | { kind: "reserved"; module_id: string }
207
+ | { kind: "direct" }
208
+ | { kind: "unverified" };
209
+
205
210
  export interface RouteBindRequest {
206
211
  route_channel: number;
207
212
  target: RouteTarget;
208
213
  identity: BindIdentity;
214
+ principal?: Principal;
209
215
  }
210
216
 
211
217
  export type BindDecision =
@@ -533,6 +539,7 @@ export class SubcProvider {
533
539
  route_channel: numberField(request.route_channel, "route_channel"),
534
540
  target: request.target as RouteTarget,
535
541
  identity: request.identity as BindIdentity,
542
+ principal: request.principal as Principal | undefined,
536
543
  };
537
544
 
538
545
  const decision = await this.opts.onBind?.(bindRequest);