@mars-sea/dsh-commandcode-provider 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -8,8 +8,10 @@ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
8
8
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
9
9
  import { existsSync, readFileSync } from "node:fs";
10
10
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
11
- import { randomUUID } from "node:crypto";
11
+ import { randomBytes, randomUUID } from "node:crypto";
12
12
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
13
+ import { createServer } from "node:http";
14
+ import { createServer as createServer$1 } from "node:net";
13
15
  //#region src/accounts.ts
14
16
  /**
15
17
  * Multi-account pool for the Command Code provider (host side).
@@ -1859,30 +1861,30 @@ const USAGE_REMOTE_PACKAGE = "@mars-sea/dsh-commandcode-provider";
1859
1861
  /** Canonical `<namespace>/<method>` endpoint of the usage report Remote. */
1860
1862
  const USAGE_REPORT_ENDPOINT = "commandcode/report";
1861
1863
  /** Reject one boundary value with a field-naming error. */
1862
- function reject(field) {
1864
+ function reject$1(field) {
1863
1865
  throw new TypeError(`commandcode/report result: invalid ${field}`);
1864
1866
  }
1865
1867
  /** Read one required finite number field (`field` is the dotted error label). */
1866
1868
  function numberField(source, key, field) {
1867
1869
  const value = source[key];
1868
- if (typeof value !== "number" || !Number.isFinite(value)) reject(field);
1870
+ if (typeof value !== "number" || !Number.isFinite(value)) reject$1(field);
1869
1871
  return value;
1870
1872
  }
1871
1873
  /** Read one required string field (`field` is the dotted error label). */
1872
1874
  function stringField(source, key, field) {
1873
1875
  const value = source[key];
1874
- if (typeof value !== "string") reject(field);
1876
+ if (typeof value !== "string") reject$1(field);
1875
1877
  return value;
1876
1878
  }
1877
1879
  /** Read one required boolean field (`field` is the dotted error label). */
1878
1880
  function booleanField(source, key, field) {
1879
1881
  const value = source[key];
1880
- if (typeof value !== "boolean") reject(field);
1882
+ if (typeof value !== "boolean") reject$1(field);
1881
1883
  return value;
1882
1884
  }
1883
1885
  /** Narrow an unknown value to a plain record, or reject. */
1884
1886
  function record(value, field) {
1885
- if (typeof value !== "object" || value === null || Array.isArray(value)) reject(field);
1887
+ if (typeof value !== "object" || value === null || Array.isArray(value)) reject$1(field);
1886
1888
  return value;
1887
1889
  }
1888
1890
  /** Validate one window-limit block (`fiveHour` / `weekly`). */
@@ -1903,11 +1905,11 @@ function windowLimit(value, field) {
1903
1905
  function parseUsageReport(value) {
1904
1906
  const source = record(value, "report");
1905
1907
  const failures = source.failures;
1906
- if (!Array.isArray(failures) || failures.some((entry) => typeof entry !== "string")) reject("failures");
1908
+ if (!Array.isArray(failures) || failures.some((entry) => typeof entry !== "string")) reject$1("failures");
1907
1909
  const report = { failures };
1908
1910
  if (source.blocked !== void 0) {
1909
1911
  const blocked = source.blocked;
1910
- if (blocked !== "invalid-key" && blocked !== "service-unavailable" && blocked !== "network") reject("blocked");
1912
+ if (blocked !== "invalid-key" && blocked !== "service-unavailable" && blocked !== "network") reject$1("blocked");
1911
1913
  report.blocked = blocked;
1912
1914
  }
1913
1915
  if (source.account !== void 0) {
@@ -1945,7 +1947,7 @@ function parseUsageReport(value) {
1945
1947
  if (source.plan !== void 0) {
1946
1948
  const plan = record(source.plan, "plan");
1947
1949
  const monthly = plan.monthlyCredits;
1948
- if (monthly !== null && (typeof monthly !== "number" || !Number.isFinite(monthly))) reject("plan.monthlyCredits");
1950
+ if (monthly !== null && (typeof monthly !== "number" || !Number.isFinite(monthly))) reject$1("plan.monthlyCredits");
1949
1951
  report.plan = {
1950
1952
  planId: stringField(plan, "planId", "plan.planId"),
1951
1953
  name: stringField(plan, "name", "plan.name"),
@@ -1972,7 +1974,7 @@ function parseAccountUsage(value) {
1972
1974
  /** Parse the wire result into a {@link CommandCodeAccountsReport}. */
1973
1975
  function parseAccountsReport(value) {
1974
1976
  const accounts = record(value, "result").accounts;
1975
- if (!Array.isArray(accounts)) reject("accounts");
1977
+ if (!Array.isArray(accounts)) reject$1("accounts");
1976
1978
  return { accounts: accounts.map(parseAccountUsage) };
1977
1979
  }
1978
1980
  /**
@@ -2001,6 +2003,82 @@ const USAGE_HOST_CONTRIBUTION = {
2001
2003
  }]
2002
2004
  };
2003
2005
  //#endregion
2006
+ //#region src/login-wire.ts
2007
+ /** The canonical endpoint paths of the three login Remotes. */
2008
+ const LOGIN_BEGIN_ENDPOINT = "commandcode/loginBegin";
2009
+ const LOGIN_STATUS_ENDPOINT = "commandcode/loginStatus";
2010
+ const LOGIN_CANCEL_ENDPOINT = "commandcode/loginCancel";
2011
+ const REASONS = [
2012
+ "denied",
2013
+ "timeout",
2014
+ "invalid-key",
2015
+ "network",
2016
+ "unavailable",
2017
+ "cancelled",
2018
+ "error"
2019
+ ];
2020
+ /** Reject one boundary value with a field-naming error. */
2021
+ function reject(field) {
2022
+ throw new TypeError(`commandcode/login result: invalid ${field}`);
2023
+ }
2024
+ /**
2025
+ * Parse one untrusted boundary value into a {@link CommandCodeLoginStatus}.
2026
+ * Every field is shape-checked so a malformed frame fails the boundary
2027
+ * instead of leaking into the page.
2028
+ */
2029
+ function parseLoginStatus(value) {
2030
+ if (typeof value !== "object" || value === null || Array.isArray(value)) reject("status");
2031
+ const source = value;
2032
+ const state = source.state;
2033
+ if (state !== "idle" && state !== "waiting" && state !== "success" && state !== "failed") reject("state");
2034
+ const status = { state };
2035
+ if (source.authUrl !== void 0) {
2036
+ if (typeof source.authUrl !== "string") reject("authUrl");
2037
+ status.authUrl = source.authUrl;
2038
+ }
2039
+ if (source.userName !== void 0) {
2040
+ if (typeof source.userName !== "string") reject("userName");
2041
+ status.userName = source.userName;
2042
+ }
2043
+ if (source.keyName !== void 0) {
2044
+ if (typeof source.keyName !== "string") reject("keyName");
2045
+ status.keyName = source.keyName;
2046
+ }
2047
+ if (source.reason !== void 0) {
2048
+ if (!REASONS.includes(source.reason)) reject("reason");
2049
+ status.reason = source.reason;
2050
+ }
2051
+ if (source.message !== void 0) {
2052
+ if (typeof source.message !== "string") reject("message");
2053
+ status.message = source.message;
2054
+ }
2055
+ return status;
2056
+ }
2057
+ /** The strict result codec shared by all three login endpoints. */
2058
+ const loginStatusSchema = { parse: parseLoginStatus };
2059
+ /** Build one login invocation descriptor (uniform result, no parameters). */
2060
+ function loginDescriptor(endpoint, method) {
2061
+ return {
2062
+ id: `${USAGE_REMOTE_PACKAGE}#${endpoint}`,
2063
+ service: "commandcodeUsage",
2064
+ namespace: "commandcode",
2065
+ method,
2066
+ invocation: { kind: "direct" },
2067
+ parameters: [],
2068
+ result: {
2069
+ mode: "strict",
2070
+ typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeLoginStatus`,
2071
+ schema: loginStatusSchema
2072
+ }
2073
+ };
2074
+ }
2075
+ /** The three login descriptors, shared verbatim by Host registration and Client mount. */
2076
+ const LOGIN_DESCRIPTORS = [
2077
+ loginDescriptor(LOGIN_BEGIN_ENDPOINT, "loginBegin"),
2078
+ loginDescriptor(LOGIN_STATUS_ENDPOINT, "loginStatus"),
2079
+ loginDescriptor(LOGIN_CANCEL_ENDPOINT, "loginCancel")
2080
+ ];
2081
+ //#endregion
2004
2082
  //#region src/usage-remote.ts
2005
2083
  /**
2006
2084
  * The Remote receiver: a Cordis service the Gateway resolves by key
@@ -2035,6 +2113,29 @@ var CommandCodeUsageService = class extends TypertRemoteService {
2035
2113
  report: await this.deps.adapter.getUsage()
2036
2114
  }] };
2037
2115
  }
2116
+ /**
2117
+ * Start (or rejoin) a browser-login attempt and return its fresh status —
2118
+ * `waiting` carrying the Studio URL. Rejects when the flow cannot start
2119
+ * (no free loopback port, disposed plugin); the Gateway folds the throw
2120
+ * into the failure branch the page renders.
2121
+ */
2122
+ async loginBegin() {
2123
+ return this.requireLogin().begin();
2124
+ }
2125
+ /** Poll a login attempt's status. */
2126
+ async loginStatus() {
2127
+ return this.deps.login?.status() ?? { state: "idle" };
2128
+ }
2129
+ /** Cancel a waiting attempt; returns the post-cancel status. */
2130
+ async loginCancel() {
2131
+ this.deps.login?.cancel();
2132
+ return this.deps.login?.status() ?? { state: "idle" };
2133
+ }
2134
+ requireLogin() {
2135
+ const login = this.deps.login;
2136
+ if (login === void 0) throw new Error("login flow is not wired in this setup; paste the API key instead");
2137
+ return login;
2138
+ }
2038
2139
  };
2039
2140
  /**
2040
2141
  * Provide the usage service and register its Remote descriptor. The registry
@@ -2044,11 +2145,395 @@ var CommandCodeUsageService = class extends TypertRemoteService {
2044
2145
  function applyUsageRemote(ctx, deps) {
2045
2146
  ctx.inject(["typert"], (remoteCtx) => {
2046
2147
  new CommandCodeUsageService(remoteCtx, deps);
2047
- const unregister = remoteCtx.typert.register(USAGE_HOST_CONTRIBUTION);
2148
+ const unregister = remoteCtx.typert.register({
2149
+ ...USAGE_HOST_CONTRIBUTION,
2150
+ invocations: [...USAGE_HOST_CONTRIBUTION.invocations, ...LOGIN_DESCRIPTORS]
2151
+ });
2048
2152
  remoteCtx.effect(() => () => void unregister(), "dsh-commandcode-provider: usage remote");
2049
2153
  });
2050
2154
  }
2051
2155
  //#endregion
2156
+ //#region src/login.ts
2157
+ /**
2158
+ * Host half of the Command Code browser login (the loopback flow).
2159
+ *
2160
+ * Mirrors what the official `command-code login` CLI command performs
2161
+ * (reverse-engineered from `command-code@1.32.1`, `createAuthFlowController`
2162
+ * + `createAuthServer` in its bundle):
2163
+ *
2164
+ * 1. Bind a temporary HTTP server on `127.0.0.1`, first available port from
2165
+ * 5959 upward (10 attempts).
2166
+ * 2. Generate a random state token and open
2167
+ * `{studio}/studio/auth/cli?callback=http://localhost:{port}/callback&state={state}`.
2168
+ * 3. After the user signs in, the Studio page POSTs the credentials JSON
2169
+ * `{ apiKey, state, userId, userName, keyName }` to the loopback callback —
2170
+ * no OAuth code exchange, the page holds the final API key.
2171
+ * 4. The delivered key is validated against `GET {apiBase}/alpha/whoami`
2172
+ * before anything is stored.
2173
+ *
2174
+ * Server behaviour is mirrored exactly: POST-only `/callback`, a 10 KB body
2175
+ * cap, JSON responses (`{success:true}` / `{success:false,error}`), CORS for
2176
+ * the Studio origins only, and state-token equality as the anti-forgery
2177
+ * check. One deliberate hardening over the CLI build: the CORS origin is
2178
+ * echoed only when it is allowlisted (the CLI falls back to the first
2179
+ * origin), which browsers treat identically.
2180
+ *
2181
+ * Storage stays out of this module: the plugin entry supplies
2182
+ * {@link CommandCodeLoginFlowDeps.storeKey}, which writes through the dsh
2183
+ * credentials seam so the next request resolves the new key with no restart.
2184
+ * Everything external (fetch, ports, randomness, timing) is injectable for
2185
+ * node tests; the tests drive a real loopback server end to end.
2186
+ *
2187
+ * @module dsh-commandcode-provider/login
2188
+ */
2189
+ /** Give up on the browser after this long without a callback (mirrors the CLI). */
2190
+ const LOGIN_TIMEOUT_MS = 12e4;
2191
+ /** First local port the flow tries (mirrors the CLI). */
2192
+ const LOGIN_START_PORT = 5959;
2193
+ /** How many consecutive ports to try from {@link LOGIN_START_PORT}. */
2194
+ const LOGIN_MAX_PORT_ATTEMPTS = 10;
2195
+ /** Reject callback bodies larger than this (mirrors the CLI). */
2196
+ const LOGIN_BODY_LIMIT_BYTES = 1e4;
2197
+ /** The Studio origins allowed to POST credentials to the loopback server. */
2198
+ const LOGIN_ALLOWED_ORIGINS = [
2199
+ "http://localhost:3000",
2200
+ "https://staging.commandcode.ai",
2201
+ "https://commandcode.ai"
2202
+ ];
2203
+ /** The Studio route that performs the browser-side login. */
2204
+ const STUDIO_AUTH_PATH = "/studio/auth/cli";
2205
+ /** Compose the Studio authorization URL (pure, exported for tests). */
2206
+ function buildCommandAuthUrl(options) {
2207
+ const callback = `http://localhost:${options.port}/callback`;
2208
+ return `${options.studioBase}${STUDIO_AUTH_PATH}?callback=${encodeURIComponent(callback)}&state=${encodeURIComponent(options.state)}`;
2209
+ }
2210
+ /** Map an API base onto the Studio base the CLI pairs it with. */
2211
+ function studioBaseForApiBase(apiBase) {
2212
+ if (/^https:\/\/staging-api\.commandcode\.ai/i.test(apiBase)) return "https://staging.commandcode.ai";
2213
+ if (/^http:\/\/localhost(:\d+)?$/i.test(apiBase)) return "http://localhost:3000";
2214
+ return "https://commandcode.ai";
2215
+ }
2216
+ /**
2217
+ * Validate one candidate key against `/alpha/whoami` (pure, exported for
2218
+ * tests). Mirrors the CLI's verdicts: 401 → invalid_key, other non-OK →
2219
+ * server_error, transport failure → network_error.
2220
+ */
2221
+ async function validateCommandApiKey(fetchImpl, apiBase, apiKey) {
2222
+ try {
2223
+ const response = await fetchImpl(`${apiBase}/alpha/whoami`, {
2224
+ method: "GET",
2225
+ headers: {
2226
+ "Content-Type": "application/json",
2227
+ Authorization: `Bearer ${apiKey}`
2228
+ }
2229
+ });
2230
+ if (response.status === 401) return {
2231
+ valid: false,
2232
+ error: "invalid_key"
2233
+ };
2234
+ if (response.ok) return { valid: true };
2235
+ return {
2236
+ valid: false,
2237
+ error: "server_error"
2238
+ };
2239
+ } catch {
2240
+ return {
2241
+ valid: false,
2242
+ error: "network_error"
2243
+ };
2244
+ }
2245
+ }
2246
+ /** Whether one loopback port is free right now. */
2247
+ function checkPortAvailable(port) {
2248
+ return new Promise((resolve) => {
2249
+ const probe = createServer$1();
2250
+ probe.once("error", () => resolve(false));
2251
+ probe.once("listening", () => probe.close(() => resolve(true)));
2252
+ probe.listen(port, "127.0.0.1");
2253
+ });
2254
+ }
2255
+ /** Whether a callback body carries every credential field the CLI requires. */
2256
+ function isCallbackCredentials(value) {
2257
+ if (typeof value !== "object" || value === null) return false;
2258
+ const record = value;
2259
+ return typeof record.apiKey === "string" && record.apiKey !== "" && typeof record.state === "string" && typeof record.userId === "string" && typeof record.userName === "string" && typeof record.keyName === "string";
2260
+ }
2261
+ /**
2262
+ * One browser-login attempt machine. Single-flight by design: `begin()` while
2263
+ * waiting returns the live attempt's status instead of starting a second one;
2264
+ * a terminal state makes the next `begin()` start fresh.
2265
+ */
2266
+ var CommandCodeLoginFlow = class {
2267
+ deps;
2268
+ listeners = /* @__PURE__ */ new Set();
2269
+ statusValue = { state: "idle" };
2270
+ server;
2271
+ timer;
2272
+ /** Settle hooks of the live attempt's callback promise. */
2273
+ settle;
2274
+ disposed = false;
2275
+ constructor(deps) {
2276
+ this.deps = deps;
2277
+ }
2278
+ /** Subscribe to state transitions. @returns the disposer. */
2279
+ onChange(listener) {
2280
+ this.listeners.add(listener);
2281
+ return () => this.listeners.delete(listener);
2282
+ }
2283
+ /** The current attempt's status face. */
2284
+ status() {
2285
+ return this.statusValue;
2286
+ }
2287
+ /**
2288
+ * Start an attempt (or rejoin the live one) and resolve with its status —
2289
+ * `waiting` carrying the Studio URL once the loopback server is up.
2290
+ * Rejects only when the flow cannot start at all (no free port, disposed).
2291
+ */
2292
+ async begin() {
2293
+ if (this.disposed) throw new Error("login flow has been disposed");
2294
+ if (this.statusValue.state === "waiting") return this.statusValue;
2295
+ this.teardown();
2296
+ const port = await this.findPort();
2297
+ const expectedState = this.deps.randomToken?.(32) ?? randomBytes(32).toString("base64url");
2298
+ const settled = new Promise((resolve, reject) => {
2299
+ this.settle = {
2300
+ resolve,
2301
+ reject
2302
+ };
2303
+ });
2304
+ await this.bindServer(port, expectedState);
2305
+ const apiBase = this.readApiBase();
2306
+ this.setStatus({
2307
+ state: "waiting",
2308
+ authUrl: buildCommandAuthUrl({
2309
+ studioBase: studioBaseForApiBase(apiBase),
2310
+ port,
2311
+ state: expectedState
2312
+ })
2313
+ });
2314
+ this.timer = setTimeout(() => {
2315
+ this.teardown();
2316
+ this.setStatus({
2317
+ state: "failed",
2318
+ reason: "timeout",
2319
+ message: "No browser callback arrived within the login window."
2320
+ });
2321
+ }, this.deps.timeoutMs ?? 12e4);
2322
+ this.timer.unref?.();
2323
+ settled.then((credentials) => this.complete(credentials), (failure) => this.failFrom(failure));
2324
+ return this.statusValue;
2325
+ }
2326
+ /** Cancel a waiting attempt; terminal states are untouched. */
2327
+ cancel() {
2328
+ if (this.disposed || this.statusValue.state !== "waiting") return;
2329
+ this.teardown();
2330
+ this.setStatus({
2331
+ state: "failed",
2332
+ reason: "cancelled"
2333
+ });
2334
+ }
2335
+ /** Stop everything; a waiting attempt ends cancelled. Idempotent. */
2336
+ dispose() {
2337
+ if (this.disposed) return;
2338
+ this.disposed = true;
2339
+ const wasWaiting = this.statusValue.state === "waiting";
2340
+ this.teardown();
2341
+ if (wasWaiting) this.setStatus({
2342
+ state: "failed",
2343
+ reason: "cancelled"
2344
+ });
2345
+ }
2346
+ readApiBase() {
2347
+ return (typeof this.deps.apiBase === "function" ? this.deps.apiBase() : this.deps.apiBase) ?? "https://api.commandcode.ai";
2348
+ }
2349
+ setStatus(next) {
2350
+ this.statusValue = next;
2351
+ for (const listener of [...this.listeners]) listener();
2352
+ }
2353
+ /** First free port among the consecutive candidates. */
2354
+ async findPort() {
2355
+ const startPort = this.deps.startPort ?? 5959;
2356
+ const attempts = this.deps.maxPortAttempts ?? 10;
2357
+ for (let index = 0; index < attempts; index += 1) {
2358
+ const candidate = startPort + index;
2359
+ if (await checkPortAvailable(candidate)) return candidate;
2360
+ }
2361
+ throw new Error(`No available port found after ${attempts} attempts starting from port ${startPort}`);
2362
+ }
2363
+ /**
2364
+ * Bind the attempt's loopback server, resolving when the port is live.
2365
+ * Pre-bind failures reject (surfacing from `begin()`); a later server error
2366
+ * settles the live attempt as a tagged failure instead.
2367
+ */
2368
+ bindServer(port, expectedState) {
2369
+ return new Promise((resolve, reject) => {
2370
+ let binding = true;
2371
+ const server = createServer((request, response) => this.handleCallback(request, response, expectedState));
2372
+ this.server = server;
2373
+ server.once("error", (error) => {
2374
+ if (this.server !== server) return;
2375
+ this.server = void 0;
2376
+ const tagged = new LoginSettleError("error", `Could not bind the login callback server on port ${port}: ${error.code ?? error.message}`);
2377
+ if (binding) {
2378
+ binding = false;
2379
+ reject(tagged);
2380
+ } else this.settle?.reject(tagged);
2381
+ });
2382
+ server.listen(port, "127.0.0.1", () => {
2383
+ if (!binding) return;
2384
+ binding = false;
2385
+ resolve();
2386
+ });
2387
+ });
2388
+ }
2389
+ /** One request against the attempt's callback endpoint (CLI-mirrored). */
2390
+ handleCallback(request, response, expectedState) {
2391
+ response.setHeader("Connection", "close");
2392
+ response.setHeader("Access-Control-Allow-Origin", corsOrigin(request.headers.origin));
2393
+ response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
2394
+ response.setHeader("Access-Control-Allow-Headers", "Content-Type");
2395
+ response.setHeader("Content-Type", "application/json");
2396
+ const json = (code, body) => {
2397
+ response.writeHead(code);
2398
+ response.end(JSON.stringify(body));
2399
+ };
2400
+ if (request.method === "OPTIONS") {
2401
+ response.writeHead(204);
2402
+ response.end();
2403
+ return;
2404
+ }
2405
+ if ((request.url?.split("?")[0] ?? "/") !== "/callback") {
2406
+ json(404, {
2407
+ success: false,
2408
+ error: "Not found"
2409
+ });
2410
+ return;
2411
+ }
2412
+ if (request.method !== "POST") {
2413
+ json(405, {
2414
+ success: false,
2415
+ error: "Method not allowed. Use POST."
2416
+ });
2417
+ return;
2418
+ }
2419
+ let body = "";
2420
+ request.on("data", (chunk) => {
2421
+ body += chunk.toString();
2422
+ if (body.length > 1e4) request.destroy();
2423
+ });
2424
+ request.on("end", () => {
2425
+ let payload;
2426
+ try {
2427
+ payload = JSON.parse(body);
2428
+ } catch {
2429
+ json(400, {
2430
+ success: false,
2431
+ error: "Invalid JSON"
2432
+ });
2433
+ return;
2434
+ }
2435
+ if (typeof payload === "object" && payload !== null && "error" in payload) {
2436
+ const denial = payload;
2437
+ const description = denial.error_description ?? denial.error;
2438
+ this.settleAttempt(json, 200, { success: true }, new LoginSettleError(denial.error === "access_denied" ? "denied" : "error", typeof description === "string" && description !== "" ? description : "Authorization failed"));
2439
+ return;
2440
+ }
2441
+ if (!isCallbackCredentials(payload)) {
2442
+ json(400, {
2443
+ success: false,
2444
+ error: "Missing required fields"
2445
+ });
2446
+ return;
2447
+ }
2448
+ if (payload.state !== expectedState) {
2449
+ json(403, {
2450
+ success: false,
2451
+ error: "Invalid state token"
2452
+ });
2453
+ return;
2454
+ }
2455
+ this.settleAttempt(json, 200, { success: true }, void 0, { ...payload });
2456
+ });
2457
+ request.on("error", () => {});
2458
+ }
2459
+ /** Answer a decisive callback, stop listening, and settle the attempt. */
2460
+ settleAttempt(json, code, body, failure, credentials) {
2461
+ json(code, body);
2462
+ const settle = this.settle;
2463
+ this.teardown();
2464
+ if (settle === void 0) return;
2465
+ if (failure !== void 0) settle.reject(failure);
2466
+ else if (credentials !== void 0) settle.resolve(credentials);
2467
+ }
2468
+ /** Post-validation completion: whoami check, then hand-off to storage. */
2469
+ async complete(credentials) {
2470
+ if (this.disposed || this.statusValue.state !== "waiting") return;
2471
+ const validation = await validateCommandApiKey(this.deps.fetchImpl ?? fetch, this.readApiBase(), credentials.apiKey);
2472
+ if (!validation.valid) {
2473
+ const reason = validation.error === "invalid_key" ? "invalid-key" : validation.error === "network_error" ? "network" : "error";
2474
+ this.setStatus({
2475
+ state: "failed",
2476
+ reason,
2477
+ message: `/alpha/whoami rejected the delivered key (${validation.error}).`
2478
+ });
2479
+ return;
2480
+ }
2481
+ try {
2482
+ await this.deps.storeKey(credentials);
2483
+ } catch (error) {
2484
+ this.setStatus({
2485
+ state: "failed",
2486
+ reason: "unavailable",
2487
+ message: error instanceof Error ? error.message : String(error)
2488
+ });
2489
+ return;
2490
+ }
2491
+ if (this.disposed) return;
2492
+ this.clearTimer();
2493
+ this.setStatus({
2494
+ state: "success",
2495
+ userName: credentials.userName,
2496
+ keyName: credentials.keyName
2497
+ });
2498
+ }
2499
+ /** Map a tagged settle rejection onto the status face. */
2500
+ failFrom(failure) {
2501
+ if (!(failure instanceof LoginSettleError)) return;
2502
+ if (this.disposed || this.statusValue.state !== "waiting") return;
2503
+ this.setStatus({
2504
+ state: "failed",
2505
+ reason: failure.reason,
2506
+ message: failure.message
2507
+ });
2508
+ }
2509
+ clearTimer() {
2510
+ if (this.timer !== void 0) {
2511
+ clearTimeout(this.timer);
2512
+ this.timer = void 0;
2513
+ }
2514
+ }
2515
+ /** Close the server and watchdog without touching the published status. */
2516
+ teardown() {
2517
+ this.clearTimer();
2518
+ this.server?.close();
2519
+ this.server = void 0;
2520
+ this.settle = void 0;
2521
+ }
2522
+ };
2523
+ /** A tagged settle failure carrying the stable copy reason. */
2524
+ var LoginSettleError = class extends Error {
2525
+ reason;
2526
+ constructor(reason, message) {
2527
+ super(message);
2528
+ this.reason = reason;
2529
+ this.name = "LoginSettleError";
2530
+ }
2531
+ };
2532
+ /** Echo the Origin header only when the Studio allowlist contains it. */
2533
+ function corsOrigin(origin) {
2534
+ return origin !== void 0 && LOGIN_ALLOWED_ORIGINS.includes(origin) ? origin : "";
2535
+ }
2536
+ //#endregion
2052
2537
  //#region src/index.ts
2053
2538
  /**
2054
2539
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
@@ -2229,9 +2714,20 @@ function apply(ctx, config) {
2229
2714
  reports: usageReports
2230
2715
  });
2231
2716
  });
2717
+ const loginFlow = new CommandCodeLoginFlow({
2718
+ apiBase: () => options().apiBase,
2719
+ storeKey: async ({ apiKey }) => {
2720
+ const ref = credentialRef(current().apiKeyEnv ?? DEFAULT_API_KEY_ENV);
2721
+ const credentials = ctx.get("credentials");
2722
+ if (credentials === void 0) throw new Error("the credentials service is unavailable in this profile; paste the key manually");
2723
+ await credentials.set(ref, apiKey);
2724
+ }
2725
+ });
2726
+ ctx.effect(() => () => loginFlow.dispose(), "dsh-commandcode-provider: login flow");
2232
2727
  applyUsageRemote(ctx, {
2233
2728
  adapter,
2234
- reports: usageReports
2729
+ reports: usageReports,
2730
+ login: loginFlow
2235
2731
  });
2236
2732
  installSettingsSection(ctx, NS, Config, config, {
2237
2733
  setSource: (source) => {
@@ -2241,6 +2737,6 @@ function apply(ctx, config) {
2241
2737
  });
2242
2738
  }
2243
2739
  //#endregion
2244
- export { BILLING_ACCESS_TTL_MS, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommands, applyUsageRemote, capabilityDescription, commandDefinition, compareByPlan, dealLabel, formatContext, inject, modelVisibleInPlan, name, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectActiveAccount, subscriptionPlanInfo, usageReportSchema };
2740
+ export { BILLING_ACCESS_TTL_MS, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommands, applyUsageRemote, buildCommandAuthUrl, capabilityDescription, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectActiveAccount, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
2245
2741
 
2246
2742
  //# sourceMappingURL=index.js.map