@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/CHANGELOG.md +15 -0
- package/README.md +5 -2
- package/README.zh-CN.md +5 -2
- package/lib/client.js +715 -40
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +197 -1
- package/lib/index.js +509 -13
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|