@route_33/opencode 0.4.7-rc.7 → 0.4.7-rc.9

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,19 +1,219 @@
1
- // src/cli/read-env-base-url/read-env-base-url.ts
2
- var readEnvBaseUrl = (env = process.env) => {
3
- const value = env["ROUTE33_BASE_URL"]?.trim();
4
- return value === "" ? void 0 : value;
1
+ // src/auth/auth-error/auth-error.ts
2
+ var AuthError = class extends Error {
3
+ constructor(code, options) {
4
+ super(
5
+ "Route 33 authorization failed",
6
+ options?.cause === void 0 ? void 0 : { cause: options.cause }
7
+ );
8
+ this.code = code;
9
+ this.name = "AuthError";
10
+ this.statusCode = options?.statusCode;
11
+ }
12
+ code;
13
+ statusCode;
5
14
  };
6
15
 
7
- // src/cli/read-env-api-base/read-env-api-base.ts
8
- var readEnvApiBase = (env = process.env) => {
9
- const value = env["ROUTE33_API_BASE"]?.trim();
10
- return value === "" ? void 0 : value;
16
+ // src/auth/build-authorize-url/build-authorize-url.ts
17
+ var buildAuthorizeUrl = (issuerUrl2, request) => {
18
+ const url = new URL("/authorize", issuerUrl2);
19
+ url.search = new URLSearchParams({
20
+ client_id: request.clientId,
21
+ code_challenge: request.codeChallenge,
22
+ code_challenge_method: "S256",
23
+ redirect_uri: request.redirectUri,
24
+ response_type: "code",
25
+ scope: request.scope,
26
+ state: request.state
27
+ }).toString();
28
+ return url;
11
29
  };
12
30
 
13
- // src/cli/read-env-auth-issuer/read-env-auth-issuer.ts
14
- var readEnvAuthIssuer = (env = process.env) => {
15
- const value = env["ROUTE33_AUTH_ISSUER"]?.trim();
16
- return value === "" ? void 0 : value;
31
+ // src/contract/adapters/contract-parse-error/contract-parse-error.ts
32
+ var ContractParseError = class extends Error {
33
+ };
34
+
35
+ // src/contract/adapters/gateway-api-key/gateway-api-key.ts
36
+ var GatewayApiKey = class {
37
+ value;
38
+ constructor(value) {
39
+ this.value = value;
40
+ }
41
+ };
42
+
43
+ // src/contract/adapters/management-jwt/management-jwt.ts
44
+ var ManagementJwt = class {
45
+ value;
46
+ constructor(value) {
47
+ this.value = value;
48
+ }
49
+ };
50
+
51
+ // src/contract/adapters/parse-gateway-key/parse-gateway-key.ts
52
+ var parseGatewayKey = (value) => {
53
+ if (typeof value !== "string" || value.length === 0) {
54
+ throw new Error("gateway key must be non-empty");
55
+ }
56
+ return new GatewayApiKey(value);
57
+ };
58
+
59
+ // src/contract/adapters/parse-management-jwt/parse-management-jwt.ts
60
+ var parseManagementJwt = (value) => {
61
+ if (typeof value !== "string" || value.length === 0) {
62
+ throw new Error("management token must be non-empty");
63
+ }
64
+ return new ManagementJwt(value);
65
+ };
66
+
67
+ // src/contract/predicates/has-string-fields/has-string-fields.ts
68
+ var hasStringFields = (record, fields) => fields.every((field) => typeof record[field] === "string");
69
+
70
+ // src/contract/predicates/is-nullable-string/is-nullable-string.ts
71
+ var isNullableString = (value) => typeof value === "string" || value === null;
72
+
73
+ // src/contract/predicates/is-owner-type/is-owner-type.ts
74
+ var isOwnerType = (value) => value === "project" || value === "user_personal" || value === "user_project";
75
+
76
+ // src/contract/predicates/is-record/is-record.ts
77
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
78
+
79
+ // src/contract/adapters/parse-management-subject/parse-management-subject.ts
80
+ var parseManagementSubject = (credential) => {
81
+ const encodedPayload = credential.value.split(".")[1];
82
+ if (encodedPayload === void 0 || encodedPayload.length === 0) {
83
+ throw new ContractParseError("management token carries no usable subject");
84
+ }
85
+ let payload;
86
+ try {
87
+ payload = JSON.parse(
88
+ Buffer.from(encodedPayload, "base64url").toString("utf8")
89
+ );
90
+ } catch {
91
+ throw new ContractParseError("management token payload is not JSON");
92
+ }
93
+ if (!isRecord(payload)) {
94
+ throw new ContractParseError("management token payload is not an object");
95
+ }
96
+ const subject = payload["sub"];
97
+ if (typeof subject !== "string" || subject.length === 0) {
98
+ throw new ContractParseError("management token carries no usable subject");
99
+ }
100
+ return subject;
101
+ };
102
+
103
+ // src/contract/auth-session/parse-token-set/parse-token-set.ts
104
+ var parseTokenSet = (value) => {
105
+ if (!isRecord(value)) {
106
+ throw new ContractParseError("invalid token response");
107
+ }
108
+ if (typeof value["access_token"] !== "string" || typeof value["expires_in"] !== "number" || value["token_type"] !== "Bearer") {
109
+ throw new ContractParseError("invalid token response");
110
+ }
111
+ return {
112
+ access_token: value["access_token"],
113
+ expires_in: value["expires_in"],
114
+ token_type: "Bearer",
115
+ ...typeof value["id_token"] === "string" ? { id_token: value["id_token"] } : {},
116
+ ...typeof value["refresh_token"] === "string" ? { refresh_token: value["refresh_token"] } : {},
117
+ ...typeof value["scope"] === "string" ? { scope: value["scope"] } : {}
118
+ };
119
+ };
120
+
121
+ // src/auth/exchange-code-and-parse/exchange-code-and-parse.ts
122
+ var exchangeCodeAndParse = async (options) => {
123
+ const body = new URLSearchParams({
124
+ client_id: options.clientId,
125
+ code: options.input.code,
126
+ code_verifier: options.input.codeVerifier,
127
+ grant_type: "authorization_code",
128
+ redirect_uri: options.input.redirectUri
129
+ });
130
+ let response;
131
+ try {
132
+ response = await (options.fetchImpl ?? fetch)(options.tokenUrl, {
133
+ body,
134
+ headers: { "content-type": "application/x-www-form-urlencoded" },
135
+ method: "POST"
136
+ });
137
+ } catch (cause) {
138
+ throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
139
+ }
140
+ if (!response.ok) {
141
+ throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", {
142
+ statusCode: response.status
143
+ });
144
+ }
145
+ let payload;
146
+ try {
147
+ payload = await response.json();
148
+ } catch (cause) {
149
+ throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
150
+ }
151
+ try {
152
+ return parseTokenSet(payload);
153
+ } catch (cause) {
154
+ if (cause instanceof ContractParseError) {
155
+ throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
156
+ }
157
+ throw cause;
158
+ }
159
+ };
160
+
161
+ // src/auth/refresh-session/refresh-session.ts
162
+ var refreshSession = async (options) => {
163
+ const body = new URLSearchParams({
164
+ client_id: options.clientId,
165
+ grant_type: "refresh_token",
166
+ refresh_token: options.refreshToken
167
+ });
168
+ let response;
169
+ try {
170
+ response = await (options.fetchImpl ?? fetch)(options.tokenUrl, {
171
+ body,
172
+ headers: { "content-type": "application/x-www-form-urlencoded" },
173
+ method: "POST"
174
+ });
175
+ } catch (cause) {
176
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
177
+ }
178
+ if (!response.ok) {
179
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", {
180
+ statusCode: response.status
181
+ });
182
+ }
183
+ let payload;
184
+ try {
185
+ payload = await response.json();
186
+ } catch (cause) {
187
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
188
+ }
189
+ try {
190
+ return parseTokenSet(payload);
191
+ } catch (cause) {
192
+ if (cause instanceof ContractParseError) {
193
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
194
+ }
195
+ throw cause;
196
+ }
197
+ };
198
+
199
+ // src/auth/create-auth-session/create-auth-session.ts
200
+ var createAuthSession = (options) => {
201
+ const tokenUrl = new URL("/oauth/token", options.issuerUrl);
202
+ return {
203
+ authorizationUrl: (request) => buildAuthorizeUrl(options.issuerUrl, request),
204
+ exchangeCode: (input) => exchangeCodeAndParse({
205
+ clientId: options.clientId,
206
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
207
+ input,
208
+ tokenUrl
209
+ }),
210
+ refresh: (refreshToken) => refreshSession({
211
+ clientId: options.clientId,
212
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
213
+ refreshToken,
214
+ tokenUrl
215
+ })
216
+ };
17
217
  };
18
218
 
19
219
  // src/shared/default-auth-issuer-url/default-auth-issuer-url.ts
@@ -61,164 +261,406 @@ var resolveCacheDir = (env) => {
61
261
  );
62
262
  };
63
263
 
64
- // src/cli/read-env-cache-dir/read-env-cache-dir.ts
65
- var readEnvCacheDir = (env = process.env) => resolveCacheDir(env);
66
-
67
- // src/cli/read-env-catalog-ttl-ms/read-env-catalog-ttl-ms.ts
68
- var readEnvCatalogTtlMs = (env = process.env) => {
69
- const value = Number(env["ROUTE33_CATALOG_TTL_MS"]);
70
- return Number.isFinite(value) && value > 0 ? value : DEFAULT_CATALOG_TTL_MS;
71
- };
72
-
73
- // src/cli/read-env-client-id/read-env-client-id.ts
74
- var readEnvClientId = (env = process.env) => {
75
- const value = env["ROUTE33_CLIENT_ID"]?.trim();
76
- return value === "" ? void 0 : value;
77
- };
78
-
79
- // src/cli/read-env-direct-api-key/read-env-direct-api-key.ts
80
- var readEnvDirectApiKey = (env = process.env) => {
81
- const value = env["ROUTE33_API_KEY"]?.trim();
82
- return value === "" ? void 0 : value;
83
- };
84
-
85
- // src/cli/read-env-disable-id-shim/read-env-disable-id-shim.ts
86
- var readEnvDisableIdShim = (env = process.env) => env["ROUTE33_DISABLE_ID_SHIM"] === "1";
87
-
88
- // src/cli/read-env-discovery/read-env-discovery.ts
89
- var readEnvDiscovery = (env = process.env) => env["ROUTE33_DISCOVERY"] !== "0";
264
+ // src/auth/open-browser-and-capture/open-browser-and-capture.ts
265
+ import { timingSafeEqual } from "node:crypto";
90
266
 
91
- // src/cli/read-env-strict/read-env-strict.ts
92
- var readEnvStrict = (env = process.env) => env["ROUTE33_STRICT"] !== "0";
267
+ // src/auth/loopback-callback/loopback-callback.ts
268
+ import { createServer } from "node:http";
93
269
 
