@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0-preview.20260912
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/gui/dist/assets/index-D_t6sCWs.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
clearPruneFailure,
|
|
18
18
|
countSnapshots,
|
|
19
19
|
findOperation,
|
|
20
|
+
findCommittedOperation,
|
|
20
21
|
listOperations,
|
|
21
22
|
markPruneFailure,
|
|
22
23
|
pruneSnapshots,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
deleteRecord,
|
|
33
34
|
integrationsDir,
|
|
34
35
|
readRecords,
|
|
36
|
+
readRecordsStrict,
|
|
35
37
|
writeRecord,
|
|
36
38
|
type OwnershipRecord,
|
|
37
39
|
} from "./ownership";
|
|
@@ -41,6 +43,7 @@ export interface IntegrationStateStore {
|
|
|
41
43
|
/** The integrations directory itself — never a config root to resolve again. */
|
|
42
44
|
readonly root: string;
|
|
43
45
|
readRecords(): Partial<Record<IntegrationClientId, OwnershipRecord>>;
|
|
46
|
+
readRecordsStrict(): Partial<Record<IntegrationClientId, OwnershipRecord>>;
|
|
44
47
|
putRecord(record: OwnershipRecord): void;
|
|
45
48
|
dropRecord(clientId: IntegrationClientId): void;
|
|
46
49
|
appendJournal(entry: JournalEntry): void;
|
|
@@ -48,6 +51,7 @@ export interface IntegrationStateStore {
|
|
|
48
51
|
retireOperation(record: JournalTombstone): void;
|
|
49
52
|
listOperations(clientId?: IntegrationClientId, limit?: number): JournalEntry[];
|
|
50
53
|
findOperation(opId: string): JournalEntry | null;
|
|
54
|
+
findCommittedOperation(opId: string): JournalEntry | null;
|
|
51
55
|
captureSnapshot(clientId: IntegrationClientId, opId: string, text: string | null): SnapshotRef;
|
|
52
56
|
readSnapshot(entry: JournalEntry):
|
|
53
57
|
| { kind: "none" }
|
|
@@ -79,12 +83,14 @@ export function createIntegrationStateStore(root: string = integrationsDir()): I
|
|
|
79
83
|
const store: IntegrationStateStore = {
|
|
80
84
|
root: dir,
|
|
81
85
|
readRecords: () => readRecords(dir),
|
|
86
|
+
readRecordsStrict: () => readRecordsStrict(dir),
|
|
82
87
|
putRecord: record => writeRecord(record, dir),
|
|
83
88
|
dropRecord: clientId => deleteRecord(clientId, dir),
|
|
84
89
|
appendJournal: entry => appendOperation(entry, dir),
|
|
85
90
|
retireOperation: record => appendTombstone(record, dir),
|
|
86
91
|
listOperations: (clientId, limit) => listOperations(clientId, limit, dir),
|
|
87
92
|
findOperation: opId => findOperation(opId, dir),
|
|
93
|
+
findCommittedOperation: opId => findCommittedOperation(opId, dir),
|
|
88
94
|
captureSnapshot: (clientId, opId, text) => captureSnapshot(clientId, opId, text, dir),
|
|
89
95
|
readSnapshot: entry => readSnapshot(entry, dir),
|
|
90
96
|
countSnapshots: clientId => countSnapshots(clientId, dir),
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* Design of record: devlog/_fin/260802_client_toggle_api/030 and 031.
|
|
11
11
|
*/
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
+
import { createClineIO, ClineTransactionError } from "./cline-io";
|
|
14
|
+
import { parseClineDocument, serializeClineDocument, preserveClineSelection } from "./cline-document";
|
|
13
15
|
import { dirname } from "node:path";
|
|
14
16
|
import { EXPORT_CLIENTS, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
15
17
|
import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
@@ -35,7 +37,29 @@ import { serializeDocument, UnserializableValueError } from "./serialize";
|
|
|
35
37
|
import { ClientPathError } from "../clients/config-export";
|
|
36
38
|
import { matchesOperationResult, newOpId, type JournalEntry } from "./journal";
|
|
37
39
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
38
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
patchYamlFragmentSource,
|
|
42
|
+
sourcePrunableYamlContainers,
|
|
43
|
+
yamlFragmentUnsupportedStyle,
|
|
44
|
+
} from "./omp-yaml-source";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* "comments or formatting" used to be the only refusal this path could report.
|
|
48
|
+
* For a flow-style container that names a cause which is not in the file, and
|
|
49
|
+
* DSH writes that shape itself, so the misdirection was routine rather than
|
|
50
|
+
* exotic: users went looking for a comment that was never there (#4260).
|
|
51
|
+
*/
|
|
52
|
+
function yamlRefusalReason(
|
|
53
|
+
source: string,
|
|
54
|
+
path: readonly string[],
|
|
55
|
+
configPath: string,
|
|
56
|
+
outcome: string,
|
|
57
|
+
): string {
|
|
58
|
+
if (yamlFragmentUnsupportedStyle(source, path)) {
|
|
59
|
+
return `${configPath} writes ${path.join(".")} as a flow mapping or sequence, a YAML style opencodex will not re-render, so ${outcome}`;
|
|
60
|
+
}
|
|
61
|
+
return `${configPath} uses YAML source opencodex cannot patch without risking unrelated comments or formatting, so ${outcome}`;
|
|
62
|
+
}
|
|
39
63
|
import { withIntegrationWriterLock, type IntegrationWriterLockSeams } from "./writer-lock";
|
|
40
64
|
|
|
41
65
|
export type RefusalReason =
|
|
@@ -126,13 +150,19 @@ interface CommitArgs {
|
|
|
126
150
|
*/
|
|
127
151
|
function commit(args: CommitArgs): WriteOutcome {
|
|
128
152
|
const { io, clientId, configPath } = args;
|
|
153
|
+
let transactionStarted = false;
|
|
129
154
|
try {
|
|
155
|
+
if (io.beginTransaction) {
|
|
156
|
+
io.beginTransaction(args);
|
|
157
|
+
transactionStarted = true;
|
|
158
|
+
}
|
|
130
159
|
if (args.nextText === null) io.removeFile(configPath);
|
|
131
160
|
else {
|
|
132
161
|
io.mkdirp(dirname(configPath));
|
|
133
162
|
io.writeText(configPath, args.nextText);
|
|
134
163
|
}
|
|
135
164
|
} catch (error) {
|
|
165
|
+
if (transactionStarted) return compensate(args, error, "could not write both Cline files");
|
|
136
166
|
return refuse(clientId, "write_failed", args.state, messageOf(error), args.snapshotPath);
|
|
137
167
|
}
|
|
138
168
|
try {
|
|
@@ -146,6 +176,7 @@ function commit(args: CommitArgs): WriteOutcome {
|
|
|
146
176
|
} catch (error) {
|
|
147
177
|
return compensate(args, error, "could not append the journal row");
|
|
148
178
|
}
|
|
179
|
+
io.finishTransaction?.();
|
|
149
180
|
return {
|
|
150
181
|
ok: true,
|
|
151
182
|
changed: true,
|
|
@@ -176,6 +207,7 @@ function compensate(args: CommitArgs, cause: unknown, what: string): WriteRefuse
|
|
|
176
207
|
message: `${what}, and the change could not be rolled back. The file or its ownership record is in an intermediate state; the backup is at ${args.snapshotPath ?? "(none)"}.`,
|
|
177
208
|
};
|
|
178
209
|
}
|
|
210
|
+
io.finishTransaction?.();
|
|
179
211
|
return refuse(clientId, "write_failed", args.state, `${what}; the change was rolled back. Cause: ${messageOf(cause)}`, args.snapshotPath);
|
|
180
212
|
}
|
|
181
213
|
|
|
@@ -198,7 +230,7 @@ function sourcePreservingFragmentValue(
|
|
|
198
230
|
/** Shared preflight: detect, gate, read, parse and classify. */
|
|
199
231
|
function preflight(input: IntegrationWriteInput) {
|
|
200
232
|
const store = input.store ?? createIntegrationStateStore();
|
|
201
|
-
|
|
233
|
+
let io = input.io ?? defaultIntegrationIO(store);
|
|
202
234
|
const clientId = input.clientId;
|
|
203
235
|
const spec = INTEGRATION_CLIENTS[clientId];
|
|
204
236
|
const exportSpec = EXPORT_CLIENTS[clientId];
|
|
@@ -224,7 +256,11 @@ function preflight(input: IntegrationWriteInput) {
|
|
|
224
256
|
const resolved = input.resolvedPaths ?? resolveIntegrationPaths(clientId, input.env, input.home);
|
|
225
257
|
configPath = resolved.configPath;
|
|
226
258
|
detectDir = resolved.detectDir;
|
|
259
|
+
if (clientId === "cline") io = createClineIO(io, configPath, store, true);
|
|
227
260
|
} catch (error) {
|
|
261
|
+
if (error instanceof ClineTransactionError) {
|
|
262
|
+
return { failed: { ...refuse(clientId, "unsafe", "unsafe", error.message, error.snapshotPath), residual: true } } as const;
|
|
263
|
+
}
|
|
228
264
|
if (!(error instanceof ClientPathError)) throw error;
|
|
229
265
|
return { failed: refuse(clientId, "unsafe", "unsafe", error.message) } as const;
|
|
230
266
|
}
|
|
@@ -240,7 +276,7 @@ function preflight(input: IntegrationWriteInput) {
|
|
|
240
276
|
} as const;
|
|
241
277
|
}
|
|
242
278
|
const before = target.before;
|
|
243
|
-
const parsed = parseConfig(before, exportSpec.format);
|
|
279
|
+
const parsed = clientId === "cline" ? parseClineDocument(before) : parseConfig(before, exportSpec.format);
|
|
244
280
|
if (parsed === PARSE_FAILED) {
|
|
245
281
|
return { failed: refuse(clientId, "unsafe", "unsafe",
|
|
246
282
|
`${configPath} could not be parsed, or holds something opencodex cannot rewrite without changing it (a non-finite number, a large integer or a tiny one a rewrite would round, -0, a duplicate member, or nesting deeper than 1000 levels)`) } as const;
|
|
@@ -387,6 +423,7 @@ function applyOrRefreshIntegration(
|
|
|
387
423
|
// every container exists and "did we create this?" is unanswerable.
|
|
388
424
|
created = createdContainerPaths(base, contribution);
|
|
389
425
|
const nextDocument = mergeContribution(base, contribution);
|
|
426
|
+
if (clientId === "cline") preserveClineSelection(parsed, nextDocument);
|
|
390
427
|
if (spec.sourcePreservingYaml && before !== null) {
|
|
391
428
|
const value = sourcePreservingFragmentValue(contribution, spec.sourcePreservingYaml.path);
|
|
392
429
|
const patched = value === undefined
|
|
@@ -399,11 +436,11 @@ function applyOrRefreshIntegration(
|
|
|
399
436
|
);
|
|
400
437
|
if (patched === null) {
|
|
401
438
|
return refuse(clientId, "unsafe", "unsafe",
|
|
402
|
-
|
|
439
|
+
yamlRefusalReason(before, spec.sourcePreservingYaml.path, configPath, "it was left alone"));
|
|
403
440
|
}
|
|
404
441
|
text = patched;
|
|
405
442
|
} else {
|
|
406
|
-
text = serializeDocument(nextDocument, exportSpec.format);
|
|
443
|
+
text = clientId === "cline" ? serializeClineDocument(nextDocument) : serializeDocument(nextDocument, exportSpec.format);
|
|
407
444
|
}
|
|
408
445
|
} catch (error) {
|
|
409
446
|
if (error instanceof AmbiguousSelectorError) {
|
|
@@ -536,7 +573,7 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
536
573
|
: recordedCreated;
|
|
537
574
|
if (prunableCreated === null) {
|
|
538
575
|
return refuse(clientId, "unsafe", "unsafe",
|
|
539
|
-
|
|
576
|
+
yamlRefusalReason(before ?? "", spec.sourcePreservingYaml!.path, configPath, "nothing was removed"));
|
|
540
577
|
}
|
|
541
578
|
let doc: unknown;
|
|
542
579
|
let removed: boolean;
|
|
@@ -559,11 +596,11 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
559
596
|
}, doc);
|
|
560
597
|
if (patched === null) {
|
|
561
598
|
return refuse(clientId, "unsafe", "unsafe",
|
|
562
|
-
|
|
599
|
+
yamlRefusalReason(before, spec.sourcePreservingYaml.path, configPath, "nothing was removed"));
|
|
563
600
|
}
|
|
564
601
|
text = patched;
|
|
565
602
|
} else {
|
|
566
|
-
text = serializeDocument(doc, exportSpec.format);
|
|
603
|
+
text = clientId === "cline" ? serializeClineDocument(doc) : serializeDocument(doc, exportSpec.format);
|
|
567
604
|
}
|
|
568
605
|
} catch (error) {
|
|
569
606
|
if (!(error instanceof UnserializableValueError)) throw error;
|
|
@@ -593,7 +630,7 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
593
630
|
|
|
594
631
|
export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome {
|
|
595
632
|
const store = input.store ?? createIntegrationStateStore();
|
|
596
|
-
|
|
633
|
+
let io = input.io ?? defaultIntegrationIO(store);
|
|
597
634
|
const entry = store.findOperation(input.opId);
|
|
598
635
|
if (!entry) throw new Error(`unknown operation ${input.opId}`);
|
|
599
636
|
if (entry.clientId !== input.clientId) throw new Error("restore input names a different client than the operation");
|
|
@@ -609,6 +646,13 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
609
646
|
return refuse(clientId, "conflict", "conflict",
|
|
610
647
|
`that operation was recorded for ${configPath}, but this client now resolves to ${resolvedPath}`);
|
|
611
648
|
}
|
|
649
|
+
if (clientId === "cline") {
|
|
650
|
+
try { io = createClineIO(io, configPath, store, true); }
|
|
651
|
+
catch (error) {
|
|
652
|
+
if (!(error instanceof ClineTransactionError)) throw error;
|
|
653
|
+
return { ...refuse(clientId, "unsafe", "unsafe", error.message, error.snapshotPath), residual: true };
|
|
654
|
+
}
|
|
655
|
+
}
|
|
612
656
|
const snapshot = store.readSnapshot(entry);
|
|
613
657
|
if (snapshot.kind === "expired") {
|
|
614
658
|
return refuse(clientId, "snapshot_expired", "absent", "that backup has expired");
|
|
@@ -3,7 +3,7 @@ import { jcsStringify } from "../digest";
|
|
|
3
3
|
import type { LabBehaviorSource, LabBehaviorValues } from "../live/types";
|
|
4
4
|
|
|
5
5
|
const CLOSED_KEYS = new Set([
|
|
6
|
-
"wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.modelSuffixMode",
|
|
6
|
+
"wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.chatCompletionsPath", "wire.commandCodeVersion", "wire.modelSuffixMode",
|
|
7
7
|
"auth.mode", "auth.transport",
|
|
8
8
|
"responses.stateful", "responses.upstreamStreaming", "responses.serviceTier", "responses.fastWireKind", "responses.fastWireValue", "responses.snapshotRepair", "responses.itemIdRepair",
|
|
9
9
|
"limits.contextWindow", "limits.maxInputTokens", "limits.maxOutputTokens",
|
package/src/lib/abort.ts
CHANGED
|
@@ -144,3 +144,39 @@ export function cancelBodyOnAbort(body: ReadableStream<Uint8Array> | null, signa
|
|
|
144
144
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
145
145
|
return () => signal.removeEventListener("abort", onAbort);
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compose multiple AbortSignals into a single signal that aborts when ANY input
|
|
150
|
+
* aborts. Uses `AbortSignal.any` when available (Node >=20.3 / Bun >=1.0);
|
|
151
|
+
* falls back to a manual implementation for older runtimes.
|
|
152
|
+
*
|
|
153
|
+
* The caller gets a `cleanup` alongside the signal and must call it once the
|
|
154
|
+
* work it guards has settled. `{ once: true }` only removes a listener that
|
|
155
|
+
* actually fired, so on the polyfill path a long-lived parent signal - a proxy
|
|
156
|
+
* session's, say - accumulates one listener per request until it aborts or the
|
|
157
|
+
* process exits. `signalWithTimeout` in this file has always had that
|
|
158
|
+
* discipline; this function did not.
|
|
159
|
+
*/
|
|
160
|
+
export function anySignal(signals: AbortSignal[]): { signal: AbortSignal; cleanup: () => void } {
|
|
161
|
+
const builtin = (AbortSignal as unknown as { any?: (s: AbortSignal[]) => AbortSignal }).any;
|
|
162
|
+
if (typeof builtin === "function") return { signal: builtin(signals), cleanup: () => {} };
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const listeners: Array<[AbortSignal, () => void]> = [];
|
|
165
|
+
const cleanup = (): void => {
|
|
166
|
+
for (const [source, handler] of listeners.splice(0)) source.removeEventListener("abort", handler);
|
|
167
|
+
};
|
|
168
|
+
const onAbort = (reason: unknown): void => {
|
|
169
|
+
if (!controller.signal.aborted) controller.abort(reason);
|
|
170
|
+
cleanup();
|
|
171
|
+
};
|
|
172
|
+
for (const s of signals) {
|
|
173
|
+
if (s.aborted) {
|
|
174
|
+
onAbort(s.reason);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
const handler = () => onAbort(s.reason);
|
|
178
|
+
listeners.push([s, handler]);
|
|
179
|
+
s.addEventListener("abort", handler);
|
|
180
|
+
}
|
|
181
|
+
return { signal: controller.signal, cleanup };
|
|
182
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 1. `localManagementOrigin` — authenticated management discovery/state (`/api/*`). It is
|
|
8
8
|
* served by the public listener and, on a hub, additionally by the loopback-only
|
|
9
9
|
* `hub.managementIngress`. Callers must still send a management credential: management
|
|
10
|
-
* authentication has no loopback bypass (structure/
|
|
10
|
+
* authentication has no loopback bypass (structure/gui-and-management-api.md), and the unauthenticated loopback
|
|
11
11
|
* listener deliberately does not serve `/api/*` at all.
|
|
12
12
|
* 2. `localInferenceDestination` — the data plane a client wire actually speaks.
|
|
13
13
|
*
|
|
@@ -16,6 +16,39 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { clearableDeadline } from "./abort";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Responses the origin may already be executing. RFC 9110 §9.2.2 forbids an intermediary
|
|
21
|
+
* from automatically repeating a non-idempotent request; when a post-send transport (the
|
|
22
|
+
* Codex WebSocket relay) settles a gateway status because the origin never acknowledged the
|
|
23
|
+
* turn, the request body must not be sent again by this process — not by the transient-5xx
|
|
24
|
+
* layer below, not by a pool account rotation, not by a combo hop. The status is returned
|
|
25
|
+
* to the caller so the user agent can apply its own retry policy, exactly as it does on the
|
|
26
|
+
* direct path. The WeakSet is the in-process marker; the structured error codes are the
|
|
27
|
+
* marker that survives body re-wrapping (combo failure consumption re-parses the JSON).
|
|
28
|
+
*/
|
|
29
|
+
const nonReplayableResponses = new WeakSet<Response>();
|
|
30
|
+
|
|
31
|
+
export function markResponseNonReplayable(response: Response): void {
|
|
32
|
+
nonReplayableResponses.add(response);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isNonReplayableResponse(response: Response): boolean {
|
|
36
|
+
return nonReplayableResponses.has(response);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Origin never produced a response event; the turn may still be executing. */
|
|
40
|
+
export const UPSTREAM_NO_RESPONSE_CODE = "upstream_no_response";
|
|
41
|
+
/** Transport closed after the send, before any response event. */
|
|
42
|
+
export const UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE = "upstream_closed_before_response";
|
|
43
|
+
const NON_REPLAYABLE_UPSTREAM_CODES: ReadonlySet<string> = new Set([
|
|
44
|
+
UPSTREAM_NO_RESPONSE_CODE,
|
|
45
|
+
UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export function isNonReplayableUpstreamCode(code: unknown): boolean {
|
|
49
|
+
return typeof code === "string" && NON_REPLAYABLE_UPSTREAM_CODES.has(code);
|
|
50
|
+
}
|
|
51
|
+
|
|
19
52
|
// 1 initial + 2 retries: the pool may hold more than one stale socket.
|
|
20
53
|
const RESET_RETRY_MAX_ATTEMPTS = 3;
|
|
21
54
|
const RESET_RETRY_BASE_DELAY_MS = 150;
|
|
@@ -395,7 +428,9 @@ export async function fetchWithTransientRetry(
|
|
|
395
428
|
let attemptStart = Date.now();
|
|
396
429
|
let res = await fetchWithResetRetry(countedFetch, { ...opts, attempts: remaining() });
|
|
397
430
|
for (let attempt = 0; sent < budget; attempt++) {
|
|
398
|
-
|
|
431
|
+
// A non-replayable gateway status was settled after the request body had already left
|
|
432
|
+
// for the origin; retrying it here is the automatic resend the marker exists to forbid.
|
|
433
|
+
if (res.ok || !isTransientUpstreamStatus(res.status) || isNonReplayableResponse(res)) return res;
|
|
399
434
|
// Checked before cancelResponseBodyBestEffort so an already-aborted caller never receives
|
|
400
435
|
// a response whose body we just cancelled.
|
|
401
436
|
if (opts.abortSignal?.aborted) return res;
|
|
@@ -67,6 +67,17 @@ function headroomOf(provider: string, accountId: string): number | null {
|
|
|
67
67
|
return 100 - Math.max(...percents);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Remaining headroom percent for one account, or null when nothing has measured it.
|
|
72
|
+
*
|
|
73
|
+
* Exported for the generic fill-first threshold, which needs the measurement itself rather
|
|
74
|
+
* than an ordering. Null stays null all the way out: a caller must decide what "unmeasured"
|
|
75
|
+
* means for its own rule instead of being handed a fabricated 0 or 100.
|
|
76
|
+
*/
|
|
77
|
+
export function accountHeadroomPercent(provider: string, accountId: string): number | null {
|
|
78
|
+
return headroomOf(provider, accountId);
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
/** Unknown usage is not exhaustion; Kiro's explicit overage verdict is authoritative. */
|
|
71
82
|
export function isAccountQuotaExhausted(provider: string, accountId: string): boolean {
|
|
72
83
|
const exhaustion = provider === "kiro" ? getKiroAccountExhaustion(`${provider}\u0000${accountId}`) : null;
|
|
@@ -37,6 +37,14 @@ function errorHtml(message: string): string {
|
|
|
37
37
|
|
|
38
38
|
export type CallbackResult = { code: string; state: string };
|
|
39
39
|
|
|
40
|
+
/** Close every response so pooled sockets cannot send the next login to a retired flow. */
|
|
41
|
+
function closingResponse(body: string, status: number, contentType = "text/html"): Response {
|
|
42
|
+
return new Response(body, {
|
|
43
|
+
status,
|
|
44
|
+
headers: { "Content-Type": contentType, "Connection": "close" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
/**
|
|
41
49
|
* The redirect URI advertised to providers must stay `localhost` (it is what the OAuth
|
|
42
50
|
* apps have registered), but Windows commonly resolves `localhost` to `::1` first while
|
|
@@ -177,7 +185,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
177
185
|
#handleCallback(req: Request, expectedState: string): Response {
|
|
178
186
|
const url = new URL(req.url);
|
|
179
187
|
if (url.pathname !== this.callbackPath) {
|
|
180
|
-
return
|
|
188
|
+
return closingResponse("Not Found", 404, "text/plain");
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
const code = url.searchParams.get("code");
|
|
@@ -214,10 +222,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
214
222
|
});
|
|
215
223
|
}
|
|
216
224
|
|
|
217
|
-
return
|
|
218
|
-
status: ok ? 200 : consumeFlow ? 500 : 400,
|
|
219
|
-
headers: { "Content-Type": "text/html" },
|
|
220
|
-
});
|
|
225
|
+
return closingResponse(ok ? SUCCESS_HTML : errorHtml(errMessage), ok ? 200 : consumeFlow ? 500 : 400);
|
|
221
226
|
}
|
|
222
227
|
|
|
223
228
|
#waitForCallback(expectedState: string): Promise<CallbackResult> {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlist for the Cognition/Devin api-server origin.
|
|
3
|
+
*
|
|
4
|
+
* RegisterUser returns the tenant's api-server host, and that host then receives
|
|
5
|
+
* GetUserJwt, GetCascadeModelConfigs and GetChatMessage - the first of which
|
|
6
|
+
* carries the long-lived api_key. A host taken from the network without
|
|
7
|
+
* validation turns a spoofed or compromised RegisterUser response into
|
|
8
|
+
* credential exfiltration, so every value that reaches a request URL or the
|
|
9
|
+
* credential store passes through here first.
|
|
10
|
+
*
|
|
11
|
+
* This lives in its own module rather than in `../devin.ts` because the
|
|
12
|
+
* credential store imports the validator and `../devin.ts` imports the store's
|
|
13
|
+
* sibling types; a shared leaf keeps that from becoming a cycle.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const DEVIN_DEFAULT_API_SERVER = "https://server.codeium.com";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the normalized api-server base URL, or undefined when the input is not
|
|
20
|
+
* an allowlisted Cognition host.
|
|
21
|
+
*
|
|
22
|
+
* Unlike the Copilot equivalent this keeps the path. EU and FedStart tenants are
|
|
23
|
+
* reached at `https://eu.windsurf.com/_route/api_server`, so the path prefix is
|
|
24
|
+
* part of the address rather than decoration, and normalizing to the origin
|
|
25
|
+
* would silently point those accounts at the wrong service.
|
|
26
|
+
*/
|
|
27
|
+
export function validateDevinApiBaseUrl(raw: string | undefined | null): string | undefined {
|
|
28
|
+
if (raw === undefined || raw === null) return undefined;
|
|
29
|
+
const trimmed = String(raw).trim();
|
|
30
|
+
if (!trimmed) return undefined;
|
|
31
|
+
let parsed: URL;
|
|
32
|
+
try {
|
|
33
|
+
parsed = new URL(trimmed);
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (parsed.protocol !== "https:") return undefined;
|
|
38
|
+
if (parsed.username || parsed.password) return undefined;
|
|
39
|
+
if (parsed.port && parsed.port !== "443") return undefined;
|
|
40
|
+
if (parsed.search || parsed.hash) return undefined;
|
|
41
|
+
const host = parsed.hostname.toLowerCase();
|
|
42
|
+
if (host === "localhost" || host.endsWith(".localhost")) return undefined;
|
|
43
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":")) return undefined;
|
|
44
|
+
const allowed =
|
|
45
|
+
host === "server.codeium.com" ||
|
|
46
|
+
// The shipped client (Devin Desktop 3.9.19,
|
|
47
|
+
// Contents/Resources/app/extensions/windsurf/dist/extension.js) also names
|
|
48
|
+
// these two, and a beta account's RegisterUser can return one.
|
|
49
|
+
host === "server-staging.codeium.com" ||
|
|
50
|
+
host === "server-beta.codeium.com" ||
|
|
51
|
+
host === "windsurf.com" ||
|
|
52
|
+
host.endsWith(".windsurf.com") ||
|
|
53
|
+
host === "windsurf.fedstart.com";
|
|
54
|
+
if (!allowed) return undefined;
|
|
55
|
+
const path = parsed.pathname.replace(/\/+$/, "");
|
|
56
|
+
if (path && !/^(\/[A-Za-z0-9._-]+)+$/.test(path)) return undefined;
|
|
57
|
+
return `https://${host}${path}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Same check, falling back to the default US host when the input is unusable. */
|
|
61
|
+
export function resolveDevinApiBaseUrl(raw: string | undefined | null): string {
|
|
62
|
+
return validateDevinApiBaseUrl(raw) ?? DEVIN_DEFAULT_API_SERVER;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { loginDevin } from "../devin";
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exchange a Firebase ID token for a long-lived Cognition/Devin API key.
|
|
3
|
+
*
|
|
4
|
+
* This calls the same Connect-RPC endpoint the Devin desktop client uses
|
|
5
|
+
* after browser sign-in completes:
|
|
6
|
+
*
|
|
7
|
+
* POST https://register.windsurf.com/exa.seat_management_pb.SeatManagementService/RegisterUser
|
|
8
|
+
* Content-Type: application/json
|
|
9
|
+
* Body: { "firebase_id_token": "<jwt>" }
|
|
10
|
+
*
|
|
11
|
+
* Connect-RPC happily accepts plain JSON over HTTPS (no gRPC framing required),
|
|
12
|
+
* so we skip @connectrpc/connect entirely and use `fetch`. The response shape
|
|
13
|
+
* matches `exa.seat_management_pb.RegisterUserResponse`:
|
|
14
|
+
*
|
|
15
|
+
* { api_key, name, api_server_url, redirect_url, team_options[] }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { OAuthLoginResult, WindsurfRegion } from './types.js';
|
|
19
|
+
import { anySignal } from '../../lib/abort.js';
|
|
20
|
+
import { validateDevinApiBaseUrl } from './api-base.js';
|
|
21
|
+
|
|
22
|
+
interface RegisterUserResponseJson {
|
|
23
|
+
api_key?: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
api_server_url?: string;
|
|
26
|
+
redirect_url?: string;
|
|
27
|
+
team_options?: unknown[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ConnectErrorJson {
|
|
31
|
+
code?: string;
|
|
32
|
+
message?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class WindsurfRegistrationError extends Error {
|
|
36
|
+
readonly status: number;
|
|
37
|
+
readonly connectCode?: string;
|
|
38
|
+
readonly traceId?: string;
|
|
39
|
+
|
|
40
|
+
constructor(message: string, status: number, connectCode?: string, traceId?: string) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'WindsurfRegistrationError';
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.connectCode = connectCode;
|
|
45
|
+
this.traceId = traceId;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const TRACE_ID_RE = /\(trace ID: ([0-9a-f]+)\)/i;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Connect error codes that are safe to repeat to the user.
|
|
53
|
+
*
|
|
54
|
+
* The message body is not: a Connect error can echo the request, and the
|
|
55
|
+
* request here is the Firebase ID token. That message reaches CLI output and
|
|
56
|
+
* /api/logs, and redactSecretString does not recognise a bare JWT, so the code
|
|
57
|
+
* is the only part of an error body that leaves this function.
|
|
58
|
+
*/
|
|
59
|
+
const SAFE_CONNECT_CODES = new Set([
|
|
60
|
+
'canceled', 'unknown', 'invalid_argument', 'deadline_exceeded', 'not_found', 'already_exists',
|
|
61
|
+
'permission_denied', 'resource_exhausted', 'failed_precondition', 'aborted', 'out_of_range',
|
|
62
|
+
'unimplemented', 'internal', 'unavailable', 'data_loss', 'unauthenticated',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function safeConnectCode(value: unknown): string | undefined {
|
|
66
|
+
return typeof value === 'string' && SAFE_CONNECT_CODES.has(value) ? value : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Exchange the Firebase ID token for a Windsurf API key.
|
|
71
|
+
*
|
|
72
|
+
* `firebaseIdToken` is the `access_token` (or `firebase_id_token`) value the
|
|
73
|
+
* Windsurf sign-in page returns in the OAuth callback URL — we treat it as
|
|
74
|
+
* opaque.
|
|
75
|
+
*/
|
|
76
|
+
export async function registerUser(
|
|
77
|
+
firebaseIdToken: string,
|
|
78
|
+
region: WindsurfRegion,
|
|
79
|
+
abortSignal?: AbortSignal,
|
|
80
|
+
): Promise<OAuthLoginResult> {
|
|
81
|
+
if (!firebaseIdToken) {
|
|
82
|
+
throw new WindsurfRegistrationError('Empty firebase_id_token', 0, 'invalid_argument');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The register host reaches the network holding the Firebase ID token, so it
|
|
86
|
+
// passes the same allowlist as the api-server host rather than being trusted
|
|
87
|
+
// because it came from a config object.
|
|
88
|
+
const registerBase = validateDevinApiBaseUrl(region.registerApiServerUrl);
|
|
89
|
+
if (!registerBase) {
|
|
90
|
+
throw new WindsurfRegistrationError(
|
|
91
|
+
'Refusing to send the sign-in token to a non-Cognition register host.',
|
|
92
|
+
0,
|
|
93
|
+
'permission_denied',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const url = `${registerBase}/exa.seat_management_pb.SeatManagementService/RegisterUser`;
|
|
97
|
+
|
|
98
|
+
// 30s internal timeout — RegisterUser responds in ~200ms in steady state.
|
|
99
|
+
// CLI users on flaky networks need bounded waits or the sign-in command
|
|
100
|
+
// hangs forever. Compose with the caller's signal via a small polyfill
|
|
101
|
+
// (`anySignal`) because Node 18 / older Bun lack AbortSignal.any; the
|
|
102
|
+
// previous fallback `combinedSignal = abortSignal` would drop the
|
|
103
|
+
// timeout entirely on those runtimes.
|
|
104
|
+
const timeoutSignal = AbortSignal.timeout(30_000);
|
|
105
|
+
const composed = abortSignal ? anySignal([abortSignal, timeoutSignal]) : undefined;
|
|
106
|
+
const combinedSignal: AbortSignal = composed?.signal ?? timeoutSignal;
|
|
107
|
+
|
|
108
|
+
let response: Response;
|
|
109
|
+
try {
|
|
110
|
+
response = await fetch(url, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
// Connect protocol version header — not strictly required for JSON, but
|
|
115
|
+
// matches what the official Connect clients send and avoids accidental
|
|
116
|
+
// routing into a non-Connect HTTP handler.
|
|
117
|
+
'Connect-Protocol-Version': '1',
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({ firebase_id_token: firebaseIdToken }),
|
|
120
|
+
// A 307/308 would replay this POST, and its body is the sign-in token, at
|
|
121
|
+
// whatever host Location names. Fail instead of following.
|
|
122
|
+
redirect: 'error',
|
|
123
|
+
signal: combinedSignal,
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
// Detach from the caller's signal; it can outlive this one exchange.
|
|
127
|
+
composed?.cleanup();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const text = await response.text();
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
let connectCode: string | undefined;
|
|
134
|
+
let traceId: string | undefined;
|
|
135
|
+
try {
|
|
136
|
+
const errJson = JSON.parse(text) as ConnectErrorJson;
|
|
137
|
+
connectCode = safeConnectCode(errJson.code);
|
|
138
|
+
// The trace id is an opaque server identifier and is the one part of the
|
|
139
|
+
// message worth keeping for a support conversation.
|
|
140
|
+
traceId = typeof errJson.message === 'string' ? errJson.message.match(TRACE_ID_RE)?.[1] : undefined;
|
|
141
|
+
} catch {
|
|
142
|
+
// Non-JSON error body. It stays unread; only the status is reported.
|
|
143
|
+
}
|
|
144
|
+
const message = `RegisterUser failed (HTTP ${response.status}${connectCode ? `, ${connectCode}` : ''}${traceId ? `, trace ${traceId}` : ''})`;
|
|
145
|
+
throw new WindsurfRegistrationError(message, response.status, connectCode, traceId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let parsed: RegisterUserResponseJson;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(text) as RegisterUserResponseJson;
|
|
151
|
+
} catch {
|
|
152
|
+
throw new WindsurfRegistrationError(
|
|
153
|
+
// The body is not echoed: a 200 that fails to parse can still contain the
|
|
154
|
+
// key or the token that produced it.
|
|
155
|
+
`RegisterUser returned 200 with a body that is not JSON (${text.length} bytes)`,
|
|
156
|
+
response.status,
|
|
157
|
+
'internal',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const apiKey = parsed.api_key;
|
|
162
|
+
// Empty `api_server_url` is normal for single-tenant accounts — the desktop
|
|
163
|
+
// extension's `getApiServerUrl` helper falls back to the configured default
|
|
164
|
+
// when this is empty/missing. We mirror that behavior here.
|
|
165
|
+
const apiServerUrl = parsed.api_server_url && parsed.api_server_url.length > 0
|
|
166
|
+
? parsed.api_server_url
|
|
167
|
+
: 'https://server.codeium.com';
|
|
168
|
+
|
|
169
|
+
if (!apiKey) {
|
|
170
|
+
throw new WindsurfRegistrationError(
|
|
171
|
+
'RegisterUser returned 200 but api_key was empty',
|
|
172
|
+
response.status,
|
|
173
|
+
'malformed_response',
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
// `name` is optional in the response — default it instead of failing login.
|
|
177
|
+
// src/oauth/devin.ts uses it only as a display label for the account email.
|
|
178
|
+
const name = parsed.name && parsed.name.length > 0 ? parsed.name : 'Devin account';
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
apiKey,
|
|
182
|
+
name,
|
|
183
|
+
apiServerUrl,
|
|
184
|
+
redirectUrl: parsed.redirect_url,
|
|
185
|
+
};
|
|
186
|
+
}
|