@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 +1 -1
- package/src/client.ts +49 -7
- package/src/index.ts +5 -0
- package/src/provider.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/subc-client",
|
|
3
|
-
"version": "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
|
|
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
|
|
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(
|
|
889
|
-
|
|
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);
|