@yansigit/opencodex 2.33.0 → 2.35.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.
Files changed (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -27,6 +27,9 @@ const PROVIDER_ALIASES: Record<string, string> = {
27
27
  "google-antigravity": "google",
28
28
  "antigravity": "google",
29
29
  "gemini-antigravity": "google",
30
+ "google-aistudio": "google",
31
+ "aistudio": "google",
32
+ "gemini-aistudio": "google",
30
33
  "deepseek": "deepseek",
31
34
  "moonshot": "moonshot",
32
35
  "zhipu-bigmodel": "zai",
@@ -274,10 +274,11 @@ export interface ImageBridgeDeps {
274
274
  /** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
275
275
  onUsage?: (usage: OcxUsage | undefined) => void;
276
276
  /**
277
- * Optional 429 key-failover for the routed (non-xAI) model. Return a rebuilt adapter for the
278
- * rotated key, or null when the pool is exhausted.
277
+ * Optional 429 failover for the routed (non-xAI) model. Return a rebuilt adapter for the
278
+ * rotated credential, or null when the pool is exhausted. Async hooks support OAuth refresh;
279
+ * existing synchronous key-pool hooks remain valid.
279
280
  */
280
- on429?: (retryAfterHeader: string | null) => ProviderAdapter | null;
281
+ on429?: (retryAfterHeader: string | null) => ProviderAdapter | null | Promise<ProviderAdapter | null>;
281
282
  /** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
282
283
  retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
283
284
  /** Called when the bridged Responses stream completes (parity with runTurn / routed paths). */
@@ -592,7 +593,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
592
593
  }
593
594
  // 429 key-failover parity with web-search / normal routed path.
594
595
  while (prepared.response.status === 429 && deps.on429) {
595
- const rotated = deps.on429(prepared.response.headers.get("retry-after"));
596
+ const rotated = await deps.on429(prepared.response.headers.get("retry-after"));
596
597
  if (!rotated) break;
597
598
  try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
598
599
  adapter = rotated;
@@ -274,7 +274,7 @@
274
274
  "assertions": [
275
275
  { "id": "text", "operator": "normalized_text_equals", "selector": "/client/response/normalizedText", "expected": "OK", "required": true },
276
276
  { "id": "terminal", "operator": "terminal_signal_equals", "selector": "/client/response/terminal", "expected": "completed", "required": true },
277
- { "id": "phase", "operator": "json_path_equals", "selector": "/client/response/events/6/data/item/phase", "expected": "final_answer", "required": true }
277
+ { "id": "phase", "operator": "json_path_equals", "selector": "/client/response/events/7/data/item/phase", "expected": "final_answer", "required": true }
278
278
  ]
279
279
  },
280
280
  {
@@ -128,7 +128,7 @@ async function main(): Promise<void> {
128
128
  writeLine({ type: "result", patch });
129
129
  }
130
130
 
131
- main().catch((error: unknown) => {
131
+ await main().catch((error: unknown) => {
132
132
  writeLine({
133
133
  type: "error",
134
134
  code: "harness_failure",
@@ -40,6 +40,7 @@ const INITIAL_OWNED_PATHS = [
40
40
  "artifacts",
41
41
  "auth.json",
42
42
  "auth.store.lock",
43
+ "admin-api-token",
43
44
  "catalog-backup.json",
44
45
  "claude-env.sh",
45
46
  "codex-accounts.json",
@@ -333,6 +334,25 @@ export function removeOwnedConfigState(configDir: string): ConfigRemovalResult {
333
334
  }
334
335
  }
335
336
 
337
+ // Per-catalog backups are named `catalog-backup-<16 hex>.json` (catalogBackupPathFor), one per
338
+ // CODEX_HOME, so they cannot be enumerated as literal manifest entries the way every other
339
+ // owned file can. Without this, `ocx uninstall` always reported "unowned files remain" and
340
+ // refused to remove a home OpenCodex created itself — the file is unambiguously ours, produced
341
+ // by our own writer, and the strict hex shape keeps the match from widening.
342
+ for (const name of readdirSync(configDir)) {
343
+ if (!/^catalog-backup-[0-9a-f]{16}\.json$/.test(name)) continue;
344
+ const path = join(configDir, name);
345
+ try {
346
+ removeOwnedEntry(rootPath, path);
347
+ } catch (error) {
348
+ return {
349
+ status: "partial",
350
+ reason: `could not remove owned path ${name}: ${error instanceof Error ? error.message : String(error)}`,
351
+ residualPaths: [path],
352
+ };
353
+ }
354
+ }
355
+
336
356
  try {
337
357
  unlinkSync(join(configDir, CONFIG_UNINSTALL_MANIFEST));
338
358
  unlinkSync(join(configDir, CONFIG_OWNER_FILE));
package/src/lib/errors.ts CHANGED
@@ -16,11 +16,18 @@ export class OcxRequestValidationError extends Error {
16
16
 
17
17
  /** OpenAI / Codex hard block for high-risk cybersecurity activity (HTTP 400 or mid-stream). */
18
18
  export const CYBER_POLICY_ERROR_CODE = "cyber_policy";
19
+ export const CYBER_POLICY_FALLBACK_MESSAGE = "Request blocked by the upstream cybersecurity policy.";
19
20
 
20
21
  export function isCyberPolicyCode(code: string | null | undefined): boolean {
21
22
  return code === CYBER_POLICY_ERROR_CODE;
22
23
  }
23
24
 
25
+ /** Preserve a structured upstream error type; otherwise use the dedicated policy identity. */
26
+ export function cyberPolicyErrorType(type: string | null | undefined): string {
27
+ const trimmed = typeof type === "string" ? type.trim() : "";
28
+ return trimmed || CYBER_POLICY_ERROR_CODE;
29
+ }
30
+
24
31
  /**
25
32
  * Detect OpenAI cyber-policy refusals from message text when structured `code` was stripped.
26
33
  * Matches Codex fallback copy and Cursor/API agent wording (session evidence 2026-07-24).
@@ -149,9 +156,11 @@ export function classifyError(status: number, type: string, message: string): Oc
149
156
  return { message, type: "invalid_request_error", code: "client_closed_request" };
150
157
  }
151
158
  // Codex only shows the dedicated cyber UI when error.code === "cyber_policy".
152
- // Prefer that code (and invalid_request_error) over generic remaps / 502 upstream_server_error.
159
+ // The public wire does not establish invalid_request_error as the canonical type, so
160
+ // message-only classification keeps the dedicated identity instead of inventing one.
161
+ // Structured callers re-apply their real upstream type with cyberPolicyErrorType().
153
162
  if (type === CYBER_POLICY_ERROR_CODE || isCyberPolicyMessage(text)) {
154
- return { message, type: "invalid_request_error", code: CYBER_POLICY_ERROR_CODE };
163
+ return { message, type: CYBER_POLICY_ERROR_CODE, code: CYBER_POLICY_ERROR_CODE };
155
164
  }
156
165
  // A LOCAL preflight refusal keeps its own code (#1524). The message necessarily says
157
166
  // "context window" -- that is what it is refusing on -- so the generic remap below would
@@ -0,0 +1,101 @@
1
+ import { statSync } from "node:fs";
2
+
3
+ export interface PackageTreeObservation {
4
+ readonly device: bigint;
5
+ readonly inode: bigint;
6
+ readonly contentTimeNs: bigint;
7
+ readonly size: bigint;
8
+ }
9
+
10
+ export type PackageTreeIntegrityStatus =
11
+ | { readonly ok: true }
12
+ | { readonly ok: false; readonly reason: "package_tree_replaced" | "package_tree_unreadable" };
13
+
14
+ export interface PackageTreeIntegrityGuard {
15
+ status(): PackageTreeIntegrityStatus;
16
+ }
17
+
18
+ type ObservePackageTree = () => PackageTreeObservation | null;
19
+ type PackageTreeRuntimeInstall = "bun" | "npm" | "source";
20
+
21
+ const packageManifestUrl = new URL("../../package.json", import.meta.url);
22
+
23
+ function observePackageManifest(): PackageTreeObservation | null {
24
+ try {
25
+ const stat = statSync(packageManifestUrl, { bigint: true });
26
+ return {
27
+ device: stat.dev,
28
+ inode: stat.ino,
29
+ // mtimeNs, NOT ctimeNs. An inode-change time moves for METADATA writes that
30
+ // replace nothing: chmod, chown, touch, an editor normalizing permissions, a
31
+ // backup tool restoring modes. Each of those left device, inode and size
32
+ // identical, so the comparison below called the manifest "replaced" and every
33
+ // /v1/* request answered 503 until the process was restarted. Measured on
34
+ // macOS: chmod alone moved ctimeNs and left mtimeNs untouched.
35
+ //
36
+ // mtimeNs still catches every real replacement. An in-place rewrite of the
37
+ // same byte length moves mtimeNs while inode and size hold; an atomic
38
+ // install (write-then-rename, which is what a package manager does) changes
39
+ // the inode as well. Both were measured before this change was made.
40
+ contentTimeNs: stat.mtimeNs,
41
+ size: stat.size,
42
+ };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function sameObservation(left: PackageTreeObservation, right: PackageTreeObservation): boolean {
49
+ return left.device === right.device
50
+ && left.inode === right.inode
51
+ && left.contentTimeNs === right.contentTimeNs
52
+ && left.size === right.size;
53
+ }
54
+
55
+ /**
56
+ * How long an `ok` observation is reused before the manifest is stat'd again.
57
+ *
58
+ * `status()` runs on `/healthz`, `/readyz` and every `/v1/*` request, so an unthrottled guard
59
+ * adds a filesystem syscall to the proxy's hot path to detect an event that happens at most
60
+ * once per install. A replaced tree is not time-critical either: the process is already
61
+ * serving broken imports, and one more second of that is not worse than a syscall per turn
62
+ * forever.
63
+ *
64
+ * A NEGATIVE result is never cached — once the tree looks wrong, every later request re-checks,
65
+ * so a repaired install recovers on its own instead of staying refused for a window.
66
+ */
67
+ const PACKAGE_TREE_RECHECK_MS = 1_000;
68
+
69
+ export function createPackageTreeIntegrityGuard(
70
+ observe: ObservePackageTree = observePackageManifest,
71
+ now: () => number = Date.now,
72
+ ): PackageTreeIntegrityGuard {
73
+ const boot = observe();
74
+ let lastOkAt: number | null = null;
75
+ return {
76
+ status(): PackageTreeIntegrityStatus {
77
+ const at = now();
78
+ if (lastOkAt !== null && at - lastOkAt < PACKAGE_TREE_RECHECK_MS) return { ok: true };
79
+ const current = observe();
80
+ if (boot === null || current === null) return { ok: false, reason: "package_tree_unreadable" };
81
+ if (!sameObservation(boot, current)) return { ok: false, reason: "package_tree_replaced" };
82
+ lastOkAt = at;
83
+ return { ok: true };
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Installed packages fail closed when their manifest is replaced under a live process.
90
+ * A source checkout is different: editing package.json is ordinary development work, and
91
+ * Bun keeps serving the module snapshot already loaded by this process until the operator
92
+ * chooses to restart. Fencing every request there makes running dev directly impossible.
93
+ */
94
+ export function createRuntimePackageTreeIntegrityGuard(
95
+ installer: PackageTreeRuntimeInstall,
96
+ observe: ObservePackageTree = observePackageManifest,
97
+ now: () => number = Date.now,
98
+ ): PackageTreeIntegrityGuard {
99
+ if (installer === "source") return { status: () => ({ ok: true }) };
100
+ return createPackageTreeIntegrityGuard(observe, now);
101
+ }
@@ -0,0 +1,65 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { OcxProviderConfig } from "../types";
3
+ import {
4
+ parseGoogleCookieJar,
5
+ validateAiStudioCookies,
6
+ } from "./google-aistudio-auth";
7
+ import {
8
+ cookieHeaderFromSession,
9
+ loadAiStudioSession,
10
+ type AiStudioSessionData,
11
+ } from "./aistudio-session-sync";
12
+
13
+ export type AiStudioCredentialResolution =
14
+ | { kind: "ready"; cookieHeader: string; source: "provider-api-key" | "provider-header" | "session"; fingerprint: string }
15
+ | { kind: "missing"; reason: string }
16
+ | { kind: "invalid"; reason: string };
17
+
18
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
19
+
20
+ function validCookieHeader(value: unknown): string | undefined {
21
+ if (typeof value !== "string") return undefined;
22
+ const cookieHeader = value.trim();
23
+ if (!cookieHeader || CONTROL_CHARACTERS.test(cookieHeader)) return undefined;
24
+ const jar = parseGoogleCookieJar(cookieHeader);
25
+ return validateAiStudioCookies(jar).valid ? jar.cookieHeader : undefined;
26
+ }
27
+
28
+ function ready(cookieHeader: string, source: "provider-api-key" | "provider-header" | "session"): AiStudioCredentialResolution {
29
+ return {
30
+ kind: "ready",
31
+ cookieHeader,
32
+ source,
33
+ fingerprint: createHash("sha256").update(cookieHeader).digest("hex"),
34
+ };
35
+ }
36
+
37
+ export function resolveAiStudioCredentials(
38
+ provider: OcxProviderConfig,
39
+ session?: AiStudioSessionData | null,
40
+ ): AiStudioCredentialResolution {
41
+ const invalidSources: string[] = [];
42
+ const apiKey = validCookieHeader(provider.apiKey);
43
+ if (apiKey) return ready(apiKey, "provider-api-key");
44
+ if ((typeof provider.apiKey === "string" && provider.apiKey.trim())
45
+ || (provider.apiKey !== undefined && provider.apiKey !== null && typeof provider.apiKey !== "string")) {
46
+ invalidSources.push("provider apiKey");
47
+ }
48
+
49
+ const cookieEntry = Object.entries(provider.headers ?? {}).find(([name]) => name.toLowerCase() === "cookie");
50
+ const headerCookie = validCookieHeader(cookieEntry?.[1]);
51
+ if (headerCookie) return ready(headerCookie, "provider-header");
52
+ if ((typeof cookieEntry?.[1] === "string" && cookieEntry[1].trim())
53
+ || (cookieEntry !== undefined && typeof cookieEntry[1] !== "string")) {
54
+ invalidSources.push("provider Cookie header");
55
+ }
56
+
57
+ const savedSession = session === undefined ? loadAiStudioSession() : session;
58
+ const sessionCookie = validCookieHeader(cookieHeaderFromSession(savedSession));
59
+ if (sessionCookie) return ready(sessionCookie, "session");
60
+ if (savedSession) invalidSources.push("saved session");
61
+
62
+ return invalidSources.length > 0
63
+ ? { kind: "invalid", reason: "AI Studio credentials are malformed or missing SAPISID." }
64
+ : { kind: "missing", reason: "AI Studio credentials are missing." };
65
+ }
@@ -0,0 +1,116 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawn, type ChildProcess } from "node:child_process";
4
+ import { loadAiStudioSession, getAiStudioSessionPath } from "./aistudio-session-sync";
5
+ import { resolveAiStudioCredentials } from "./aistudio-credentials";
6
+ import type { OcxProviderConfig } from "../types";
7
+
8
+ export function isNativeWebKitSupported(): boolean {
9
+ return process.platform === "darwin";
10
+ }
11
+
12
+ export function getAiStudioNativeDaemonSourcePath(): string {
13
+ return join(process.cwd(), "integrations/aistudio-daemon/main.swift");
14
+ }
15
+
16
+ export function getAiStudioNativeDaemonBinaryPath(): string {
17
+ return join(process.cwd(), ".tmp/aistudio-webkit-daemon");
18
+ }
19
+
20
+ export async function buildAiStudioNativeDaemon(outputPath?: string): Promise<string> {
21
+ const src = getAiStudioNativeDaemonSourcePath();
22
+ const dest = outputPath ?? getAiStudioNativeDaemonBinaryPath();
23
+
24
+ const proc = Bun.spawn(["swiftc", "-O", src, "-o", dest], {
25
+ stdout: "pipe",
26
+ stderr: "pipe",
27
+ });
28
+ const exitCode = await proc.exited;
29
+ if (exitCode !== 0) {
30
+ const errText = await new Response(proc.stderr).text();
31
+ throw new Error(`Failed to compile native WebKit daemon: ${errText}`);
32
+ }
33
+ return dest;
34
+ }
35
+
36
+ type NativeLoginChild = { exited: Promise<number>; kill: () => void };
37
+ type NativeLoginSpawn = (command: string[], options: { stdout: "pipe"; stderr: "pipe" }) => NativeLoginChild;
38
+
39
+ export type AiStudioNativeLoginResult =
40
+ | { kind: "authenticated"; sessionPath: string }
41
+ | { kind: "cancelled" }
42
+ | { kind: "unsupported" }
43
+ | { kind: "failed"; error: string };
44
+
45
+ export interface AiStudioNativeLoginOptions {
46
+ platform?: NodeJS.Platform;
47
+ sessionPath?: string;
48
+ signal?: AbortSignal;
49
+ /** Test-only process seam. Production always uses Bun.spawn. */
50
+ spawn?: NativeLoginSpawn;
51
+ }
52
+
53
+ /** Spawn one visible native login and report only after its session is durable and valid. */
54
+ export async function runAiStudioNativeLogin(options: AiStudioNativeLoginOptions = {}): Promise<AiStudioNativeLoginResult> {
55
+ if ((options.platform ?? process.platform) !== "darwin") return { kind: "unsupported" };
56
+ const sessionPath = options.sessionPath ?? getAiStudioSessionPath();
57
+ const spawnLogin = options.spawn ?? ((command, spawnOptions) => Bun.spawn(command, spawnOptions) as unknown as NativeLoginChild);
58
+ let child: NativeLoginChild;
59
+ try {
60
+ child = spawnLogin(["swift", getAiStudioNativeDaemonSourcePath(), "--login"], { stdout: "pipe", stderr: "pipe" });
61
+ } catch (error) {
62
+ return { kind: "failed", error: `Could not start native AI Studio login: ${error instanceof Error ? error.message : String(error)}` };
63
+ }
64
+ let aborted = options.signal?.aborted === true;
65
+ const terminate = () => {
66
+ aborted = true;
67
+ try { child.kill(); } catch { /* already exited */ }
68
+ };
69
+ options.signal?.addEventListener("abort", terminate, { once: true });
70
+ try {
71
+ const exitCode = await child.exited;
72
+ if (aborted || options.signal?.aborted) return { kind: "cancelled" };
73
+ if (exitCode === 2) return { kind: "cancelled" };
74
+ if (exitCode !== 0) return { kind: "failed", error: `Native AI Studio login failed (exit code ${exitCode})` };
75
+ const session = loadAiStudioSession(sessionPath);
76
+ const credentials = resolveAiStudioCredentials({} as OcxProviderConfig, session);
77
+ return credentials.kind === "ready"
78
+ ? { kind: "authenticated", sessionPath }
79
+ : { kind: "failed", error: "Native login completed without a valid AI Studio session" };
80
+ } catch (error) {
81
+ return { kind: "failed", error: error instanceof Error ? error.message : String(error) };
82
+ } finally {
83
+ options.signal?.removeEventListener("abort", terminate);
84
+ }
85
+ }
86
+
87
+ export interface NativeDaemonHandle {
88
+ process: ChildProcess;
89
+ stop: () => void;
90
+ }
91
+
92
+ export function startAiStudioNativeDaemon(port = 10100): NativeDaemonHandle {
93
+ const binPath = getAiStudioNativeDaemonBinaryPath();
94
+ const srcPath = getAiStudioNativeDaemonSourcePath();
95
+
96
+ const runner = existsSync(binPath)
97
+ ? [binPath, "--port", String(port)]
98
+ : ["swift", srcPath, "--port", String(port)];
99
+
100
+ const child = spawn(runner[0]!, runner.slice(1), {
101
+ detached: true,
102
+ stdio: "ignore",
103
+ });
104
+ child.unref();
105
+
106
+ return {
107
+ process: child,
108
+ stop: () => {
109
+ try {
110
+ child.kill();
111
+ } catch (err) {
112
+ void err;
113
+ }
114
+ },
115
+ };
116
+ }
@@ -0,0 +1,95 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getConfigDir } from "../config";
4
+
5
+ export interface AiStudioCookieItem {
6
+ name: string;
7
+ value: string;
8
+ domain?: string;
9
+ path?: string;
10
+ }
11
+
12
+ export interface AiStudioSessionData {
13
+ selectedProject: string;
14
+ windowId: string;
15
+ cookies: AiStudioCookieItem[];
16
+ }
17
+
18
+ export function serializeSessionBundle(data: AiStudioSessionData): string {
19
+ const jsonStr = JSON.stringify(data);
20
+ return Buffer.from(jsonStr, "utf-8").toString("base64");
21
+ }
22
+
23
+ export function parseSessionBundle(encoded: string): AiStudioSessionData {
24
+ if (!encoded || typeof encoded !== "string") {
25
+ throw new Error("Invalid session token");
26
+ }
27
+
28
+ const raw = Buffer.from(encoded.trim(), "base64").toString("utf-8");
29
+ let data: any;
30
+ try {
31
+ data = JSON.parse(raw);
32
+ } catch (err) {
33
+ void err;
34
+ throw new Error("Failed to parse session token: invalid JSON");
35
+ }
36
+
37
+ if (!data || typeof data !== "object" || !Array.isArray(data.cookies)) {
38
+ throw new Error("Invalid session bundle schema: cookies array required");
39
+ }
40
+
41
+ return {
42
+ selectedProject: String(data.selectedProject || ""),
43
+ windowId: String(data.windowId || ""),
44
+ cookies: data.cookies.map((c: any) => ({
45
+ name: String(c.name || ""),
46
+ value: String(c.value || ""),
47
+ domain: c.domain ? String(c.domain) : undefined,
48
+ path: c.path ? String(c.path) : undefined,
49
+ })),
50
+ };
51
+ }
52
+
53
+ export function getAiStudioSessionPath(): string {
54
+ return join(getConfigDir(), "aistudio-session.json");
55
+ }
56
+
57
+ export function saveAiStudioSession(data: AiStudioSessionData, dest = getAiStudioSessionPath()): string {
58
+ mkdirSync(join(dest, ".."), { recursive: true });
59
+ writeFileSync(dest, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 0o600 });
60
+ try { chmodSync(dest, 0o600); } catch { /* best-effort on platforms without POSIX modes */ }
61
+ return dest;
62
+ }
63
+
64
+ export function saveAiStudioSessionFromToken(tokenOrBase64: string, dest?: string): string {
65
+ const session = parseSessionBundle(tokenOrBase64);
66
+ return saveAiStudioSession(session, dest);
67
+ }
68
+
69
+ export function loadAiStudioSession(path = getAiStudioSessionPath()): AiStudioSessionData | null {
70
+ if (!existsSync(path)) return null;
71
+ try {
72
+ const data = JSON.parse(readFileSync(path, "utf-8"));
73
+ if (!data || typeof data !== "object" || !Array.isArray(data.cookies)) return null;
74
+ return {
75
+ selectedProject: String(data.selectedProject || ""),
76
+ windowId: String(data.windowId || ""),
77
+ cookies: data.cookies.map((c: any) => ({
78
+ name: String(c.name || ""),
79
+ value: String(c.value || ""),
80
+ domain: c.domain ? String(c.domain) : undefined,
81
+ path: c.path ? String(c.path) : undefined,
82
+ })),
83
+ };
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ export function cookieHeaderFromSession(session: AiStudioSessionData | null | undefined): string {
90
+ if (!session?.cookies?.length) return "";
91
+ return session.cookies
92
+ .filter((c) => c && c.name && c.value && !/[\r\n\u0000]/.test(c.name) && !/[\r\n\u0000]/.test(c.value))
93
+ .map((c) => c.name.trim() + "=" + c.value.trim())
94
+ .join("; ");
95
+ }