@hasna/switcher 0.1.3 → 0.1.5
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/README.md +15 -6
- package/dist/cli/index.js +279 -253
- package/dist/credentials.d.ts +71 -8
- package/dist/domain.d.ts +1 -1
- package/dist/index.js +45 -6
- package/dist/mcp/index.js +47 -8
- package/dist/provider-stream.d.ts +17 -0
- package/dist/sdk.d.ts +10 -1
- package/dist/sdk.js +45 -6
- package/dist/serve/index.js +2 -2
- package/openapi.json +1 -1
- package/package.json +1 -1
package/dist/credentials.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { type CredentialChainOptions } from "@hasna/contracts/client";
|
|
2
3
|
import { CommandInterrupted, type ProviderInput } from "./domain";
|
|
3
4
|
declare const keychain: z.ZodObject<{
|
|
4
5
|
kind: z.ZodLiteral<"keychain">;
|
|
@@ -13,7 +14,7 @@ declare const keychain: z.ZodObject<{
|
|
|
13
14
|
service: string;
|
|
14
15
|
account: string;
|
|
15
16
|
}>;
|
|
16
|
-
export declare const credentialBindingSchema: z.ZodObject<{
|
|
17
|
+
export declare const credentialBindingSchema: z.ZodEffects<z.ZodObject<{
|
|
17
18
|
schema: z.ZodLiteral<1>;
|
|
18
19
|
credentialEnv: z.ZodString;
|
|
19
20
|
origins: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
|
|
@@ -32,9 +33,15 @@ export declare const credentialBindingSchema: z.ZodObject<{
|
|
|
32
33
|
}>, z.ZodObject<{
|
|
33
34
|
kind: z.ZodLiteral<"vault">;
|
|
34
35
|
key: z.ZodString;
|
|
35
|
-
url: z.ZodEffects<z.ZodString, string, string
|
|
36
|
+
url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
36
37
|
executable: z.ZodEffects<z.ZodString, string, string>;
|
|
37
38
|
operator: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
39
|
+
kind: z.ZodLiteral<"contracts">;
|
|
40
|
+
}, "strict", z.ZodTypeAny, {
|
|
41
|
+
kind: "contracts";
|
|
42
|
+
}, {
|
|
43
|
+
kind: "contracts";
|
|
44
|
+
}>, z.ZodObject<{
|
|
38
45
|
kind: z.ZodLiteral<"env">;
|
|
39
46
|
}, "strict", z.ZodTypeAny, {
|
|
40
47
|
kind: "env";
|
|
@@ -42,7 +49,7 @@ export declare const credentialBindingSchema: z.ZodObject<{
|
|
|
42
49
|
kind: "env";
|
|
43
50
|
}>, z.ZodObject<{
|
|
44
51
|
kind: z.ZodLiteral<"keychain">;
|
|
45
|
-
account: z.ZodString
|
|
52
|
+
account: z.ZodEffects<z.ZodString, string, string>;
|
|
46
53
|
}, "strict", z.ZodTypeAny, {
|
|
47
54
|
kind: "keychain";
|
|
48
55
|
account: string;
|
|
@@ -51,27 +58,31 @@ export declare const credentialBindingSchema: z.ZodObject<{
|
|
|
51
58
|
account: string;
|
|
52
59
|
}>]>;
|
|
53
60
|
}, "strict", z.ZodTypeAny, {
|
|
54
|
-
url: string;
|
|
55
61
|
key: string;
|
|
56
62
|
executable: string;
|
|
57
63
|
kind: "vault";
|
|
58
64
|
operator: {
|
|
65
|
+
kind: "contracts";
|
|
66
|
+
} | {
|
|
59
67
|
kind: "env";
|
|
60
68
|
} | {
|
|
61
69
|
kind: "keychain";
|
|
62
70
|
account: string;
|
|
63
71
|
};
|
|
72
|
+
url?: string | undefined;
|
|
64
73
|
}, {
|
|
65
|
-
url: string;
|
|
66
74
|
key: string;
|
|
67
75
|
executable: string;
|
|
68
76
|
kind: "vault";
|
|
69
77
|
operator: {
|
|
78
|
+
kind: "contracts";
|
|
79
|
+
} | {
|
|
70
80
|
kind: "env";
|
|
71
81
|
} | {
|
|
72
82
|
kind: "keychain";
|
|
73
83
|
account: string;
|
|
74
84
|
};
|
|
85
|
+
url?: string | undefined;
|
|
75
86
|
}>]>;
|
|
76
87
|
}, "strict", z.ZodTypeAny, {
|
|
77
88
|
credentialEnv: string;
|
|
@@ -80,16 +91,18 @@ export declare const credentialBindingSchema: z.ZodObject<{
|
|
|
80
91
|
service: string;
|
|
81
92
|
account: string;
|
|
82
93
|
} | {
|
|
83
|
-
url: string;
|
|
84
94
|
key: string;
|
|
85
95
|
executable: string;
|
|
86
96
|
kind: "vault";
|
|
87
97
|
operator: {
|
|
98
|
+
kind: "contracts";
|
|
99
|
+
} | {
|
|
88
100
|
kind: "env";
|
|
89
101
|
} | {
|
|
90
102
|
kind: "keychain";
|
|
91
103
|
account: string;
|
|
92
104
|
};
|
|
105
|
+
url?: string | undefined;
|
|
93
106
|
};
|
|
94
107
|
schema: 1;
|
|
95
108
|
origins: string[];
|
|
@@ -100,16 +113,62 @@ export declare const credentialBindingSchema: z.ZodObject<{
|
|
|
100
113
|
service: string;
|
|
101
114
|
account: string;
|
|
102
115
|
} | {
|
|
103
|
-
url: string;
|
|
104
116
|
key: string;
|
|
105
117
|
executable: string;
|
|
106
118
|
kind: "vault";
|
|
107
119
|
operator: {
|
|
120
|
+
kind: "contracts";
|
|
121
|
+
} | {
|
|
108
122
|
kind: "env";
|
|
109
123
|
} | {
|
|
110
124
|
kind: "keychain";
|
|
111
125
|
account: string;
|
|
112
126
|
};
|
|
127
|
+
url?: string | undefined;
|
|
128
|
+
};
|
|
129
|
+
schema: 1;
|
|
130
|
+
origins: string[];
|
|
131
|
+
}>, {
|
|
132
|
+
credentialEnv: string;
|
|
133
|
+
source: {
|
|
134
|
+
kind: "keychain";
|
|
135
|
+
service: string;
|
|
136
|
+
account: string;
|
|
137
|
+
} | {
|
|
138
|
+
key: string;
|
|
139
|
+
executable: string;
|
|
140
|
+
kind: "vault";
|
|
141
|
+
operator: {
|
|
142
|
+
kind: "contracts";
|
|
143
|
+
} | {
|
|
144
|
+
kind: "env";
|
|
145
|
+
} | {
|
|
146
|
+
kind: "keychain";
|
|
147
|
+
account: string;
|
|
148
|
+
};
|
|
149
|
+
url?: string | undefined;
|
|
150
|
+
};
|
|
151
|
+
schema: 1;
|
|
152
|
+
origins: string[];
|
|
153
|
+
}, {
|
|
154
|
+
credentialEnv: string;
|
|
155
|
+
source: {
|
|
156
|
+
kind: "keychain";
|
|
157
|
+
service: string;
|
|
158
|
+
account: string;
|
|
159
|
+
} | {
|
|
160
|
+
key: string;
|
|
161
|
+
executable: string;
|
|
162
|
+
kind: "vault";
|
|
163
|
+
operator: {
|
|
164
|
+
kind: "contracts";
|
|
165
|
+
} | {
|
|
166
|
+
kind: "env";
|
|
167
|
+
} | {
|
|
168
|
+
kind: "keychain";
|
|
169
|
+
account: string;
|
|
170
|
+
};
|
|
171
|
+
url?: string | undefined;
|
|
113
172
|
};
|
|
114
173
|
schema: 1;
|
|
115
174
|
origins: string[];
|
|
@@ -130,16 +189,18 @@ export declare class CredentialBindings {
|
|
|
130
189
|
service: string;
|
|
131
190
|
account: string;
|
|
132
191
|
} | {
|
|
133
|
-
url: string;
|
|
134
192
|
key: string;
|
|
135
193
|
executable: string;
|
|
136
194
|
kind: "vault";
|
|
137
195
|
operator: {
|
|
196
|
+
kind: "contracts";
|
|
197
|
+
} | {
|
|
138
198
|
kind: "env";
|
|
139
199
|
} | {
|
|
140
200
|
kind: "keychain";
|
|
141
201
|
account: string;
|
|
142
202
|
};
|
|
203
|
+
url?: string | undefined;
|
|
143
204
|
};
|
|
144
205
|
schema: 1;
|
|
145
206
|
origins: string[];
|
|
@@ -173,6 +234,8 @@ export declare class CredentialResolver {
|
|
|
173
234
|
export declare class CredentialInterrupted extends CommandInterrupted {
|
|
174
235
|
constructor(exitCode: number);
|
|
175
236
|
}
|
|
237
|
+
/** Select an operator through the shared credential seam, then pin that choice. */
|
|
238
|
+
export declare function vaultEnvironment(binding: CredentialBinding, env: NodeJS.ProcessEnv, options?: Pick<CredentialChainOptions, "keychain">): Promise<NodeJS.ProcessEnv>;
|
|
176
239
|
/** Internal child mode: the key travels only through an authenticated loopback request. */
|
|
177
240
|
export declare function deliverVaultCredential(): Promise<void>;
|
|
178
241
|
export {};
|
package/dist/domain.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type RoutingEvent } from "./model-policy-schema";
|
|
|
3
3
|
export { modelPolicySchema, routingEventSchema, routingEventsSchema } from "./model-policy-schema";
|
|
4
4
|
export type { ModelPolicy, RoutingEvent } from "./model-policy-schema";
|
|
5
5
|
export type { AuthStyle } from "./auth";
|
|
6
|
-
export declare const VERSION = "0.1.
|
|
6
|
+
export declare const VERSION = "0.1.5";
|
|
7
7
|
export declare const harnessSchema: z.ZodEnum<["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]>;
|
|
8
8
|
export declare const protocolSchema: z.ZodEnum<["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]>;
|
|
9
9
|
export declare const idSchema: z.ZodString;
|
package/dist/index.js
CHANGED
|
@@ -200,7 +200,7 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
// src/sdk.ts
|
|
203
|
-
import {
|
|
203
|
+
import { createClientTransport } from "@hasna/contracts/client";
|
|
204
204
|
|
|
205
205
|
// src/presets.ts
|
|
206
206
|
var route = (protocol, baseUrl, options = {}) => ({
|
|
@@ -361,6 +361,7 @@ function apiError(status, data, apiKey) {
|
|
|
361
361
|
|
|
362
362
|
class SwitcherClient {
|
|
363
363
|
options;
|
|
364
|
+
transport;
|
|
364
365
|
baseUrl;
|
|
365
366
|
constructor(options) {
|
|
366
367
|
this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
|
|
@@ -368,9 +369,50 @@ class SwitcherClient {
|
|
|
368
369
|
throw new Error("Switcher API key is required.");
|
|
369
370
|
this.options = { ...options };
|
|
370
371
|
}
|
|
372
|
+
static fromEnvironment(env = process.env, options = {}) {
|
|
373
|
+
const shared = createClientTransport("switcher", env, {
|
|
374
|
+
credentials: options.credentials,
|
|
375
|
+
timeoutMs: options.timeoutMs ?? 120000,
|
|
376
|
+
retry: false,
|
|
377
|
+
fetchImpl: async (input, init) => {
|
|
378
|
+
const address = String(input).replace(/\/v1\/(health|ready|version)$/, "/$1");
|
|
379
|
+
const key = new Headers(init?.headers).get("x-api-key") ?? "";
|
|
380
|
+
let response;
|
|
381
|
+
try {
|
|
382
|
+
response = await (options.fetch ?? fetch)(address, init);
|
|
383
|
+
} catch {
|
|
384
|
+
throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
|
|
385
|
+
}
|
|
386
|
+
if (response.status === 401 || response.status === 403) {
|
|
387
|
+
await response.body?.cancel().catch(() => {});
|
|
388
|
+
throw new SwitcherError(response.status, "api_error", `Switcher API returned HTTP ${response.status}. Check the configured credential; no alternate identity was selected.`);
|
|
389
|
+
}
|
|
390
|
+
let data;
|
|
391
|
+
try {
|
|
392
|
+
data = await boundedJson(response);
|
|
393
|
+
} catch {
|
|
394
|
+
throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
|
|
395
|
+
}
|
|
396
|
+
if (!response.ok)
|
|
397
|
+
throw apiError(response.status, data, key);
|
|
398
|
+
return Response.json(data);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
const client = new SwitcherClient({ baseUrl: shared.resolution.baseUrl, apiKey: () => {
|
|
402
|
+
throw new Error("Shared credential transport was not invoked.");
|
|
403
|
+
} });
|
|
404
|
+
client.transport = shared.client;
|
|
405
|
+
return client;
|
|
406
|
+
}
|
|
371
407
|
async request(method, path, body, options = {}) {
|
|
372
408
|
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
|
|
373
409
|
throw new Error("Invalid API path.");
|
|
410
|
+
if (this.transport)
|
|
411
|
+
return this.transport.request(method, path.startsWith("/v1/") ? path.slice(3) : path, body, {
|
|
412
|
+
idempotencyKey: method === "GET" ? undefined : options.idempotencyKey ?? crypto.randomUUID(),
|
|
413
|
+
headers: options.version === undefined ? undefined : { "if-match": String(options.version) },
|
|
414
|
+
retry: false
|
|
415
|
+
});
|
|
374
416
|
const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
|
|
375
417
|
if (!apiKey || /[\r\n]/.test(apiKey))
|
|
376
418
|
throw new Error("Switcher API key is required.");
|
|
@@ -467,11 +509,8 @@ class SwitcherClient {
|
|
|
467
509
|
return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
|
|
468
510
|
}
|
|
469
511
|
}
|
|
470
|
-
function clientFromEnv(env = process.env) {
|
|
471
|
-
|
|
472
|
-
if (!env.HASNA_SWITCHER_API_URL || !credential())
|
|
473
|
-
throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
|
|
474
|
-
return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
|
|
512
|
+
function clientFromEnv(env = process.env, options = {}) {
|
|
513
|
+
return SwitcherClient.fromEnvironment(env, options);
|
|
475
514
|
}
|
|
476
515
|
export {
|
|
477
516
|
providerFromPreset,
|
package/dist/mcp/index.js
CHANGED
|
@@ -55,7 +55,7 @@ var routingEventSchema = z.object({
|
|
|
55
55
|
var routingEventsSchema = z.array(routingEventSchema).max(1000);
|
|
56
56
|
|
|
57
57
|
// src/domain.ts
|
|
58
|
-
var VERSION = "0.1.
|
|
58
|
+
var VERSION = "0.1.5";
|
|
59
59
|
var harnessSchema = z2.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
|
|
60
60
|
var protocolSchema = z2.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
|
|
61
61
|
var idSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
@@ -199,7 +199,7 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
// src/sdk.ts
|
|
202
|
-
import {
|
|
202
|
+
import { createClientTransport } from "@hasna/contracts/client";
|
|
203
203
|
|
|
204
204
|
// src/presets.ts
|
|
205
205
|
var route = (protocol, baseUrl, options = {}) => ({
|
|
@@ -321,6 +321,7 @@ function apiError(status, data, apiKey) {
|
|
|
321
321
|
|
|
322
322
|
class SwitcherClient {
|
|
323
323
|
options;
|
|
324
|
+
transport;
|
|
324
325
|
baseUrl;
|
|
325
326
|
constructor(options) {
|
|
326
327
|
this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
|
|
@@ -328,9 +329,50 @@ class SwitcherClient {
|
|
|
328
329
|
throw new Error("Switcher API key is required.");
|
|
329
330
|
this.options = { ...options };
|
|
330
331
|
}
|
|
332
|
+
static fromEnvironment(env = process.env, options = {}) {
|
|
333
|
+
const shared = createClientTransport("switcher", env, {
|
|
334
|
+
credentials: options.credentials,
|
|
335
|
+
timeoutMs: options.timeoutMs ?? 120000,
|
|
336
|
+
retry: false,
|
|
337
|
+
fetchImpl: async (input, init) => {
|
|
338
|
+
const address = String(input).replace(/\/v1\/(health|ready|version)$/, "/$1");
|
|
339
|
+
const key = new Headers(init?.headers).get("x-api-key") ?? "";
|
|
340
|
+
let response;
|
|
341
|
+
try {
|
|
342
|
+
response = await (options.fetch ?? fetch)(address, init);
|
|
343
|
+
} catch {
|
|
344
|
+
throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
|
|
345
|
+
}
|
|
346
|
+
if (response.status === 401 || response.status === 403) {
|
|
347
|
+
await response.body?.cancel().catch(() => {});
|
|
348
|
+
throw new SwitcherError(response.status, "api_error", `Switcher API returned HTTP ${response.status}. Check the configured credential; no alternate identity was selected.`);
|
|
349
|
+
}
|
|
350
|
+
let data;
|
|
351
|
+
try {
|
|
352
|
+
data = await boundedJson(response);
|
|
353
|
+
} catch {
|
|
354
|
+
throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
|
|
355
|
+
}
|
|
356
|
+
if (!response.ok)
|
|
357
|
+
throw apiError(response.status, data, key);
|
|
358
|
+
return Response.json(data);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
const client = new SwitcherClient({ baseUrl: shared.resolution.baseUrl, apiKey: () => {
|
|
362
|
+
throw new Error("Shared credential transport was not invoked.");
|
|
363
|
+
} });
|
|
364
|
+
client.transport = shared.client;
|
|
365
|
+
return client;
|
|
366
|
+
}
|
|
331
367
|
async request(method, path, body, options = {}) {
|
|
332
368
|
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
|
|
333
369
|
throw new Error("Invalid API path.");
|
|
370
|
+
if (this.transport)
|
|
371
|
+
return this.transport.request(method, path.startsWith("/v1/") ? path.slice(3) : path, body, {
|
|
372
|
+
idempotencyKey: method === "GET" ? undefined : options.idempotencyKey ?? crypto.randomUUID(),
|
|
373
|
+
headers: options.version === undefined ? undefined : { "if-match": String(options.version) },
|
|
374
|
+
retry: false
|
|
375
|
+
});
|
|
334
376
|
const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
|
|
335
377
|
if (!apiKey || /[\r\n]/.test(apiKey))
|
|
336
378
|
throw new Error("Switcher API key is required.");
|
|
@@ -427,11 +469,8 @@ class SwitcherClient {
|
|
|
427
469
|
return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
|
|
428
470
|
}
|
|
429
471
|
}
|
|
430
|
-
function clientFromEnv(env = process.env) {
|
|
431
|
-
|
|
432
|
-
if (!env.HASNA_SWITCHER_API_URL || !credential())
|
|
433
|
-
throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
|
|
434
|
-
return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
|
|
472
|
+
function clientFromEnv(env = process.env, options = {}) {
|
|
473
|
+
return SwitcherClient.fromEnvironment(env, options);
|
|
435
474
|
}
|
|
436
475
|
|
|
437
476
|
// src/mcp.ts
|
|
@@ -467,6 +506,6 @@ tool("runs_get", "Get launch metadata.", { id: z3.string() }, (p) => clientFromE
|
|
|
467
506
|
if (process.argv.includes("--version"))
|
|
468
507
|
console.log(VERSION);
|
|
469
508
|
else if (process.argv.includes("--help"))
|
|
470
|
-
console.log("switcher-mcp: authenticated Switcher API tools over MCP stdio.
|
|
509
|
+
console.log("switcher-mcp: authenticated Switcher API tools over MCP stdio. Resolves API URL/key through @hasna/contracts (Keychain, canonical config/credentials, or environment).");
|
|
471
510
|
else
|
|
472
511
|
await server.connect(new StdioServerTransport);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HarnessLaunchInput } from "./harness-types";
|
|
2
|
+
/** Recognize whole SSE terminal events, never marker text inside a delta. */
|
|
3
|
+
export declare function terminalEventObserver(protocol: HarnessLaunchInput["protocol"], contentType: string | null): (chunk: Uint8Array) => boolean;
|
|
4
|
+
/** One lifecycle for all bridges: terminal SSE, caller cancellation, and real failure. */
|
|
5
|
+
export declare function proxyProviderStream(input: {
|
|
6
|
+
response: Response;
|
|
7
|
+
protocol: HarnessLaunchInput["protocol"];
|
|
8
|
+
requestSignal: AbortSignal;
|
|
9
|
+
abort: AbortController;
|
|
10
|
+
closing: () => boolean;
|
|
11
|
+
release: () => void;
|
|
12
|
+
inspect?: (chunk: Uint8Array, done?: boolean) => void;
|
|
13
|
+
interrupted?: () => void;
|
|
14
|
+
}): {
|
|
15
|
+
stream: ReadableStream<Uint8Array>;
|
|
16
|
+
cancel: () => Promise<void>;
|
|
17
|
+
};
|
package/dist/sdk.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { components } from "./generated/api";
|
|
2
|
+
import { type CredentialChainOptions } from "@hasna/contracts/client";
|
|
2
3
|
export { providerFromPreset } from "./presets";
|
|
3
4
|
export type ProviderPreset = components["schemas"]["ProviderPreset"];
|
|
4
5
|
export type ProviderInput = components["schemas"]["ProviderInput"];
|
|
@@ -29,10 +30,18 @@ export type ClientOptions = {
|
|
|
29
30
|
fetch?: typeof fetch;
|
|
30
31
|
timeoutMs?: number;
|
|
31
32
|
};
|
|
33
|
+
export type EnvironmentClientOptions = {
|
|
34
|
+
credentials?: CredentialChainOptions;
|
|
35
|
+
fetch?: typeof fetch;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
};
|
|
32
38
|
export declare class SwitcherClient {
|
|
33
39
|
private readonly options;
|
|
40
|
+
private transport?;
|
|
34
41
|
readonly baseUrl: string;
|
|
35
42
|
constructor(options: ClientOptions);
|
|
43
|
+
/** Use Contracts' paired authority/credential resolution on every request. */
|
|
44
|
+
static fromEnvironment(env?: Record<string, string | undefined>, options?: EnvironmentClientOptions): SwitcherClient;
|
|
36
45
|
request<T>(method: string, path: string, body?: unknown, options?: {
|
|
37
46
|
version?: number;
|
|
38
47
|
idempotencyKey?: string;
|
|
@@ -617,4 +626,4 @@ export declare class SwitcherClient {
|
|
|
617
626
|
routingEventsDropped?: number;
|
|
618
627
|
}>;
|
|
619
628
|
}
|
|
620
|
-
export declare function clientFromEnv(env?: Record<string, string | undefined
|
|
629
|
+
export declare function clientFromEnv(env?: Record<string, string | undefined>, options?: EnvironmentClientOptions): SwitcherClient;
|
package/dist/sdk.js
CHANGED
|
@@ -200,7 +200,7 @@ async function boundedJson(response, maxBytes = MAX_BYTES) {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
// src/sdk.ts
|
|
203
|
-
import {
|
|
203
|
+
import { createClientTransport } from "@hasna/contracts/client";
|
|
204
204
|
|
|
205
205
|
// src/presets.ts
|
|
206
206
|
var route = (protocol, baseUrl, options = {}) => ({
|
|
@@ -361,6 +361,7 @@ function apiError(status, data, apiKey) {
|
|
|
361
361
|
|
|
362
362
|
class SwitcherClient {
|
|
363
363
|
options;
|
|
364
|
+
transport;
|
|
364
365
|
baseUrl;
|
|
365
366
|
constructor(options) {
|
|
366
367
|
this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
|
|
@@ -368,9 +369,50 @@ class SwitcherClient {
|
|
|
368
369
|
throw new Error("Switcher API key is required.");
|
|
369
370
|
this.options = { ...options };
|
|
370
371
|
}
|
|
372
|
+
static fromEnvironment(env = process.env, options = {}) {
|
|
373
|
+
const shared = createClientTransport("switcher", env, {
|
|
374
|
+
credentials: options.credentials,
|
|
375
|
+
timeoutMs: options.timeoutMs ?? 120000,
|
|
376
|
+
retry: false,
|
|
377
|
+
fetchImpl: async (input, init) => {
|
|
378
|
+
const address = String(input).replace(/\/v1\/(health|ready|version)$/, "/$1");
|
|
379
|
+
const key = new Headers(init?.headers).get("x-api-key") ?? "";
|
|
380
|
+
let response;
|
|
381
|
+
try {
|
|
382
|
+
response = await (options.fetch ?? fetch)(address, init);
|
|
383
|
+
} catch {
|
|
384
|
+
throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
|
|
385
|
+
}
|
|
386
|
+
if (response.status === 401 || response.status === 403) {
|
|
387
|
+
await response.body?.cancel().catch(() => {});
|
|
388
|
+
throw new SwitcherError(response.status, "api_error", `Switcher API returned HTTP ${response.status}. Check the configured credential; no alternate identity was selected.`);
|
|
389
|
+
}
|
|
390
|
+
let data;
|
|
391
|
+
try {
|
|
392
|
+
data = await boundedJson(response);
|
|
393
|
+
} catch {
|
|
394
|
+
throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
|
|
395
|
+
}
|
|
396
|
+
if (!response.ok)
|
|
397
|
+
throw apiError(response.status, data, key);
|
|
398
|
+
return Response.json(data);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
const client = new SwitcherClient({ baseUrl: shared.resolution.baseUrl, apiKey: () => {
|
|
402
|
+
throw new Error("Shared credential transport was not invoked.");
|
|
403
|
+
} });
|
|
404
|
+
client.transport = shared.client;
|
|
405
|
+
return client;
|
|
406
|
+
}
|
|
371
407
|
async request(method, path, body, options = {}) {
|
|
372
408
|
if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) && !["/health", "/ready", "/version"].includes(path) || path.includes(".."))
|
|
373
409
|
throw new Error("Invalid API path.");
|
|
410
|
+
if (this.transport)
|
|
411
|
+
return this.transport.request(method, path.startsWith("/v1/") ? path.slice(3) : path, body, {
|
|
412
|
+
idempotencyKey: method === "GET" ? undefined : options.idempotencyKey ?? crypto.randomUUID(),
|
|
413
|
+
headers: options.version === undefined ? undefined : { "if-match": String(options.version) },
|
|
414
|
+
retry: false
|
|
415
|
+
});
|
|
374
416
|
const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
|
|
375
417
|
if (!apiKey || /[\r\n]/.test(apiKey))
|
|
376
418
|
throw new Error("Switcher API key is required.");
|
|
@@ -467,11 +509,8 @@ class SwitcherClient {
|
|
|
467
509
|
return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
|
|
468
510
|
}
|
|
469
511
|
}
|
|
470
|
-
function clientFromEnv(env = process.env) {
|
|
471
|
-
|
|
472
|
-
if (!env.HASNA_SWITCHER_API_URL || !credential())
|
|
473
|
-
throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
|
|
474
|
-
return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
|
|
512
|
+
function clientFromEnv(env = process.env, options = {}) {
|
|
513
|
+
return SwitcherClient.fromEnvironment(env, options);
|
|
475
514
|
}
|
|
476
515
|
export {
|
|
477
516
|
providerFromPreset,
|
package/dist/serve/index.js
CHANGED
|
@@ -62,7 +62,7 @@ function canonicalPolicyJSON(value) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// src/domain.ts
|
|
65
|
-
var VERSION = "0.1.
|
|
65
|
+
var VERSION = "0.1.5";
|
|
66
66
|
var harnessSchema = z2.enum(["claude", "codex", "grok", "opencode", "opencode2", "pi", "omp", "dsh", "cline", "hermes", "prime-agent", "gemini", "aider", "kilo"]);
|
|
67
67
|
var protocolSchema = z2.enum(["anthropic-messages", "openai-responses", "openai-chat", "gemini-generate-content"]);
|
|
68
68
|
var idSchema = z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
|
|
@@ -808,7 +808,7 @@ var openapi_default = {
|
|
|
808
808
|
openapi: "3.0.3",
|
|
809
809
|
info: {
|
|
810
810
|
title: "Switcher API",
|
|
811
|
-
version: "0.1.
|
|
811
|
+
version: "0.1.5",
|
|
812
812
|
description: "Authenticated provider/profile/catalog control plane. Launches run locally; the API never returns provider credentials."
|
|
813
813
|
},
|
|
814
814
|
security: [
|
package/openapi.json
CHANGED
package/package.json
CHANGED