94
- // src/cli/resolve-v1-secret-store-directory/resolve-v1-secret-store-directory.ts
95
- import * as os2 from "node:os";
96
- import * as path2 from "node:path";
97
- var resolveV1SecretStoreDirectory = (options = {}) => {
98
- const explicit = options.directory?.trim();
99
- if (explicit !== void 0 && explicit.length > 0) {
100
- return path2.resolve(explicit);
101
- }
102
- const hostConfig = (options.env ?? process.env)["OPENCODE_CONFIG_DIR"]?.trim();
103
- if (hostConfig !== void 0 && hostConfig.length > 0) {
104
- return path2.join(path2.resolve(hostConfig), "route33");
105
- }
106
- return path2.join(
107
- options.homeDirectory ?? os2.homedir(),
108
- ".config",
109
- "opencode",
110
- "route33"
111
- );
270
+ // src/auth/loopback-callback/callback-page/callback-page.ts
271
+ var MARK_PATHS = [
272
+ "M310.6,175.8c0-6.1-2.7-11.8-7.2-15.9l-127.6-114.4V7h-43.5v48.3c0,6.2,2.6,12.1,7.2,16.2l92.5,82.9H0v43.5h234.7l-95.6,91.4c-4.3,4.1-6.7,9.8-6.7,15.7v48.3h43.5v-39l128.1-122.4c4.4-4.2,6.8-10,6.7-16.1h0Z",
273
+ "M457.2,175.9v-2.6c18.4-4.9,33-14.1,44-27.7,11-13.6,16.5-30.3,16.5-50.3s-4.5-32.9-13.4-47.5c-9-14.5-21.6-26.1-38-34.9-16.4-8.7-35.8-13.1-58.3-13.1s-41.1,4-58.8,12.1-31.8,19.3-42.4,33.7-16.1,31-16.6,50h51.1c.3-10.8,3.6-20,9.8-27.7,6.2-7.6,14.2-13.5,24.1-17.5,9.9-4.1,20.6-6.1,32-6.1s22.3,2.2,31.1,6.7c8.8,4.5,15.6,10.8,20.5,18.8,4.9,8.1,7.3,17.5,7.3,28.4s-2.8,21.1-8.4,29.4-13.3,14.8-23.2,19.5-21.3,7.1-34.2,7.1h-29.9v43.3h29.9c16.2,0,30,2.5,41.5,7.5,11.5,5,20.2,12,26.1,21.1,6,9,9,19.4,9,31.2s-3,21.2-9.1,29.8-14.4,15.3-25.1,20.1c-10.7,4.9-22.9,7.3-36.7,7.3s-24.2-2.1-34.5-6.2-18.6-10-24.8-17.5c-6.3-7.5-9.8-16.5-10.6-26.8h-53.2c.6,19,6.3,35.7,17,50.2,10.7,14.5,25.2,25.7,43.6,33.8,18.4,8.1,39.4,12.1,63.1,12.1s45.1-4.4,63.7-13.2c18.6-8.8,33.3-20.8,44-36,10.8-15.2,16.1-32.5,16.1-51.8s-6.4-40.7-19.2-55.6c-12.8-14.9-30.8-24.2-54-27.8h0Z",
274
+ "M763,203.7c-12.8-14.9-30.8-24.2-54-27.8v-2.6c18.4-4.9,33-14.1,44-27.7,11-13.6,16.5-30.3,16.5-50.3s-4.5-32.9-13.4-47.5c-8.9-14.5-21.6-26.1-38-34.9-16.4-8.7-35.8-13.1-58.3-13.1s-41.1,4-58.8,12.1-31.8,19.3-42.4,33.7-16.1,31-16.6,50h51.1c.3-10.8,3.6-20,9.8-27.7,6.2-7.6,14.2-13.5,24.1-17.5,9.9-4.1,20.6-6.1,32-6.1s22.3,2.2,31.1,6.7c8.8,4.5,15.6,10.8,20.5,18.8,4.9,8.1,7.3,17.5,7.3,28.4s-2.8,21.1-8.4,29.4c-5.6,8.3-13.3,14.8-23.2,19.5s-21.3,7.1-34.2,7.1h-29.9v43.3h29.9c16.2,0,30,2.5,41.5,7.5,11.5,5,20.2,12,26.1,21.1,6,9,8.9,19.4,8.9,31.2s-3,21.2-9.1,29.8c-6.1,8.6-14.4,15.3-25.1,20.1-10.7,4.9-22.9,7.3-36.7,7.3s-24.2-2.1-34.5-6.2-18.6-10-24.8-17.5c-6.3-7.5-9.8-16.5-10.6-26.8h-53.2c.6,19,6.3,35.7,17,50.2,10.7,14.5,25.2,25.7,43.6,33.8,18.4,8.1,39.4,12.1,63.1,12.1s45.1-4.4,63.7-13.2,33.3-20.8,44-36c10.8-15.2,16.1-32.5,16.1-51.8s-6.4-40.7-19.2-55.6h0Z"
275
+ ];
276
+ var BRAND_MARK = `<svg class="r33-mark" viewBox="0 0 782.2 360.3" fill="currentColor" role="img" aria-label="route33.ai" xmlns="http://www.w3.org/2000/svg"><path d="${MARK_PATHS[0]}"/><path d="${MARK_PATHS[1]}"/><path d="${MARK_PATHS[2]}"/></svg>`;
277
+ var PAGE_STYLE = `:root{color-scheme:dark;--r33-euro-blue:#0000FF;--r33-off-white:#F1F1F1;--r33-off-black:#121212;--r33-grey-300:#C9CBCF;--r33-radius-fillet:4px;--r33-space-2:8px;--r33-space-3:16px;--r33-space-4:24px;--r33-space-6:64px;--r33-font-body:"Geist","Helvetica Neue",Arial,sans-serif;--r33-font-mono:"Geist Mono","SFMono-Regular",Menlo,monospace;--r33-surface:var(--r33-off-black);--r33-text-primary:var(--r33-off-white);--r33-text-secondary:var(--r33-grey-300);--r33-fill-accent:var(--r33-euro-blue);--r33-on-fill-accent:var(--r33-off-white)}
278
+ *{box-sizing:border-box}
279
+ html,body{margin:0}
280
+ .r33-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:var(--r33-space-6);background:var(--r33-surface);color:var(--r33-text-primary);font-family:var(--r33-font-body);-webkit-font-smoothing:antialiased}
281
+ .r33-panel{display:grid;gap:var(--r33-space-4);max-width:460px;width:100%}
282
+ .r33-mark{height:40px;width:auto;color:var(--r33-text-primary)}
283
+ .r33-status{display:inline-flex;align-items:center;gap:var(--r33-space-2);font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--r33-text-secondary)}
284
+ .r33-status__dot{width:8px;height:8px;background:var(--r33-fill-accent)}
285
+ .r33-status__dot--error{background:transparent;border:1px solid var(--r33-text-primary);border-radius:50%}
286
+ .r33-headline{margin:0;font-size:48px;line-height:0.85;letter-spacing:-0.02em;font-weight:600;color:var(--r33-text-primary);text-wrap:balance}
287
+ .r33-subline{margin:0;font-size:16px;line-height:1.45;color:var(--r33-text-secondary);max-width:40ch;text-wrap:pretty}
288
+ .r33-code{margin:0;font-size:13px;letter-spacing:0.04em;color:var(--r33-text-secondary)}
289
+ .r33-code__value{font-family:var(--r33-font-mono);text-transform:uppercase;color:var(--r33-text-primary)}
290
+ @media (prefers-reduced-motion:reduce){*{animation-duration:1ms!important;transition-duration:1ms!important}}`;
291
+ var buildCallbackPage = (state, errorCode) => {
292
+ const isError = state === "error";
293
+ const safeCode = (errorCode ?? "").replace(/[^a-z0-9_-]/gi, "");
294
+ const title = isError ? "Sign-in failed" : "Signed in";
295
+ const headline = isError ? "Sign-in failed" : "Signed in to route33";
296
+ const subline = isError ? "Return to OpenCode and retry." : "Return to OpenCode \u2014 one more step may wait there: if it asks which budget covers usage, pick a project. Then this window can be closed.";
297
+ const statusLabel = isError ? "Authentication error" : "Authenticated";
298
+ const dotClass = isError ? "r33-status__dot r33-status__dot--error" : "r33-status__dot";
299
+ const codeLine = isError && safeCode.length > 0 ? `<p class="r33-code">Error code: <span class="r33-code__value">${safeCode}</span></p>` : "";
300
+ return `<!doctype html>
301
+ <html lang="en" data-theme="dark">
302
+ <head>
303
+ <meta charset="utf-8">
304
+ <meta name="viewport" content="width=device-width, initial-scale=1">
305
+ <title>${title} \xB7 route33.ai</title>
306
+ <style>${PAGE_STYLE}</style>
307
+ </head>
308
+ <body>
309
+ <main class="r33-page">
310
+ <div class="r33-panel">
311
+ ${BRAND_MARK}
312
+ <span class="r33-status"><span class="${dotClass}"></span>${statusLabel}</span>
313
+ <h1 class="r33-headline">${headline}</h1>
314
+ <p class="r33-subline">${subline}</p>
315
+ ${codeLine}
316
+ </div>
317
+ </main>
318
+ </body>
319
+ </html>`;
112
320
  };
113
321
 
114
- // src/config/abortable-sleep/abortable-sleep.ts
115
- var abortableSleep = (milliseconds, signal) => new Promise((resolve2, reject) => {
116
- if (signal === void 0) {
117
- setTimeout(resolve2, milliseconds);
118
- return;
119
- }
120
- const abortError = () => {
121
- const reason = signal.reason;
122
- if (reason instanceof Error) {
123
- return reason;
322
+ // src/auth/loopback-callback/loopback-callback.ts
323
+ var createAuthLoopbackCallback = async () => {
324
+ let redirectUri = "http://127.0.0.1/callback";
325
+ let resolveCapture;
326
+ const captured = new Promise((resolve2) => {
327
+ resolveCapture = resolve2;
328
+ });
329
+ const server = createServer((request, response) => {
330
+ const url = new URL(request.url ?? "/", redirectUri);
331
+ if (request.method !== "GET" || url.pathname !== "/callback") {
332
+ response.writeHead(404).end();
333
+ return;
124
334
  }
125
- return new Error(String(reason), { cause: reason });
335
+ const resolve2 = resolveCapture;
336
+ resolveCapture = void 0;
337
+ resolve2?.(url);
338
+ const error = url.searchParams.get("error");
339
+ const page = error === null || error.length === 0 ? buildCallbackPage("success") : buildCallbackPage("error", error);
340
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
341
+ response.end(page);
342
+ });
343
+ await new Promise((resolve2, reject) => {
344
+ const onError = (cause) => {
345
+ reject(new AuthError("AUTH_CALLBACK_LISTEN_FAILED", { cause }));
346
+ };
347
+ server.once("error", onError);
348
+ server.listen(0, "127.0.0.1", () => {
349
+ server.off("error", onError);
350
+ resolve2();
351
+ });
352
+ });
353
+ const address = server.address();
354
+ if (address === null || typeof address === "string") {
355
+ server.close();
356
+ throw new AuthError("AUTH_CALLBACK_LISTEN_FAILED");
357
+ }
358
+ redirectUri = `http://127.0.0.1:${String(address.port)}/callback`;
359
+ return {
360
+ redirectUri,
361
+ close: () => {
362
+ if (!server.listening) {
363
+ return Promise.resolve();
364
+ }
365
+ return new Promise((resolve2, reject) => {
366
+ server.close((cause) => {
367
+ if (cause === void 0) {
368
+ resolve2();
369
+ return;
370
+ }
371
+ reject(new AuthError("AUTH_CALLBACK_CLEANUP_FAILED", { cause }));
372
+ });
373
+ });
374
+ },
375
+ wait: () => captured
126
376
  };
127
- if (signal.aborted) {
128
- reject(abortError());
377
+ };
378
+
379
+ // src/auth/open-system-browser/open-system-browser.ts
380
+ import { spawn } from "node:child_process";
381
+ var openSystemBrowser = async (url, options = {}) => new Promise((resolve2, reject) => {
382
+ let command = "xdg-open";
383
+ let args = [url.href];
384
+ if (process.platform === "darwin") {
385
+ command = "open";
386
+ } else if (process.platform === "win32") {
387
+ command = "cmd";
388
+ args = ["/c", "start", "", url.href];
389
+ }
390
+ if (options.spawnUrl !== void 0) {
391
+ options.spawnUrl(command, args, { detached: true, stdio: "ignore" });
392
+ resolve2();
129
393
  return;
130
394
  }
131
- const onAbort = () => {
132
- clearTimeout(timer);
133
- reject(abortError());
134
- };
135
- const timer = setTimeout(() => {
136
- signal.removeEventListener("abort", onAbort);
395
+ const child = spawn(command, args, {
396
+ detached: true,
397
+ stdio: "ignore"
398
+ });
399
+ child.once("error", reject);
400
+ child.once("spawn", () => {
401
+ child.unref();
137
402
  resolve2();
138
- }, milliseconds);
139
- signal.addEventListener("abort", onAbort, { once: true });
403
+ });
140
404
  });
141
405
 
142
- // src/config/is-resendable-body/is-resendable-body.ts
143
- var isResendableBody = (body) => body === void 0 || typeof body === "string" || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof Blob || body instanceof URLSearchParams || body instanceof FormData;
144
-
145
- // src/contract/predicates/has-string-fields/has-string-fields.ts
146
- var hasStringFields = (record, fields) => fields.every((field) => typeof record[field] === "string");
147
-
148
- // src/contract/predicates/is-nullable-string/is-nullable-string.ts
149
- var isNullableString = (value) => typeof value === "string" || value === null;
150
-
151
- // src/contract/predicates/is-owner-type/is-owner-type.ts
152
- var isOwnerType = (value) => value === "project" || value === "user_personal" || value === "user_project";
153
-
154
- // src/contract/predicates/is-record/is-record.ts
155
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
156
-
157
- // src/config/parse-exhaustion-envelope/parse-exhaustion-envelope.ts
158
- var parseExhaustionEnvelope = async (response) => {
159
- let body;
406
+ // src/auth/open-browser-and-capture/open-browser-and-capture.ts
407
+ var openBrowserAndCapture = async (options) => {
408
+ const callback = await createAuthLoopbackCallback();
409
+ const authorizationUrl = options.authorizationUrl(callback.redirectUri);
410
+ const browserRunner = options.browserRunner ?? ((url) => openSystemBrowser(url));
411
+ let timer;
412
+ const timeout = new Promise((_resolve, reject) => {
413
+ timer = setTimeout(() => {
414
+ reject(new AuthError("AUTH_CALLBACK_TIMEOUT"));
415
+ }, options.timeoutMs ?? 12e4);
416
+ });
417
+ const launch = browserRunner(authorizationUrl).catch((cause) => {
418
+ throw new AuthError("AUTH_BROWSER_LAUNCH_FAILED", { cause });
419
+ });
160
420
  try {
161
- body = await response.clone().json();
162
- } catch (error) {
163
- if (error instanceof Error) {
164
- return void 0;
421
+ const completed = await Promise.race([
422
+ Promise.all([launch, callback.wait()]),
423
+ timeout
424
+ ]);
425
+ const callbackUrl = completed[1];
426
+ const actualState = callbackUrl.searchParams.get("state");
427
+ if (actualState === null) {
428
+ throw new AuthError("AUTH_CALLBACK_RESPONSE_INVALID");
165
429
  }
166
- throw error;
167
- }
168
- if (!isRecord(body)) {
169
- return void 0;
170
- }
171
- const nestedError = body["error"];
172
- const nestedCode = isRecord(nestedError) ? nestedError["code"] : void 0;
173
- if (body["code"] !== "PROVIDER_LANES_EXHAUSTED" && nestedCode !== "PROVIDER_LANES_EXHAUSTED") {
174
- return void 0;
175
- }
176
- const nestedId = isRecord(nestedError) ? nestedError["correlationId"] : void 0;
177
- const directId = body["correlationId"];
178
- let correlationId;
179
- if (typeof directId === "string") {
180
- correlationId = directId;
181
- } else if (typeof nestedId === "string") {
182
- correlationId = nestedId;
183
- }
184
- return { correlationId };
185
- };
186
-
187
- // src/config/request-model/request-model.ts
188
- var requestModel = (body) => {
189
- if (typeof body !== "string") {
190
- return void 0;
191
- }
192
- let parsed;
193
- try {
194
- parsed = JSON.parse(body);
195
- } catch (error) {
196
- if (error instanceof SyntaxError) {
197
- return void 0;
430
+ const expectedState = Buffer.from(options.expectedState);
431
+ const receivedState = Buffer.from(actualState);
432
+ if (expectedState.length !== receivedState.length || !timingSafeEqual(expectedState, receivedState)) {
433
+ throw new AuthError("AUTH_CALLBACK_STATE_MISMATCH");
198
434
  }
199
- throw error;
435
+ const code = callbackUrl.searchParams.get("code");
436
+ if (code === null || code.length === 0) {
437
+ throw new AuthError("AUTH_CALLBACK_RESPONSE_INVALID");
438
+ }
439
+ return { code, redirectUri: callback.redirectUri };
440
+ } finally {
441
+ if (timer !== void 0) {
442
+ clearTimeout(timer);
443
+ }
444
+ await callback.close();
200
445
  }
201
- return isRecord(parsed) && typeof parsed["model"] === "string" ? parsed["model"] : void 0;
202
446
  };
203
447
 
204
- // src/config/retry-wait-seconds/retry-wait-seconds.ts
205
- var retryWaitSeconds = (options) => {
206
- const advised = options.retryAfter !== null && /^\d+$/.test(options.retryAfter) ? Number(options.retryAfter) : void 0;
207
- return advised !== void 0 && advised > 0 ? Math.min(advised, options.maxWaitSeconds) : Math.min(
208
- options.baseSeconds * 2 ** (options.attempt - 1),
209
- options.maxWaitSeconds
210
- );
211
- };
448
+ // src/auth/pkce-verifier-and-challenge/pkce-verifier-and-challenge.ts
449
+ import { randomBytes } from "node:crypto";
212
450
 
213
- // src/config/exhaustion-retry-fetch/exhaustion-retry-fetch.ts
214
- var buildExhaustionRetryFetch = (innerFetch, options = {}) => {
215
- const sleep = options.sleep ?? abortableSleep;
216
- const maxWaitSeconds = options.maxWaitSeconds ?? 1200;
217
- const baseSeconds = options.baseSeconds ?? 5;
218
- const parseRetryableExhaustion = async (response) => {
219
- if (response.status !== 429) {
220
- return void 0;
221
- }
451
+ // src/auth/pkce-challenge/pkce-challenge.ts
452
+ import { createHash } from "node:crypto";
453
+ var pkceChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
454
+
455
+ // src/auth/pkce-verifier-and-challenge/pkce-verifier-and-challenge.ts
456
+ var createPkceVerifierAndChallenge = () => {
457
+ const codeVerifier = randomBytes(32).toString("base64url");
458
+ return {
459
+ codeChallenge: pkceChallenge(codeVerifier),
460
+ codeVerifier,
461
+ state: randomBytes(32).toString("base64url")
462
+ };
463
+ };
464
+
465
+ // src/auth/requested-scopes/requested-scopes.ts
466
+ var REQUESTED_OAUTH_SCOPES = "openid offline_access projects:read keys:read keys:write";
467
+
468
+ // src/auth/login-flow/login-flow.ts
469
+ var loginFlow = async (options) => {
470
+ const log = options.logger ?? logger;
471
+ log.info("auth.login.started");
472
+ const pkce = createPkceVerifierAndChallenge();
473
+ const captured = await openBrowserAndCapture({
474
+ authorizationUrl: (redirectUri) => options.session.authorizationUrl({
475
+ clientId: options.clientId,
476
+ codeChallenge: pkce.codeChallenge,
477
+ redirectUri,
478
+ scope: REQUESTED_OAUTH_SCOPES,
479
+ state: pkce.state
480
+ }),
481
+ ...options.browserRunner === void 0 ? {} : { browserRunner: options.browserRunner },
482
+ expectedState: pkce.state,
483
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
484
+ });
485
+ const tokenSet = await options.session.exchangeCode({
486
+ code: captured.code,
487
+ codeVerifier: pkce.codeVerifier,
488
+ redirectUri: captured.redirectUri
489
+ });
490
+ let credential;
491
+ try {
492
+ credential = parseManagementJwt(tokenSet.access_token);
493
+ } catch (cause) {
494
+ throw new AuthError("AUTH_TOKEN_RESPONSE_INVALID", { cause });
495
+ }
496
+ log.info("auth.login.completed");
497
+ return { credential, tokenSet };
498
+ };
499
+
500
+ // src/cli/read-env-base-url/read-env-base-url.ts
501
+ var readEnvBaseUrl = (env = process.env) => {
502
+ const value = env["ROUTE33_BASE_URL"]?.trim();
503
+ return value === "" ? void 0 : value;
504
+ };
505
+
506
+ // src/cli/read-env-api-base/read-env-api-base.ts
507
+ var readEnvApiBase = (env = process.env) => {
508
+ const value = env["ROUTE33_API_BASE"]?.trim();
509
+ return value === "" ? void 0 : value;
510
+ };
511
+
512
+ // src/cli/read-env-auth-issuer/read-env-auth-issuer.ts
513
+ var readEnvAuthIssuer = (env = process.env) => {
514
+ const value = env["ROUTE33_AUTH_ISSUER"]?.trim();
515
+ return value === "" ? void 0 : value;
516
+ };
517
+
518
+ // src/cli/read-env-cache-dir/read-env-cache-dir.ts
519
+ var readEnvCacheDir = (env = process.env) => resolveCacheDir(env);
520
+
521
+ // src/cli/read-env-catalog-ttl-ms/read-env-catalog-ttl-ms.ts
522
+ var readEnvCatalogTtlMs = (env = process.env) => {
523
+ const value = Number(env["ROUTE33_CATALOG_TTL_MS"]);
524
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_CATALOG_TTL_MS;
525
+ };
526
+
527
+ // src/cli/read-env-client-id/read-env-client-id.ts
528
+ var readEnvClientId = (env = process.env) => {
529
+ const value = env["ROUTE33_CLIENT_ID"]?.trim();
530
+ return value === "" ? void 0 : value;
531
+ };
532
+
533
+ // src/cli/read-env-direct-api-key/read-env-direct-api-key.ts
534
+ var readEnvDirectApiKey = (env = process.env) => {
535
+ const value = env["ROUTE33_API_KEY"]?.trim();
536
+ return value === "" ? void 0 : value;
537
+ };
538
+
539
+ // src/cli/read-env-disable-id-shim/read-env-disable-id-shim.ts
540
+ var readEnvDisableIdShim = (env = process.env) => env["ROUTE33_DISABLE_ID_SHIM"] === "1";
541
+
542
+ // src/cli/read-env-discovery/read-env-discovery.ts
543
+ var readEnvDiscovery = (env = process.env) => env["ROUTE33_DISCOVERY"] !== "0";
544
+
545
+ // src/cli/read-env-strict/read-env-strict.ts
546
+ var readEnvStrict = (env = process.env) => env["ROUTE33_STRICT"] !== "0";
547
+
548
+ // src/cli/resolve-v1-secret-store-directory/resolve-v1-secret-store-directory.ts
549
+ import * as os2 from "node:os";
550
+ import * as path2 from "node:path";
551
+ var resolveV1SecretStoreDirectory = (options = {}) => {
552
+ const explicit = options.directory?.trim();
553
+ if (explicit !== void 0 && explicit.length > 0) {
554
+ return path2.resolve(explicit);
555
+ }
556
+ const hostConfig = (options.env ?? process.env)["OPENCODE_CONFIG_DIR"]?.trim();
557
+ if (hostConfig !== void 0 && hostConfig.length > 0) {
558
+ return path2.join(path2.resolve(hostConfig), "route33");
559
+ }
560
+ return path2.join(
561
+ options.homeDirectory ?? os2.homedir(),
562
+ ".config",
563
+ "opencode",
564
+ "route33"
565
+ );
566
+ };
567
+
568
+ // src/config/abortable-sleep/abortable-sleep.ts
569
+ var abortableSleep = (milliseconds, signal) => new Promise((resolve2, reject) => {
570
+ if (signal === void 0) {
571
+ setTimeout(resolve2, milliseconds);
572
+ return;
573
+ }
574
+ const abortError = () => {
575
+ const reason = signal.reason;
576
+ if (reason instanceof Error) {
577
+ return reason;
578
+ }
579
+ return new Error(String(reason), { cause: reason });
580
+ };
581
+ if (signal.aborted) {
582
+ reject(abortError());
583
+ return;
584
+ }
585
+ const onAbort = () => {
586
+ clearTimeout(timer);
587
+ reject(abortError());
588
+ };
589
+ const timer = setTimeout(() => {
590
+ signal.removeEventListener("abort", onAbort);
591
+ resolve2();
592
+ }, milliseconds);
593
+ signal.addEventListener("abort", onAbort, { once: true });
594
+ });
595
+
596
+ // src/config/is-resendable-body/is-resendable-body.ts
597
+ var isResendableBody = (body) => body === void 0 || typeof body === "string" || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof Blob || body instanceof URLSearchParams || body instanceof FormData;
598
+
599
+ // src/config/parse-exhaustion-envelope/parse-exhaustion-envelope.ts
600
+ var parseExhaustionEnvelope = async (response) => {
601
+ let body;
602
+ try {
603
+ body = await response.clone().json();
604
+ } catch (error) {
605
+ if (error instanceof Error) {
606
+ return void 0;
607
+ }
608
+ throw error;
609
+ }
610
+ if (!isRecord(body)) {
611
+ return void 0;
612
+ }
613
+ const nestedError = body["error"];
614
+ const nestedCode = isRecord(nestedError) ? nestedError["code"] : void 0;
615
+ if (body["code"] !== "PROVIDER_LANES_EXHAUSTED" && nestedCode !== "PROVIDER_LANES_EXHAUSTED") {
616
+ return void 0;
617
+ }
618
+ const nestedId = isRecord(nestedError) ? nestedError["correlationId"] : void 0;
619
+ const directId = body["correlationId"];
620
+ let correlationId;
621
+ if (typeof directId === "string") {
622
+ correlationId = directId;
623
+ } else if (typeof nestedId === "string") {
624
+ correlationId = nestedId;
625
+ }
626
+ return { correlationId };
627
+ };
628
+
629
+ // src/config/request-model/request-model.ts
630
+ var requestModel = (body) => {
631
+ if (typeof body !== "string") {
632
+ return void 0;
633
+ }
634
+ let parsed;
635
+ try {
636
+ parsed = JSON.parse(body);
637
+ } catch (error) {
638
+ if (error instanceof SyntaxError) {
639
+ return void 0;
640
+ }
641
+ throw error;
642
+ }
643
+ return isRecord(parsed) && typeof parsed["model"] === "string" ? parsed["model"] : void 0;
644
+ };
645
+
646
+ // src/config/retry-wait-seconds/retry-wait-seconds.ts
647
+ var retryWaitSeconds = (options) => {
648
+ const advised = options.retryAfter !== null && /^\d+$/.test(options.retryAfter) ? Number(options.retryAfter) : void 0;
649
+ return advised !== void 0 && advised > 0 ? Math.min(advised, options.maxWaitSeconds) : Math.min(
650
+ options.baseSeconds * 2 ** (options.attempt - 1),
651
+ options.maxWaitSeconds
652
+ );
653
+ };
654
+
655
+ // src/config/exhaustion-retry-fetch/exhaustion-retry-fetch.ts
656
+ var buildExhaustionRetryFetch = (innerFetch, options = {}) => {
657
+ const sleep = options.sleep ?? abortableSleep;
658
+ const maxWaitSeconds = options.maxWaitSeconds ?? 1200;
659
+ const baseSeconds = options.baseSeconds ?? 5;
660
+ const parseRetryableExhaustion = async (response) => {
661
+ if (response.status !== 429) {
662
+ return void 0;
663
+ }
222
664
  const contentType = response.headers.get("content-type") ?? "";
223
665
  if (!/(?:^|\/)json(?:;|$)|\+json(?:;|$)/i.test(contentType)) {
224
666
  return void 0;
@@ -443,8 +885,8 @@ var catalogMemory = /* @__PURE__ */ new Map();
443
885
  var CATALOG_CACHE_VERSION = 2;
444
886
 
445
887
  // src/catalog/catalog-cache/hash-key/hash-key.ts
446
- import { createHash } from "node:crypto";
447
- var hashKey = (baseURL, gatewayKey) => createHash("sha256").update(`${baseURL}
888
+ import { createHash as createHash2 } from "node:crypto";
889
+ var hashKey = (baseURL, gatewayKey) => createHash2("sha256").update(`${baseURL}
448
890
  ${gatewayKey.value}`).digest("hex").slice(0, 12);
449
891
 
450
892
  // src/catalog/catalog-cache/is-catalog-model-record/is-catalog-model-record.ts
@@ -548,86 +990,26 @@ var catalogModelsUrl = (baseURL) => {
548
990
  // src/catalog/default-fetch-timeout-ms/default-fetch-timeout-ms.ts
549
991
  var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
550
992
 
551
- // src/contract/adapters/contract-parse-error/contract-parse-error.ts
552
- var ContractParseError = class extends Error {
553
- };
554
-
555
- // src/contract/adapters/gateway-api-key/gateway-api-key.ts
556
- var GatewayApiKey = class {
557
- value;
558
- constructor(value) {
559
- this.value = value;
993
+ // src/contract/gateway-catalog/parse-catalog-result/parse-catalog-result.ts
994
+ var parseCatalogResult = (value) => {
995
+ if (!isRecord(value) || !Array.isArray(value["data"])) {
996
+ throw new ContractParseError("invalid catalog response");
560
997
  }
998
+ return value;
561
999
  };
562
1000
 
563
- // src/contract/adapters/management-jwt/management-jwt.ts
564
- var ManagementJwt = class {
565
- value;
566
- constructor(value) {
567
- this.value = value;
1001
+ // src/catalog/parse-catalog-entries/parse-catalog-entries.ts
1002
+ var parseCatalogEntries = (payload) => {
1003
+ if (Array.isArray(payload)) {
1004
+ return payload;
568
1005
  }
569
- };
570
-
571
- // src/contract/adapters/parse-gateway-key/parse-gateway-key.ts
572
- var parseGatewayKey = (value) => {
573
- if (typeof value !== "string" || value.length === 0) {
574
- throw new Error("gateway key must be non-empty");
575
- }
576
- return new GatewayApiKey(value);
577
- };
578
-
579
- // src/contract/adapters/parse-management-jwt/parse-management-jwt.ts
580
- var parseManagementJwt = (value) => {
581
- if (typeof value !== "string" || value.length === 0) {
582
- throw new Error("management token must be non-empty");
583
- }
584
- return new ManagementJwt(value);
585
- };
586
-
587
- // src/contract/adapters/parse-management-subject/parse-management-subject.ts
588
- var parseManagementSubject = (credential) => {
589
- const encodedPayload = credential.value.split(".")[1];
590
- if (encodedPayload === void 0 || encodedPayload.length === 0) {
591
- throw new ContractParseError("management token carries no usable subject");
592
- }
593
- let payload;
594
- try {
595
- payload = JSON.parse(
596
- Buffer.from(encodedPayload, "base64url").toString("utf8")
597
- );
598
- } catch {
599
- throw new ContractParseError("management token payload is not JSON");
600
- }
601
- if (!isRecord(payload)) {
602
- throw new ContractParseError("management token payload is not an object");
603
- }
604
- const subject = payload["sub"];
605
- if (typeof subject !== "string" || subject.length === 0) {
606
- throw new ContractParseError("management token carries no usable subject");
607
- }
608
- return subject;
609
- };
610
-
611
- // src/contract/gateway-catalog/parse-catalog-result/parse-catalog-result.ts
612
- var parseCatalogResult = (value) => {
613
- if (!isRecord(value) || !Array.isArray(value["data"])) {
614
- throw new ContractParseError("invalid catalog response");
615
- }
616
- return value;
617
- };
618
-
619
- // src/catalog/parse-catalog-entries/parse-catalog-entries.ts
620
- var parseCatalogEntries = (payload) => {
621
- if (Array.isArray(payload)) {
622
- return payload;
623
- }
624
- try {
625
- return parseCatalogResult(payload).data;
626
- } catch (error) {
627
- if (error instanceof ContractParseError) {
628
- throw new CatalogFetchError("parse", error.message);
629
- }
630
- throw error;
1006
+ try {
1007
+ return parseCatalogResult(payload).data;
1008
+ } catch (error) {
1009
+ if (error instanceof ContractParseError) {
1010
+ throw new CatalogFetchError("parse", error.message);
1011
+ }
1012
+ throw error;
631
1013
  }
632
1014
  };
633
1015
 
@@ -2453,508 +2835,134 @@ var createLeaseInstallationFactory = (options) => {
2453
2835
  const installation = {
2454
2836
  keyName: `route33:lease:${scopeId}`,
2455
2837
  secretStore: options.createSecretStore(scopeId),
2456
- ...options.createStateStore === void 0 ? {} : { stateStore: options.createStateStore(scopeId) },
2457
- state: LeaseState.restore(void 0)
2458
- };
2459
- installations.set(scopeId, installation);
2460
- return installation;
2461
- };
2462
- };
2463
-
2464
- // src/key-lease/scope-id/scope-id.ts
2465
- import { createHash as createHash2 } from "node:crypto";
2466
- var deriveScopeId = (input) => {
2467
- let identity;
2468
- switch (input.attribution.kind) {
2469
- case "personal":
2470
- identity = ["route33-lease", "personal"];
2471
- break;
2472
- case "project":
2473
- if (input.folderRealpath === void 0 || input.folderRealpath.length === 0) {
2474
- throw new TypeError("Project lease scope requires a real folder path");
2475
- }
2476
- identity = [
2477
- "route33-lease",
2478
- "project",
2479
- input.attribution.projectId,
2480
- input.folderRealpath
2481
- ];
2482
- break;
2483
- default: {
2484
- const unexpected = input.attribution;
2485
- throw new TypeError(`Unexpected attribution: ${String(unexpected)}`);
2486
- }
2487
- }
2488
- return createHash2("sha256").update(JSON.stringify(identity)).digest("hex");
2489
- };
2490
-
2491
- // src/requested-client-id/requested-client-id.ts
2492
- var DEFAULT_ROUTE33_CLIENT_ID = false ? "" : "oa_platform_203623de";
2493
-
2494
- // src/commands/change-attribution/change-attribution.ts
2495
- var changeAttribution = async (options) => {
2496
- await options.clear();
2497
- return options.prompt();
2498
- };
2499
-
2500
- // src/commands/inspect-session/inspect-session.ts
2501
- var inspectSession = (options) => {
2502
- const attribution = options.readAttribution();
2503
- const lease = options.readLease();
2504
- return {
2505
- attribution,
2506
- ...lease === void 0 ? {} : { lease: { expiresAt: lease.expiresAt, keyId: lease.keyId } }
2507
- };
2508
- };
2509
-
2510
- // src/commands/reset-attribution/reset-attribution.ts
2511
- var resetAttribution = async (options) => {
2512
- await options.clear();
2513
- };
2514
-
2515
- // src/commands/rotate-now/rotate-now.ts
2516
- var rotateNow = async (options) => {
2517
- const rotated = await options.lease({ renewalRatio: Number.EPSILON });
2518
- return rotated.lease;
2519
- };
2520
-
2521
- // src/ux/attribution-prompt/attribution-prompt.ts
2522
- var createAttributionPrompt = (projects) => {
2523
- const choicesBySelection = {
2524
- personal: { kind: "personal" }
2525
- };
2526
- const projectOptions = projects.map((project) => {
2527
- const selection = `project:${project.id}`;
2528
- choicesBySelection[selection] = {
2529
- kind: "project",
2530
- projectId: project.id
2531
- };
2532
- return {
2533
- label: `${project.name} (${project.organisation.name})`,
2534
- selection
2535
- };
2536
- });
2537
- return {
2538
- choicesBySelection,
2539
- options: [
2540
- ...projectOptions,
2541
- { label: "Use my personal key", selection: "personal" }
2542
- ],
2543
- title: "Choose where Route 33 should manage this key"
2544
- };
2545
- };
2546
-
2547
- // src/ux/attribution-prompt-error/attribution-prompt-error.ts
2548
- var MESSAGES = {
2549
- ATTRIBUTION_PRESENTER_UNAVAILABLE: "Route 33 needs OpenCode v2 to assign a budget for this folder. Update to OpenCode v2, run /connect, choose Route 33, and follow the steps.",
2550
- ATTRIBUTION_SELECTION_INVALID: "That Route 33 option is no longer available. Connect again and pick another."
2551
- };
2552
- var AttributionPromptError = class extends GatewayCredentialError {
2553
- code;
2554
- constructor(code) {
2555
- super(MESSAGES[code]);
2556
- this.name = "AttributionPromptError";
2557
- this.code = code;
2558
- }
2559
- };
2560
-
2561
- // src/ux/prompt-flow/prompt-flow.ts
2562
- var resolveAttribution = async (options) => {
2563
- const remembered = readAttribution(options.state);
2564
- if (remembered !== void 0) {
2565
- return remembered;
2566
- }
2567
- if (options.presenter === void 0) {
2568
- throw new AttributionPromptError("ATTRIBUTION_PRESENTER_UNAVAILABLE");
2569
- }
2570
- const plan = createAttributionPrompt(await options.loadProjects());
2571
- const selection = await options.presenter.pick(plan);
2572
- const attribution = plan.choicesBySelection[selection];
2573
- if (attribution === void 0) {
2574
- throw new AttributionPromptError("ATTRIBUTION_SELECTION_INVALID");
2575
- }
2576
- writeAttribution(options.state, attribution);
2577
- return attribution;
2578
- };
2579
-
2580
- // src/ux/trusted-party-copy/trusted-party-copy.ts
2581
- var TRUSTED_PARTY_COPY = {
2582
- message: "Route 33 wants to manage your keys for this OpenCode installation.",
2583
- title: "Connect Route 33"
2584
- };
2585
-
2586
- // src/auth/auth-error/auth-error.ts
2587
- var AuthError = class extends Error {
2588
- constructor(code, options) {
2589
- super(
2590
- "Route 33 authorization failed",
2591
- options?.cause === void 0 ? void 0 : { cause: options.cause }
2592
- );
2593
- this.code = code;
2594
- this.name = "AuthError";
2595
- this.statusCode = options?.statusCode;
2596
- }
2597
- code;
2598
- statusCode;
2599
- };
2600
-
2601
- // src/auth/build-authorize-url/build-authorize-url.ts
2602
- var buildAuthorizeUrl = (issuerUrl, request) => {
2603
- const url = new URL("/authorize", issuerUrl);
2604
- url.search = new URLSearchParams({
2605
- client_id: request.clientId,
2606
- code_challenge: request.codeChallenge,
2607
- code_challenge_method: "S256",
2608
- redirect_uri: request.redirectUri,
2609
- response_type: "code",
2610
- scope: request.scope,
2611
- state: request.state
2612
- }).toString();
2613
- return url;
2614
- };
2615
-
2616
- // src/contract/auth-session/parse-token-set/parse-token-set.ts
2617
- var parseTokenSet = (value) => {
2618
- if (!isRecord(value)) {
2619
- throw new ContractParseError("invalid token response");
2620
- }
2621
- if (typeof value["access_token"] !== "string" || typeof value["expires_in"] !== "number" || value["token_type"] !== "Bearer") {
2622
- throw new ContractParseError("invalid token response");
2623
- }
2624
- return {
2625
- access_token: value["access_token"],
2626
- expires_in: value["expires_in"],
2627
- token_type: "Bearer",
2628
- ...typeof value["id_token"] === "string" ? { id_token: value["id_token"] } : {},
2629
- ...typeof value["refresh_token"] === "string" ? { refresh_token: value["refresh_token"] } : {},
2630
- ...typeof value["scope"] === "string" ? { scope: value["scope"] } : {}
2631
- };
2632
- };
2633
-
2634
- // src/auth/exchange-code-and-parse/exchange-code-and-parse.ts
2635
- var exchangeCodeAndParse = async (options) => {
2636
- const body = new URLSearchParams({
2637
- client_id: options.clientId,
2638
- code: options.input.code,
2639
- code_verifier: options.input.codeVerifier,
2640
- grant_type: "authorization_code",
2641
- redirect_uri: options.input.redirectUri
2642
- });
2643
- let response;
2644
- try {
2645
- response = await (options.fetchImpl ?? fetch)(options.tokenUrl, {
2646
- body,
2647
- headers: { "content-type": "application/x-www-form-urlencoded" },
2648
- method: "POST"
2649
- });
2650
- } catch (cause) {
2651
- throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
2652
- }
2653
- if (!response.ok) {
2654
- throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", {
2655
- statusCode: response.status
2656
- });
2657
- }
2658
- let payload;
2659
- try {
2660
- payload = await response.json();
2661
- } catch (cause) {
2662
- throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
2663
- }
2664
- try {
2665
- return parseTokenSet(payload);
2666
- } catch (cause) {
2667
- if (cause instanceof ContractParseError) {
2668
- throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
2669
- }
2670
- throw cause;
2671
- }
2672
- };
2673
-
2674
- // src/auth/refresh-session/refresh-session.ts
2675
- var refreshSession = async (options) => {
2676
- const body = new URLSearchParams({
2677
- client_id: options.clientId,
2678
- grant_type: "refresh_token",
2679
- refresh_token: options.refreshToken
2680
- });
2681
- let response;
2682
- try {
2683
- response = await (options.fetchImpl ?? fetch)(options.tokenUrl, {
2684
- body,
2685
- headers: { "content-type": "application/x-www-form-urlencoded" },
2686
- method: "POST"
2687
- });
2688
- } catch (cause) {
2689
- throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
2690
- }
2691
- if (!response.ok) {
2692
- throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", {
2693
- statusCode: response.status
2694
- });
2695
- }
2696
- let payload;
2697
- try {
2698
- payload = await response.json();
2699
- } catch (cause) {
2700
- throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
2701
- }
2702
- try {
2703
- return parseTokenSet(payload);
2704
- } catch (cause) {
2705
- if (cause instanceof ContractParseError) {
2706
- throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
2707
- }
2708
- throw cause;
2709
- }
2710
- };
2711
-
2712
- // src/auth/create-auth-session/create-auth-session.ts
2713
- var createAuthSession = (options) => {
2714
- const tokenUrl = new URL("/oauth/token", options.issuerUrl);
2715
- return {
2716
- authorizationUrl: (request) => buildAuthorizeUrl(options.issuerUrl, request),
2717
- exchangeCode: (input) => exchangeCodeAndParse({
2718
- clientId: options.clientId,
2719
- ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
2720
- input,
2721
- tokenUrl
2722
- }),
2723
- refresh: (refreshToken) => refreshSession({
2724
- clientId: options.clientId,
2725
- ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
2726
- refreshToken,
2727
- tokenUrl
2728
- })
2729
- };
2730
- };
2731
-
2732
- // src/auth/open-browser-and-capture/open-browser-and-capture.ts
2733
- import { spawn } from "node:child_process";
2734
- import { timingSafeEqual } from "node:crypto";
2735
-
2736
- // src/auth/loopback-callback/loopback-callback.ts
2737
- import { createServer } from "node:http";
2738
-
2739
- // src/auth/loopback-callback/callback-page/callback-page.ts
2740
- var MARK_PATHS = [
2741
- "M310.6,175.8c0-6.1-2.7-11.8-7.2-15.9l-127.6-114.4V7h-43.5v48.3c0,6.2,2.6,12.1,7.2,16.2l92.5,82.9H0v43.5h234.7l-95.6,91.4c-4.3,4.1-6.7,9.8-6.7,15.7v48.3h43.5v-39l128.1-122.4c4.4-4.2,6.8-10,6.7-16.1h0Z",
2742
- "M457.2,175.9v-2.6c18.4-4.9,33-14.1,44-27.7,11-13.6,16.5-30.3,16.5-50.3s-4.5-32.9-13.4-47.5c-9-14.5-21.6-26.1-38-34.9-16.4-8.7-35.8-13.1-58.3-13.1s-41.1,4-58.8,12.1-31.8,19.3-42.4,33.7-16.1,31-16.6,50h51.1c.3-10.8,3.6-20,9.8-27.7,6.2-7.6,14.2-13.5,24.1-17.5,9.9-4.1,20.6-6.1,32-6.1s22.3,2.2,31.1,6.7c8.8,4.5,15.6,10.8,20.5,18.8,4.9,8.1,7.3,17.5,7.3,28.4s-2.8,21.1-8.4,29.4-13.3,14.8-23.2,19.5-21.3,7.1-34.2,7.1h-29.9v43.3h29.9c16.2,0,30,2.5,41.5,7.5,11.5,5,20.2,12,26.1,21.1,6,9,9,19.4,9,31.2s-3,21.2-9.1,29.8-14.4,15.3-25.1,20.1c-10.7,4.9-22.9,7.3-36.7,7.3s-24.2-2.1-34.5-6.2-18.6-10-24.8-17.5c-6.3-7.5-9.8-16.5-10.6-26.8h-53.2c.6,19,6.3,35.7,17,50.2,10.7,14.5,25.2,25.7,43.6,33.8,18.4,8.1,39.4,12.1,63.1,12.1s45.1-4.4,63.7-13.2c18.6-8.8,33.3-20.8,44-36,10.8-15.2,16.1-32.5,16.1-51.8s-6.4-40.7-19.2-55.6c-12.8-14.9-30.8-24.2-54-27.8h0Z",
2743
- "M763,203.7c-12.8-14.9-30.8-24.2-54-27.8v-2.6c18.4-4.9,33-14.1,44-27.7,11-13.6,16.5-30.3,16.5-50.3s-4.5-32.9-13.4-47.5c-8.9-14.5-21.6-26.1-38-34.9-16.4-8.7-35.8-13.1-58.3-13.1s-41.1,4-58.8,12.1-31.8,19.3-42.4,33.7-16.1,31-16.6,50h51.1c.3-10.8,3.6-20,9.8-27.7,6.2-7.6,14.2-13.5,24.1-17.5,9.9-4.1,20.6-6.1,32-6.1s22.3,2.2,31.1,6.7c8.8,4.5,15.6,10.8,20.5,18.8,4.9,8.1,7.3,17.5,7.3,28.4s-2.8,21.1-8.4,29.4c-5.6,8.3-13.3,14.8-23.2,19.5s-21.3,7.1-34.2,7.1h-29.9v43.3h29.9c16.2,0,30,2.5,41.5,7.5,11.5,5,20.2,12,26.1,21.1,6,9,8.9,19.4,8.9,31.2s-3,21.2-9.1,29.8c-6.1,8.6-14.4,15.3-25.1,20.1-10.7,4.9-22.9,7.3-36.7,7.3s-24.2-2.1-34.5-6.2-18.6-10-24.8-17.5c-6.3-7.5-9.8-16.5-10.6-26.8h-53.2c.6,19,6.3,35.7,17,50.2,10.7,14.5,25.2,25.7,43.6,33.8,18.4,8.1,39.4,12.1,63.1,12.1s45.1-4.4,63.7-13.2,33.3-20.8,44-36c10.8-15.2,16.1-32.5,16.1-51.8s-6.4-40.7-19.2-55.6h0Z"
2744
- ];
2745
- var BRAND_MARK = `<svg class="r33-mark" viewBox="0 0 782.2 360.3" fill="currentColor" role="img" aria-label="route33.ai" xmlns="http://www.w3.org/2000/svg"><path d="${MARK_PATHS[0]}"/><path d="${MARK_PATHS[1]}"/><path d="${MARK_PATHS[2]}"/></svg>`;
2746
- var PAGE_STYLE = `:root{color-scheme:dark;--r33-euro-blue:#0000FF;--r33-off-white:#F1F1F1;--r33-off-black:#121212;--r33-grey-300:#C9CBCF;--r33-radius-fillet:4px;--r33-space-2:8px;--r33-space-3:16px;--r33-space-4:24px;--r33-space-6:64px;--r33-font-body:"Geist","Helvetica Neue",Arial,sans-serif;--r33-font-mono:"Geist Mono","SFMono-Regular",Menlo,monospace;--r33-surface:var(--r33-off-black);--r33-text-primary:var(--r33-off-white);--r33-text-secondary:var(--r33-grey-300);--r33-fill-accent:var(--r33-euro-blue);--r33-on-fill-accent:var(--r33-off-white)}
2747
- *{box-sizing:border-box}
2748
- html,body{margin:0}
2749
- .r33-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:var(--r33-space-6);background:var(--r33-surface);color:var(--r33-text-primary);font-family:var(--r33-font-body);-webkit-font-smoothing:antialiased}
2750
- .r33-panel{display:grid;gap:var(--r33-space-4);max-width:460px;width:100%}
2751
- .r33-mark{height:40px;width:auto;color:var(--r33-text-primary)}
2752
- .r33-status{display:inline-flex;align-items:center;gap:var(--r33-space-2);font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--r33-text-secondary)}
2753
- .r33-status__dot{width:8px;height:8px;background:var(--r33-fill-accent)}
2754
- .r33-status__dot--error{background:transparent;border:1px solid var(--r33-text-primary);border-radius:50%}
2755
- .r33-headline{margin:0;font-size:48px;line-height:0.85;letter-spacing:-0.02em;font-weight:600;color:var(--r33-text-primary);text-wrap:balance}
2756
- .r33-subline{margin:0;font-size:16px;line-height:1.45;color:var(--r33-text-secondary);max-width:40ch;text-wrap:pretty}
2757
- .r33-code{margin:0;font-size:13px;letter-spacing:0.04em;color:var(--r33-text-secondary)}
2758
- .r33-code__value{font-family:var(--r33-font-mono);text-transform:uppercase;color:var(--r33-text-primary)}
2759
- @media (prefers-reduced-motion:reduce){*{animation-duration:1ms!important;transition-duration:1ms!important}}`;
2760
- var buildCallbackPage = (state, errorCode) => {
2761
- const isError = state === "error";
2762
- const safeCode = (errorCode ?? "").replace(/[^a-z0-9_-]/gi, "");
2763
- const title = isError ? "Sign-in failed" : "Signed in";
2764
- const headline = isError ? "Sign-in failed" : "Signed in to route33";
2765
- const subline = isError ? "Return to OpenCode and retry." : "Return to OpenCode \u2014 one more step may wait there: if it asks which budget covers usage, pick a project. Then this window can be closed.";
2766
- const statusLabel = isError ? "Authentication error" : "Authenticated";
2767
- const dotClass = isError ? "r33-status__dot r33-status__dot--error" : "r33-status__dot";
2768
- const codeLine = isError && safeCode.length > 0 ? `<p class="r33-code">Error code: <span class="r33-code__value">${safeCode}</span></p>` : "";
2769
- return `<!doctype html>
2770
- <html lang="en" data-theme="dark">
2771
- <head>
2772
- <meta charset="utf-8">
2773
- <meta name="viewport" content="width=device-width, initial-scale=1">
2774
- <title>${title} \xB7 route33.ai</title>
2775
- <style>${PAGE_STYLE}</style>
2776
- </head>
2777
- <body>
2778
- <main class="r33-page">
2779
- <div class="r33-panel">
2780
- ${BRAND_MARK}
2781
- <span class="r33-status"><span class="${dotClass}"></span>${statusLabel}</span>
2782
- <h1 class="r33-headline">${headline}</h1>
2783
- <p class="r33-subline">${subline}</p>
2784
- ${codeLine}
2785
- </div>
2786
- </main>
2787
- </body>
2788
- </html>`;
2789
- };
2790
-
2791
- // src/auth/loopback-callback/loopback-callback.ts
2792
- var createAuthLoopbackCallback = async () => {
2793
- let redirectUri = "http://127.0.0.1/callback";
2794
- let resolveCapture;
2795
- const captured = new Promise((resolve2) => {
2796
- resolveCapture = resolve2;
2797
- });
2798
- const server = createServer((request, response) => {
2799
- const url = new URL(request.url ?? "/", redirectUri);
2800
- if (request.method !== "GET" || url.pathname !== "/callback") {
2801
- response.writeHead(404).end();
2802
- return;
2803
- }
2804
- const resolve2 = resolveCapture;
2805
- resolveCapture = void 0;
2806
- resolve2?.(url);
2807
- const error = url.searchParams.get("error");
2808
- const page = error === null || error.length === 0 ? buildCallbackPage("success") : buildCallbackPage("error", error);
2809
- response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2810
- response.end(page);
2811
- });
2812
- await new Promise((resolve2, reject) => {
2813
- const onError = (cause) => {
2814
- reject(new AuthError("AUTH_CALLBACK_LISTEN_FAILED", { cause }));
2815
- };
2816
- server.once("error", onError);
2817
- server.listen(0, "127.0.0.1", () => {
2818
- server.off("error", onError);
2819
- resolve2();
2820
- });
2821
- });
2822
- const address = server.address();
2823
- if (address === null || typeof address === "string") {
2824
- server.close();
2825
- throw new AuthError("AUTH_CALLBACK_LISTEN_FAILED");
2826
- }
2827
- redirectUri = `http://127.0.0.1:${String(address.port)}/callback`;
2828
- return {
2829
- redirectUri,
2830
- close: () => {
2831
- if (!server.listening) {
2832
- return Promise.resolve();
2833
- }
2834
- return new Promise((resolve2, reject) => {
2835
- server.close((cause) => {
2836
- if (cause === void 0) {
2837
- resolve2();
2838
- return;
2839
- }
2840
- reject(new AuthError("AUTH_CALLBACK_CLEANUP_FAILED", { cause }));
2841
- });
2842
- });
2843
- },
2844
- wait: () => captured
2838
+ ...options.createStateStore === void 0 ? {} : { stateStore: options.createStateStore(scopeId) },
2839
+ state: LeaseState.restore(void 0)
2840
+ };
2841
+ installations.set(scopeId, installation);
2842
+ return installation;
2845
2843
  };
2846
2844
  };
2847
2845
 
2848
- // src/auth/open-browser-and-capture/open-browser-and-capture.ts
2849
- var openBrowserAndCapture = async (options) => {
2850
- const callback = await createAuthLoopbackCallback();
2851
- const authorizationUrl = options.authorizationUrl(callback.redirectUri);
2852
- const browserRunner = options.browserRunner ?? ((url) => new Promise((resolve2, reject) => {
2853
- let command = "xdg-open";
2854
- let args = [url.href];
2855
- if (process.platform === "darwin") {
2856
- command = "open";
2857
- } else if (process.platform === "win32") {
2858
- command = "cmd";
2859
- args = ["/c", "start", "", url.href];
2860
- }
2861
- const child = spawn(command, args, {
2862
- detached: true,
2863
- stdio: "ignore"
2864
- });
2865
- child.once("error", reject);
2866
- child.once("spawn", () => {
2867
- child.unref();
2868
- resolve2();
2869
- });
2870
- }));
2871
- let timer;
2872
- const timeout = new Promise((_resolve, reject) => {
2873
- timer = setTimeout(() => {
2874
- reject(new AuthError("AUTH_CALLBACK_TIMEOUT"));
2875
- }, options.timeoutMs ?? 12e4);
2876
- });
2877
- const launch = browserRunner(authorizationUrl).catch((cause) => {
2878
- throw new AuthError("AUTH_BROWSER_LAUNCH_FAILED", { cause });
2879
- });
2880
- try {
2881
- const completed = await Promise.race([
2882
- Promise.all([launch, callback.wait()]),
2883
- timeout
2884
- ]);
2885
- const callbackUrl = completed[1];
2886
- const actualState = callbackUrl.searchParams.get("state");
2887
- if (actualState === null) {
2888
- throw new AuthError("AUTH_CALLBACK_RESPONSE_INVALID");
2889
- }
2890
- const expectedState = Buffer.from(options.expectedState);
2891
- const receivedState = Buffer.from(actualState);
2892
- if (expectedState.length !== receivedState.length || !timingSafeEqual(expectedState, receivedState)) {
2893
- throw new AuthError("AUTH_CALLBACK_STATE_MISMATCH");
2894
- }
2895
- const code = callbackUrl.searchParams.get("code");
2896
- if (code === null || code.length === 0) {
2897
- throw new AuthError("AUTH_CALLBACK_RESPONSE_INVALID");
2898
- }
2899
- return { code, redirectUri: callback.redirectUri };
2900
- } finally {
2901
- if (timer !== void 0) {
2902
- clearTimeout(timer);
2846
+ // src/key-lease/scope-id/scope-id.ts
2847
+ import { createHash as createHash3 } from "node:crypto";
2848
+ var deriveScopeId = (input) => {
2849
+ let identity;
2850
+ switch (input.attribution.kind) {
2851
+ case "personal":
2852
+ identity = ["route33-lease", "personal"];
2853
+ break;
2854
+ case "project":
2855
+ if (input.folderRealpath === void 0 || input.folderRealpath.length === 0) {
2856
+ throw new TypeError("Project lease scope requires a real folder path");
2857
+ }
2858
+ identity = [
2859
+ "route33-lease",
2860
+ "project",
2861
+ input.attribution.projectId,
2862
+ input.folderRealpath
2863
+ ];
2864
+ break;
2865
+ default: {
2866
+ const unexpected = input.attribution;
2867
+ throw new TypeError(`Unexpected attribution: ${String(unexpected)}`);
2903
2868
  }
2904
- await callback.close();
2905
2869
  }
2870
+ return createHash3("sha256").update(JSON.stringify(identity)).digest("hex");
2906
2871
  };
2907
2872
 
2908
- // src/auth/pkce-verifier-and-challenge/pkce-verifier-and-challenge.ts
2909
- import { randomBytes } from "node:crypto";
2873
+ // src/requested-client-id/requested-client-id.ts
2874
+ var DEFAULT_ROUTE33_CLIENT_ID = false ? "" : "oa_platform_203623de";
2910
2875
 
2911
- // src/auth/pkce-challenge/pkce-challenge.ts
2912
- import { createHash as createHash3 } from "node:crypto";
2913
- var pkceChallenge = (verifier) => createHash3("sha256").update(verifier).digest("base64url");
2876
+ // src/commands/change-attribution/change-attribution.ts
2877
+ var changeAttribution = async (options) => {
2878
+ await options.clear();
2879
+ return options.prompt();
2880
+ };
2914
2881
 
2915
- // src/auth/pkce-verifier-and-challenge/pkce-verifier-and-challenge.ts
2916
- var createPkceVerifierAndChallenge = () => {
2917
- const codeVerifier = randomBytes(32).toString("base64url");
2882
+ // src/commands/inspect-session/inspect-session.ts
2883
+ var inspectSession = (options) => {
2884
+ const attribution = options.readAttribution();
2885
+ const lease = options.readLease();
2918
2886
  return {
2919
- codeChallenge: pkceChallenge(codeVerifier),
2920
- codeVerifier,
2921
- state: randomBytes(32).toString("base64url")
2887
+ attribution,
2888
+ ...lease === void 0 ? {} : { lease: { expiresAt: lease.expiresAt, keyId: lease.keyId } }
2922
2889
  };
2923
2890
  };
2924
2891
 
2925
- // src/auth/requested-scopes/requested-scopes.ts
2926
- var REQUESTED_OAUTH_SCOPES = "openid offline_access projects:read keys:read keys:write";
2892
+ // src/commands/reset-attribution/reset-attribution.ts
2893
+ var resetAttribution = async (options) => {
2894
+ await options.clear();
2895
+ };
2927
2896
 
2928
- // src/auth/login-flow/login-flow.ts
2929
- var loginFlow = async (options) => {
2930
- const log = options.logger ?? logger;
2931
- log.info("auth.login.started");
2932
- const pkce = createPkceVerifierAndChallenge();
2933
- const captured = await openBrowserAndCapture({
2934
- authorizationUrl: (redirectUri) => options.session.authorizationUrl({
2935
- clientId: options.clientId,
2936
- codeChallenge: pkce.codeChallenge,
2937
- redirectUri,
2938
- scope: REQUESTED_OAUTH_SCOPES,
2939
- state: pkce.state
2940
- }),
2941
- ...options.browserRunner === void 0 ? {} : { browserRunner: options.browserRunner },
2942
- expectedState: pkce.state,
2943
- ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
2944
- });
2945
- const tokenSet = await options.session.exchangeCode({
2946
- code: captured.code,
2947
- codeVerifier: pkce.codeVerifier,
2948
- redirectUri: captured.redirectUri
2897
+ // src/commands/rotate-now/rotate-now.ts
2898
+ var rotateNow = async (options) => {
2899
+ const rotated = await options.lease({ renewalRatio: Number.EPSILON });
2900
+ return rotated.lease;
2901
+ };
2902
+
2903
+ // src/ux/attribution-prompt/attribution-prompt.ts
2904
+ var createAttributionPrompt = (projects) => {
2905
+ const choicesBySelection = {
2906
+ personal: { kind: "personal" }
2907
+ };
2908
+ const projectOptions = projects.map((project) => {
2909
+ const selection = `project:${project.id}`;
2910
+ choicesBySelection[selection] = {
2911
+ kind: "project",
2912
+ projectId: project.id
2913
+ };
2914
+ return {
2915
+ label: `${project.name} (${project.organisation.name})`,
2916
+ selection
2917
+ };
2949
2918
  });
2950
- let credential;
2951
- try {
2952
- credential = parseManagementJwt(tokenSet.access_token);
2953
- } catch (cause) {
2954
- throw new AuthError("AUTH_TOKEN_RESPONSE_INVALID", { cause });
2919
+ return {
2920
+ choicesBySelection,
2921
+ options: [
2922
+ ...projectOptions,
2923
+ { label: "Use my personal key", selection: "personal" }
2924
+ ],
2925
+ title: "Choose where Route 33 should manage this key"
2926
+ };
2927
+ };
2928
+
2929
+ // src/ux/attribution-prompt-error/attribution-prompt-error.ts
2930
+ var MESSAGES = {
2931
+ ATTRIBUTION_PRESENTER_UNAVAILABLE: "Route 33 needs OpenCode v2 to assign a budget for this folder. Update to OpenCode v2, run /connect, choose Route 33, and follow the steps.",
2932
+ ATTRIBUTION_SELECTION_INVALID: "That Route 33 option is no longer available. Connect again and pick another."
2933
+ };
2934
+ var AttributionPromptError = class extends GatewayCredentialError {
2935
+ code;
2936
+ constructor(code) {
2937
+ super(MESSAGES[code]);
2938
+ this.name = "AttributionPromptError";
2939
+ this.code = code;
2955
2940
  }
2956
- log.info("auth.login.completed");
2957
- return { credential, tokenSet };
2941
+ };
2942
+
2943
+ // src/ux/prompt-flow/prompt-flow.ts
2944
+ var resolveAttribution = async (options) => {
2945
+ const remembered = readAttribution(options.state);
2946
+ if (remembered !== void 0) {
2947
+ return remembered;
2948
+ }
2949
+ if (options.presenter === void 0) {
2950
+ throw new AttributionPromptError("ATTRIBUTION_PRESENTER_UNAVAILABLE");
2951
+ }
2952
+ const plan = createAttributionPrompt(await options.loadProjects());
2953
+ const selection = await options.presenter.pick(plan);
2954
+ const attribution = plan.choicesBySelection[selection];
2955
+ if (attribution === void 0) {
2956
+ throw new AttributionPromptError("ATTRIBUTION_SELECTION_INVALID");
2957
+ }
2958
+ writeAttribution(options.state, attribution);
2959
+ return attribution;
2960
+ };
2961
+
2962
+ // src/ux/trusted-party-copy/trusted-party-copy.ts
2963
+ var TRUSTED_PARTY_COPY = {
2964
+ message: "Route 33 wants to manage your keys for this OpenCode installation.",
2965
+ title: "Connect Route 33"
2958
2966
  };
2959
2967
 
2960
2968
  // src/route33-runtime/login-session/login-session.ts
@@ -3161,7 +3169,7 @@ var Route33Runtime = class {
3161
3169
  if (typeof client !== "function") {
3162
3170
  return client;
3163
3171
  }
3164
- this.#managementClient ??= client(() => this.#session.credential());
3172
+ this.#managementClient ??= client();
3165
3173
  return this.#managementClient;
3166
3174
  }
3167
3175
  #subject(credential) {
@@ -3268,7 +3276,7 @@ var createWorkspaceKeyHeaders = ({ resolveKey, session }) => async (input, outpu
3268
3276
  // src/create-route33-plugin/create-route33-plugin.ts
3269
3277
  var createRoute33Plugin = (options = {}) => {
3270
3278
  const baseURL = readEnvBaseUrl();
3271
- const clientId = options.clientId ?? readEnvClientId() ?? DEFAULT_ROUTE33_CLIENT_ID;
3279
+ const clientId2 = options.clientId ?? readEnvClientId() ?? DEFAULT_ROUTE33_CLIENT_ID;
3272
3280
  const directKey = readEnvDirectApiKey();
3273
3281
  const resolvedConfig = {
3274
3282
  ...baseURL === void 0 ? {} : { baseURL },
@@ -3289,19 +3297,19 @@ var createRoute33Plugin = (options = {}) => {
3289
3297
  })
3290
3298
  });
3291
3299
  return (input) => {
3292
- const runtime = runtimeOptions === void 0 || installation === void 0 ? void 0 : new Route33Runtime({
3300
+ const runtime2 = runtimeOptions === void 0 || installation === void 0 ? void 0 : new Route33Runtime({
3293
3301
  ...runtimeOptions,
3294
- auth: { ...runtimeOptions.auth, clientId },
3302
+ auth: { ...runtimeOptions.auth, clientId: clientId2 },
3295
3303
  installation,
3296
3304
  state: {
3297
3305
  directory: input.worktree,
3298
3306
  outsideWorktree: "return-undefined"
3299
3307
  }
3300
3308
  });
3301
- if (runtime !== void 0) {
3302
- options.bindRuntime?.(runtime);
3309
+ if (runtime2 !== void 0) {
3310
+ options.bindRuntime?.(runtime2);
3303
3311
  if (options.registerCommands !== void 0) {
3304
- options.registerCommands(runtime.commandHandlers());
3312
+ options.registerCommands(runtime2.commandHandlers());
3305
3313
  }
3306
3314
  }
3307
3315
  let credentialProvider;
@@ -3309,14 +3317,14 @@ var createRoute33Plugin = (options = {}) => {
3309
3317
  let promptGate;
3310
3318
  if (directKey !== void 0) {
3311
3319
  credentialProvider = () => Promise.resolve(parseGatewayKey(directKey));
3312
- } else if (runtime === void 0) {
3320
+ } else if (runtime2 === void 0) {
3313
3321
  credentialProvider = () => Promise.reject(
3314
3322
  new AttributionPromptError("ATTRIBUTION_PRESENTER_UNAVAILABLE")
3315
3323
  );
3316
3324
  } else {
3317
- credentialProvider = () => runtime.resolveCredential();
3325
+ credentialProvider = () => runtime2.resolveCredential();
3318
3326
  workspaceHeaders = createWorkspaceKeyHeaders({
3319
- resolveKey: (directory) => runtime.resolveWorkspaceCredential(directory),
3327
+ resolveKey: (directory) => runtime2.resolveWorkspaceCredential(directory),
3320
3328
  session: {
3321
3329
  get: async ({ sessionID }) => {
3322
3330
  const session = await options.readSession?.(sessionID);
@@ -3751,20 +3759,20 @@ var HttpManagementKeys = class {
3751
3759
 
3752
3760
  // src/lazy-management-keys/lazy-management-keys.ts
3753
3761
  var lazyManagementKeys = (options) => {
3754
- const client = async () => new HttpManagementKeys({
3762
+ const client = (accessToken) => new HttpManagementKeys({
3755
3763
  baseUrl: options.apiBaseUrl,
3756
- credential: await options.obtainCredential(),
3764
+ credential: accessToken,
3757
3765
  ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
3758
3766
  });
3759
3767
  return {
3760
- create: async (accessToken, input) => (await client()).create(accessToken, input),
3761
- delete: async (accessToken, id) => (await client()).delete(accessToken, id),
3762
- get: async (accessToken, id) => (await client()).get(accessToken, id),
3763
- list: async (accessToken) => (await client()).list(accessToken),
3764
- listOrganizations: async (accessToken) => (await client()).listOrganizations(accessToken),
3765
- listProjects: async (accessToken, organisationId) => (await client()).listProjects(accessToken, organisationId),
3766
- regenerate: async (accessToken, id, expiresAt) => (await client()).regenerate(accessToken, id, expiresAt),
3767
- update: async (accessToken, id, input) => (await client()).update(accessToken, id, input)
3768
+ create: async (accessToken, input) => client(accessToken).create(accessToken, input),
3769
+ delete: async (accessToken, id) => client(accessToken).delete(accessToken, id),
3770
+ get: async (accessToken, id) => client(accessToken).get(accessToken, id),
3771
+ list: async (accessToken) => client(accessToken).list(accessToken),
3772
+ listOrganizations: async (accessToken) => client(accessToken).listOrganizations(accessToken),
3773
+ listProjects: async (accessToken, organisationId) => client(accessToken).listProjects(accessToken, organisationId),
3774
+ regenerate: async (accessToken, id, expiresAt) => client(accessToken).regenerate(accessToken, id, expiresAt),
3775
+ update: async (accessToken, id, input) => client(accessToken).update(accessToken, id, input)
3768
3776
  };
3769
3777
  };
3770
3778
 
@@ -3780,14 +3788,217 @@ var buildProductionRuntimeOptions = (input) => ({
3780
3788
  timeoutMs: LOGIN_BUDGET_MS
3781
3789
  },
3782
3790
  management: {
3783
- client: (obtainCredential) => lazyManagementKeys({
3784
- apiBaseUrl: input.apiBaseUrl,
3785
- obtainCredential
3786
- })
3791
+ client: () => lazyManagementKeys({ apiBaseUrl: input.apiBaseUrl })
3787
3792
  },
3788
3793
  ...input.tokenStore === void 0 ? {} : { tokenStore: input.tokenStore }
3789
3794
  });
3790
3795
 
3796
+ // src/v1-entry/host-auth-bridge/host-auth-bridge.ts
3797
+ var createHostAuthBridge = () => {
3798
+ let refresh;
3799
+ return {
3800
+ hydrate: (auth) => {
3801
+ if (typeof auth.refresh === "string" && auth.refresh !== "") {
3802
+ refresh = auth.refresh;
3803
+ }
3804
+ },
3805
+ tokenStore: {
3806
+ get: () => Promise.resolve(refresh),
3807
+ set: (token) => {
3808
+ refresh = token;
3809
+ return Promise.resolve();
3810
+ }
3811
+ }
3812
+ };
3813
+ };
3814
+
3815
+ // src/auth/authorization-url-stash/authorization-url-stash.ts
3816
+ var FALLBACK_URL = "https://route33.ai";
3817
+ var SIGN_IN_INSTRUCTIONS = "Open the Route 33 sign-in link \u2014 your browser will finish at a confirmation page.";
3818
+ var FINISHING_INSTRUCTIONS = "Finishing Route 33 setup\u2026";
3819
+ var URL_WAIT_BUDGET_MS = 3e4;
3820
+ var createAuthorizationUrlStash = (options = {}) => {
3821
+ const open = options.open ?? ((url) => openSystemBrowser(url));
3822
+ const waitBudgetMs = options.waitBudgetMs ?? URL_WAIT_BUDGET_MS;
3823
+ let resolveUrl;
3824
+ const nextUrl = new Promise((resolve2) => {
3825
+ resolveUrl = resolve2;
3826
+ });
3827
+ const browserRunner = (url) => {
3828
+ resolveUrl?.(url);
3829
+ return open(url);
3830
+ };
3831
+ const urlForConnect = async (callback) => {
3832
+ let decide;
3833
+ const decided = new Promise((resolve2) => {
3834
+ decide = resolve2;
3835
+ });
3836
+ const timer = setTimeout(() => {
3837
+ decide({ instructions: SIGN_IN_INSTRUCTIONS, url: FALLBACK_URL });
3838
+ }, waitBudgetMs);
3839
+ void nextUrl.then((url) => {
3840
+ clearTimeout(timer);
3841
+ decide({ instructions: SIGN_IN_INSTRUCTIONS, url: url.toString() });
3842
+ });
3843
+ void Promise.resolve(callback).catch(() => void 0).then(() => {
3844
+ clearTimeout(timer);
3845
+ decide({ instructions: FINISHING_INSTRUCTIONS, url: FALLBACK_URL });
3846
+ });
3847
+ return decided;
3848
+ };
3849
+ return { browserRunner, urlForConnect };
3850
+ };
3851
+
3852
+ // src/v1-entry/route33-auth-hook/route33-auth-hook.ts
3853
+ var createRoute33AuthHook = (deps) => ({
3854
+ loader: async (auth) => {
3855
+ deps.bridge.hydrate(await auth());
3856
+ return buildOptions(deps.providerBaseURL);
3857
+ },
3858
+ methods: [
3859
+ {
3860
+ authorize: async () => {
3861
+ const stash = createAuthorizationUrlStash();
3862
+ const login = deps.startLogin(stash.browserRunner);
3863
+ const decision = await stash.urlForConnect(login.then(() => void 0));
3864
+ return {
3865
+ callback: async () => {
3866
+ try {
3867
+ const { tokenSet } = await login;
3868
+ deps.bridge.hydrate({
3869
+ refresh: tokenSet.refresh_token,
3870
+ type: "oauth"
3871
+ });
3872
+ return {
3873
+ access: tokenSet.access_token,
3874
+ expires: Date.now() + tokenSet.expires_in * 1e3,
3875
+ provider: "route_33",
3876
+ refresh: tokenSet.refresh_token ?? "",
3877
+ type: "success"
3878
+ };
3879
+ } catch {
3880
+ return { type: "failed" };
3881
+ }
3882
+ },
3883
+ instructions: decision.instructions,
3884
+ method: "auto",
3885
+ url: decision.url
3886
+ };
3887
+ },
3888
+ label: "Route 33 (browser sign-in)",
3889
+ type: "oauth"
3890
+ }
3891
+ ],
3892
+ provider: "route_33"
3893
+ });
3894
+
3895
+ // src/v1-entry/host-modality-flags/host-modality-flags.ts
3896
+ var hostModalityFlags = (modalities) => ({
3897
+ audio: modalities.includes("audio"),
3898
+ image: modalities.includes("image"),
3899
+ pdf: modalities.includes("pdf"),
3900
+ text: modalities.includes("text"),
3901
+ video: modalities.includes("video")
3902
+ });
3903
+
3904
+ // src/v1-entry/host-model-capabilities/host-model-capabilities.ts
3905
+ var hostModelCapabilities = (meta, forceReasoning) => {
3906
+ const modalities = meta.modalities;
3907
+ const capabilities = meta.capabilities;
3908
+ const attachment = meta.attachment ?? capabilities.attachment ?? false;
3909
+ return {
3910
+ attachment,
3911
+ input: hostModalityFlags(modalities?.input ?? ["text"]),
3912
+ interleaved: false,
3913
+ output: hostModalityFlags(modalities?.output ?? ["text"]),
3914
+ reasoning: capabilities.reasoning === true || forceReasoning,
3915
+ temperature: capabilities.temperature === true,
3916
+ toolcall: capabilities.tool_call === true
3917
+ };
3918
+ };
3919
+
3920
+ // src/v1-entry/host-model-quantities/host-model-quantities.ts
3921
+ var ZERO_COST = {
3922
+ cache: { read: 0, write: 0 },
3923
+ input: 0,
3924
+ output: 0
3925
+ };
3926
+ var DEFAULT_LIMIT = { context: 2e5, output: 64e3 };
3927
+ var hostModelQuantities = (meta) => {
3928
+ const cost = meta.cost;
3929
+ const limit = meta.limit;
3930
+ return {
3931
+ cost: cost === void 0 ? ZERO_COST : {
3932
+ cache: {
3933
+ read: cost.cache_read ?? 0,
3934
+ write: cost.cache_write ?? 0
3935
+ },
3936
+ input: cost.input,
3937
+ output: cost.output
3938
+ },
3939
+ limit: limit === void 0 ? DEFAULT_LIMIT : {
3940
+ context: limit.context,
3941
+ output: limit.output
3942
+ }
3943
+ };
3944
+ };
3945
+
3946
+ // src/v1-entry/host-model-vendor/host-model-vendor.ts
3947
+ var hostModelVendor = (slug) => {
3948
+ const separator = slug.indexOf("/");
3949
+ const vendor = separator === -1 ? slug : slug.slice(0, separator);
3950
+ return {
3951
+ forceReasoning: vendor === "anthropic",
3952
+ npm: NATIVE_SDK_BY_VENDOR[vendor] ?? "@ai-sdk/openai-compatible"
3953
+ };
3954
+ };
3955
+
3956
+ // src/v1-entry/host-models/host-models.ts
3957
+ var toHostModels = (input) => {
3958
+ const models = {};
3959
+ for (const [slug, meta] of Object.entries(input.enriched)) {
3960
+ const vendor = hostModelVendor(slug);
3961
+ const capabilities = hostModelCapabilities(meta, vendor.forceReasoning);
3962
+ const quantities = hostModelQuantities(meta);
3963
+ models[slug] = {
3964
+ api: { id: slug, npm: vendor.npm, url: input.baseURL },
3965
+ attachment: capabilities.attachment,
3966
+ capabilities,
3967
+ cost: quantities.cost,
3968
+ headers: {},
3969
+ id: slug,
3970
+ limit: quantities.limit,
3971
+ name: meta.name,
3972
+ options: {},
3973
+ providerID: "route_33",
3974
+ release_date: "",
3975
+ status: "active"
3976
+ };
3977
+ }
3978
+ return models;
3979
+ };
3980
+
3981
+ // src/v1-entry/route33-provider-models/route33-provider-models.ts
3982
+ var createRoute33ProviderModels = (deps) => async (provider) => {
3983
+ const fallback = provider.models;
3984
+ const key = await deps.resolveKey();
3985
+ if (key === void 0) return fallback;
3986
+ const [catalog, modelsDev] = await Promise.all([
3987
+ (deps.loadCatalog ?? getCatalog)({
3988
+ ...deps.catalog,
3989
+ cacheDir: deps.cacheDir,
3990
+ gatewayKey: key
3991
+ }),
3992
+ deps.loadModelsDev()
3993
+ ]);
3994
+ const enriched = enrichCatalog(catalog, modelsDev);
3995
+ const models = toHostModels({
3996
+ baseURL: deps.catalog.baseURL,
3997
+ enriched
3998
+ });
3999
+ return Object.keys(models).length === 0 ? fallback : models;
4000
+ };
4001
+
3791
4002
  // src/contract/fixtures/fixtures.ts
3792
4003
  var contractFixtures = {
3793
4004
  apiKey: {
@@ -3835,13 +4046,64 @@ var parseContractFixtures = (fixtures) => ({
3835
4046
  });
3836
4047
 
3837
4048
  // src/index.ts
3838
- var Route33Plugin = createRoute33Plugin({
4049
+ var clientId = readEnvClientId() ?? DEFAULT_ROUTE33_CLIENT_ID;
4050
+ var issuerUrl = readEnvAuthIssuer() ?? DEFAULT_AUTH_ISSUER_URL;
4051
+ var authBridge = createHostAuthBridge();
4052
+ var startNativeLogin = (browserRunner) => loginFlow({
4053
+ browserRunner,
4054
+ clientId,
4055
+ session: createAuthSession({ clientId, issuerUrl }),
4056
+ timeoutMs: LOGIN_BUDGET_MS
4057
+ });
4058
+ var route33Plugin = createRoute33Plugin({
4059
+ bindRuntime: (instance) => {
4060
+ runtime = instance;
4061
+ },
3839
4062
  runtime: buildProductionRuntimeOptions({
3840
4063
  apiBaseUrl: readEnvApiBase() ?? DEFAULT_MANAGEMENT_API_BASE_URL,
3841
- clientId: readEnvClientId() ?? DEFAULT_ROUTE33_CLIENT_ID,
3842
- issuerUrl: readEnvAuthIssuer() ?? DEFAULT_AUTH_ISSUER_URL
4064
+ clientId,
4065
+ issuerUrl,
4066
+ tokenStore: authBridge.tokenStore
3843
4067
  })
3844
4068
  });
4069
+ var runtime;
4070
+ var modelsDevOnce;
4071
+ var Route33Plugin = async (input) => {
4072
+ const hooks = await route33Plugin(input);
4073
+ const native = readEnvDirectApiKey();
4074
+ return {
4075
+ ...hooks,
4076
+ ...native !== void 0 ? {} : {
4077
+ auth: createRoute33AuthHook({
4078
+ bridge: authBridge,
4079
+ providerBaseURL: readEnvBaseUrl() ?? DEFAULT_BASE_URL,
4080
+ startLogin: startNativeLogin
4081
+ }),
4082
+ provider: {
4083
+ id: "route_33",
4084
+ models: createRoute33ProviderModels({
4085
+ cacheDir: readEnvCacheDir(),
4086
+ catalog: {
4087
+ baseURL: readEnvBaseUrl() ?? DEFAULT_BASE_URL,
4088
+ catalogTtlMs: readEnvCatalogTtlMs(),
4089
+ disableIdShim: readEnvDisableIdShim(),
4090
+ discoveryEnabled: readEnvDiscovery()
4091
+ },
4092
+ loadModelsDev: () => {
4093
+ modelsDevOnce ??= loadModelsDev({
4094
+ cacheDir: readEnvCacheDir()
4095
+ });
4096
+ return modelsDevOnce;
4097
+ },
4098
+ resolveKey: () => {
4099
+ const active = runtime;
4100
+ return active === void 0 ? Promise.resolve(void 0) : active.resolveWorkspaceCredential(input.worktree);
4101
+ }
4102
+ })
4103
+ }
4104
+ }
4105
+ };
4106
+ };
3845
4107
  export {
3846
4108
  AttributionPromptError,
3847
4109
  AuthError,