openmausbot 0.1.90 → 0.1.92

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 (177) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +128 -15
  63. package/dist-server/container-mcp.js +277 -45
  64. package/dist-server/drivers/agents-proxy.js +16 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +46703 -39955
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +4099 -1217
  71. package/dist-server/pair-cli.js +4099 -1217
  72. package/dist-server/server/boat.js +67 -22
  73. package/dist-server/server/bot-overview.js +7 -4
  74. package/dist-server/server/browser-engine.js +10 -1
  75. package/dist-server/server/browser-live.js +46 -2
  76. package/dist-server/server/browser-runtime.js +74 -8
  77. package/dist-server/server/browser-tool-shape.js +6 -4
  78. package/dist-server/server/chief-of-staff.js +1 -1
  79. package/dist-server/server/cloud-home-start.js +136 -26
  80. package/dist-server/server/cloud-home.js +64 -5
  81. package/dist-server/server/cloud-lending.js +167 -0
  82. package/dist-server/server/cloud-move-http.js +268 -0
  83. package/dist-server/server/cloud-move.js +418 -0
  84. package/dist-server/server/cloud-owner.js +196 -0
  85. package/dist-server/server/cloud-secrets-boot.js +6 -0
  86. package/dist-server/server/cloud-secrets.js +65 -0
  87. package/dist-server/server/config.js +215 -17
  88. package/dist-server/server/container-computer.js +3 -2
  89. package/dist-server/server/decider/index.js +238 -0
  90. package/dist-server/server/decider/jev.js +183 -0
  91. package/dist-server/server/decider/log.js +62 -0
  92. package/dist-server/server/decider/relay.js +52 -0
  93. package/dist-server/server/decider/room-routing.js +88 -0
  94. package/dist-server/server/decider/types.js +5 -0
  95. package/dist-server/server/desktop-viewer-targets.js +26 -0
  96. package/dist-server/server/drivers/acp/core.js +167 -21
  97. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  98. package/dist-server/server/drivers/agents-catalog.js +22 -6
  99. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  100. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  101. package/dist-server/server/drivers/antigravity.js +21 -0
  102. package/dist-server/server/drivers/boatagent.js +20 -12
  103. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  104. package/dist-server/server/drivers/claude.js +313 -34
  105. package/dist-server/server/drivers/codex.js +228 -18
  106. package/dist-server/server/drivers/grok.js +2 -2
  107. package/dist-server/server/drivers/mistral.js +4 -3
  108. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  109. package/dist-server/server/drivers/openai-chat.js +41 -3
  110. package/dist-server/server/drivers/openai-compat.js +73 -23
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +15 -3
  113. package/dist-server/server/group-goal-run.js +6 -4
  114. package/dist-server/server/harness/registry.js +2 -2
  115. package/dist-server/server/harness-capabilities.js +2 -0
  116. package/dist-server/server/included-services.js +70 -0
  117. package/dist-server/server/index.js +1753 -263
  118. package/dist-server/server/lending-memory.js +479 -0
  119. package/dist-server/server/local-vm-idle.js +24 -6
  120. package/dist-server/server/managed-desktop.js +3 -0
  121. package/dist-server/server/mcp-bridge.js +5 -3
  122. package/dist-server/server/mcp-http.js +6 -3
  123. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  124. package/dist-server/server/mcp-oauth-store.js +70 -0
  125. package/dist-server/server/mcp-oauth.js +550 -0
  126. package/dist-server/server/mcp-probe.js +14 -1
  127. package/dist-server/server/mcp-registry.js +43 -0
  128. package/dist-server/server/memory-upkeep.js +94 -84
  129. package/dist-server/server/model-catalog/catalog.js +374 -0
  130. package/dist-server/server/model-catalog/presets.js +176 -0
  131. package/dist-server/server/model-catalog/trim.js +219 -0
  132. package/dist-server/server/model-context-window.js +4 -4
  133. package/dist-server/server/package-export.js +17 -10
  134. package/dist-server/server/package-import.js +6 -3
  135. package/dist-server/server/package-parts.js +3 -1
  136. package/dist-server/server/provider-key-check.js +4 -2
  137. package/dist-server/server/recent-work.js +1 -1
  138. package/dist-server/server/request-auth.js +25 -0
  139. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  140. package/dist-server/server/routes/bot-memory.js +31 -6
  141. package/dist-server/server/routes/decider.js +19 -0
  142. package/dist-server/server/routes/desktop-viewer.js +194 -0
  143. package/dist-server/server/routine-requests.js +1 -1
  144. package/dist-server/server/routines.js +1 -1
  145. package/dist-server/server/sessions.js +56 -9
  146. package/dist-server/server/shared-computers.js +116 -30
  147. package/dist-server/server/skills.js +25 -10
  148. package/dist-server/server/store.js +79 -124
  149. package/dist-server/server/surface.js +13 -0
  150. package/dist-server/server/system-prompt.js +11 -0
  151. package/dist-server/server/team-backup.js +6 -2
  152. package/dist-server/server/team-manifest.js +11 -3
  153. package/dist-server/server/thread-events.js +2 -0
  154. package/dist-server/server/thread-starters.js +33 -0
  155. package/dist-server/server/thread-title.js +46 -0
  156. package/dist-server/server/tts/elevenlabs.js +11 -6
  157. package/dist-server/server/tts/fish.js +4 -5
  158. package/dist-server/server/tts/index.js +20 -11
  159. package/dist-server/server/vps-computer.js +79 -12
  160. package/dist-server/server/webhooks.js +18 -1
  161. package/dist-server/server/workspace-backup-policy.js +25 -3
  162. package/dist-server/server/workspace-backup.js +28 -8
  163. package/dist-server/server/workspace.js +65 -15
  164. package/dist-server/shared/cloud-home.js +4 -0
  165. package/dist-server/shared/package-format.js +30 -4
  166. package/dist-server/shared/team-backup.js +20 -3
  167. package/dist-server/shared/workspace-backup-client.js +1 -0
  168. package/dist-server/src/lib/citations.js +177 -0
  169. package/dist-server/src/lib/composer-attachments.js +7 -1
  170. package/dist-server/vps-container-mcp.js +278 -45
  171. package/dist-server/workspace-backup.worker.js +184 -38
  172. package/enterprise/server/index.js +5 -1
  173. package/package.json +1 -1
  174. package/dist/assets/channel-BCuyHjFV.js +0 -1
  175. package/dist/assets/index-CRvo-DS2.css +0 -1
  176. package/dist/assets/index-CWYGlA63.js +0 -588
  177. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
@@ -1,10 +1,9 @@
1
1
  import { newEventId, newId } from "../contracts.js";
2
+ import { boatCredential, boatProviderApi } from "../included-services.js";
2
3
  import { appendNative } from "./native.js";
3
4
  import { OMB_ASK_TOOL, answerWithoutPreamble, askQuestionSummary, capAnswerEcho, ombAskProtocolPrompt, parseOmbAskQuestions, questionChoices, stripOmbAskBlock, } from "../../shared/ask-question.js";
4
5
  // "boxAgent" is Boat's historical driver kind (catalog and wire); keep it.
5
6
  const DRIVER_KIND = "boxAgent";
6
- // overridable so tests and a dev backend can be pointed at instead of the live provider
7
- const BOAT_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
8
7
  const MODELS = {
9
8
  default: "claude-fable-5",
10
9
  options: [
@@ -20,10 +19,15 @@ const ASK_FENCE_ANY = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask\b/;
20
19
  * prime-agent, kimi). Which one a model id belongs to comes from the public
21
20
  * catalog, `GET /api/provider-models` at the API root: an object keyed by
22
21
  * harness, each with its `models`. A bot that arrives here from another engine
23
- * carries that engine's model id, so this is what lets it keep its model. */
22
+ * carries that engine's model id, so this is what lets it keep its model. The
23
+ * root is the API in use (Boat, or Cloud Pro's relay), so it is cached per root. */
24
24
  let catalog = null;
25
- async function loadCatalog() {
26
- const root = BOAT_API.replace(/\/api\/box\/v1\/?$/, "");
25
+ let catalogRoot = "";
26
+ async function loadCatalog(api) {
27
+ const root = api.replace(/\/api\/box\/v1\/?$/, "");
28
+ if (catalog && catalogRoot === root)
29
+ return;
30
+ catalogRoot = root;
27
31
  catalog = await fetch(`${root}/api/provider-models`, { signal: AbortSignal.timeout(15_000) })
28
32
  .then((res) => (res.ok ? res.json() : null))
29
33
  .catch(() => null);
@@ -53,7 +57,11 @@ export const BoatAgentDriver = {
53
57
  defaultConfig: () => decodeConfig({}),
54
58
  async create(input) {
55
59
  const { instanceId, config } = input;
56
- const token = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN ?? "";
60
+ // The person's own token (instanceConfigs injects it), else Cloud Pro's
61
+ // included one, with the one base URL each may be sent to. Resolved per
62
+ // request, like every other Boat call.
63
+ const ownToken = input.environment.BOX_TOKEN ?? process.env.BOX_TOKEN;
64
+ const account = () => boatCredential(ownToken);
57
65
  const listeners = new Set();
58
66
  const active = new Map();
59
67
  /** One open ask per thread: the turn-held transport's pending card. The
@@ -75,9 +83,10 @@ export const BoatAgentDriver = {
75
83
  createdAt: new Date().toISOString(),
76
84
  });
77
85
  const api = async (path, opts = {}) => {
78
- const res = await fetch(`${BOAT_API}${path}`, {
86
+ const credential = account();
87
+ const res = await fetch(`${credential?.api ?? boatProviderApi()}${path}`, {
79
88
  ...opts,
80
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...opts.headers },
89
+ headers: { authorization: `Bearer ${credential?.token ?? ""}`, "content-type": "application/json", ...opts.headers },
81
90
  signal: opts.signal ?? AbortSignal.timeout(30_000),
82
91
  });
83
92
  const body = await res.json().catch(() => null);
@@ -90,7 +99,7 @@ export const BoatAgentDriver = {
90
99
  const { threadId } = turn;
91
100
  const computer = turn.integrations?.computer;
92
101
  const boxId = computer && (!computer.kind || computer.kind === "box") ? computer.boxId : undefined;
93
- if (!token)
102
+ if (!account())
94
103
  throw new Error('box not configured — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
95
104
  if (!boxId) {
96
105
  throw new Error("this bot has no computer yet — open the Computer panel and provision one");
@@ -113,8 +122,7 @@ export const BoatAgentDriver = {
113
122
  .filter((s) => s !== undefined)
114
123
  .join("\n");
115
124
  const postPrompt = async (promptText) => {
116
- if (!catalog)
117
- await loadCatalog();
125
+ await loadCatalog(account()?.api ?? boatProviderApi());
118
126
  const started = await api(`/boxes/${boxId}/prompt`, {
119
127
  method: "POST",
120
128
  body: JSON.stringify({ ...providerFor(model), prompt: promptText }),
@@ -355,7 +363,7 @@ export const BoatAgentDriver = {
355
363
  return { turnId };
356
364
  };
357
365
  const snapshot = async () => {
358
- if (!token) {
366
+ if (!account()) {
359
367
  return { state: "unavailable", reason: 'no Boat token — add {"box":{"token":"…"}} to ~/.openmausbot/config.json' };
360
368
  }
361
369
  try {
@@ -0,0 +1,481 @@
1
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { chmodSync, closeSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { createServer } from "node:http";
4
+ import { dirname, isAbsolute, join } from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { createLocalJWKSet, jwtVerify } from "jose";
7
+ import { writeFileAtomic } from "../atomic.js";
8
+ const ISSUER = "https://auth.openai.com";
9
+ const RESOURCE = "https://api.openai.com/v1";
10
+ const DIRECT_SCOPE = "chatgpt.tokens.use.direct";
11
+ const SCOPES = `openid profile email offline_access resource.invoke ${DIRECT_SCOPE}`;
12
+ const LIFETIME = 15 * 60_000;
13
+ const TERMINAL_REFRESH = new Set(["invalid_grant", "invalid_refresh_token", "token_expired", "refresh_token_expired", "refresh_token_invalidated", "refresh_token_reused"]);
14
+ const SIGN_IN = "Continue with ChatGPT in Settings to use your ChatGPT plan.";
15
+ const CONSENT = "ChatGPT plan usage is not enabled. Continue with ChatGPT in Settings and allow plan usage, or explicitly choose another provider.";
16
+ function object(value) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value))
18
+ throw new Error("Invalid ChatGPT response.");
19
+ return value;
20
+ }
21
+ function string(value, max = 65_536) {
22
+ return typeof value === "string" && value.length > 0 && value.length <= max;
23
+ }
24
+ function privateDirectory(path) {
25
+ mkdirSync(path, { recursive: true, mode: 0o700 });
26
+ if (!lstatSync(path).isDirectory() || lstatSync(path).isSymbolicLink())
27
+ throw new Error("ChatGPT credential storage must be a private directory.");
28
+ chmodSync(path, 0o700);
29
+ }
30
+ function readJson(path) {
31
+ try {
32
+ const stat = lstatSync(path);
33
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 512_000)
34
+ throw new Error("Invalid credential storage.");
35
+ chmodSync(path, 0o600);
36
+ return JSON.parse(readFileSync(path, "utf8"));
37
+ }
38
+ catch (error) {
39
+ if (error.code === "ENOENT")
40
+ return null;
41
+ throw new Error("ChatGPT credential storage could not be read safely. Check its permissions before signing in.");
42
+ }
43
+ }
44
+ // A file lock protects rotating refresh tokens across processes as well as instances.
45
+ // Fail closed on an abandoned lock: never risk reusing a refresh token while its
46
+ // previous owner might still be exchanging it. The safe recovery is reauthorization
47
+ // after an administrator confirms the old process has stopped and removes the lock.
48
+ async function locked(directory, operation) {
49
+ const path = join(directory, ".credentials.lock");
50
+ const deadline = Date.now() + 30_000;
51
+ let fd;
52
+ for (;;) {
53
+ try {
54
+ fd = openSync(path, "wx", 0o600);
55
+ break;
56
+ }
57
+ catch (error) {
58
+ if (error.code !== "EEXIST")
59
+ throw new Error("ChatGPT credentials could not be locked safely.");
60
+ if (Date.now() >= deadline)
61
+ throw new Error("ChatGPT credentials are busy. If another app process crashed, stop it and remove its abandoned credential lock before trying again.");
62
+ await delay(50);
63
+ }
64
+ }
65
+ try {
66
+ writeFileSync(fd, String(process.pid));
67
+ return await operation();
68
+ }
69
+ finally {
70
+ closeSync(fd);
71
+ unlinkSync(path);
72
+ }
73
+ }
74
+ /** Server-owned OAuth credentials. Only safe status and the one-time authorization URL reach the renderer. */
75
+ export class ChatGptPlanAuthController {
76
+ options;
77
+ issuer;
78
+ resource;
79
+ flow = null;
80
+ disposed = false;
81
+ starting = null;
82
+ retryClientId = null;
83
+ constructor(options) {
84
+ if (!isAbsolute(options.directory))
85
+ throw new Error("ChatGPT credential storage must use an absolute path.");
86
+ if (options.testServerUrl) {
87
+ const url = new URL(options.testServerUrl);
88
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
89
+ throw new Error("ChatGPT test endpoints must be a loopback origin.");
90
+ }
91
+ this.issuer = url.origin;
92
+ this.resource = `${url.origin}/v1`;
93
+ }
94
+ else {
95
+ this.issuer = ISSUER;
96
+ this.resource = RESOURCE;
97
+ }
98
+ this.options = options;
99
+ }
100
+ prepare() {
101
+ privateDirectory(dirname(this.options.directory));
102
+ privateDirectory(this.options.directory);
103
+ }
104
+ read() {
105
+ const raw = readJson(join(this.options.directory, "credentials.json"));
106
+ if (raw === null)
107
+ return null;
108
+ const record = object(raw);
109
+ if (!string(record.clientId, 256) || record.clientId === "dynamic_agent_client" || !string(record.subject, 512) || !string(record.hostId, 128)
110
+ || (record.email !== undefined && !string(record.email, 320)))
111
+ throw new Error("Saved ChatGPT registration is invalid. Check credential storage before signing in.");
112
+ if (record.tokens !== undefined) {
113
+ const tokens = object(record.tokens);
114
+ if (!string(tokens.access) || typeof tokens.refresh !== "string" || tokens.refresh.length > 65_536 || !string(tokens.id) || !Array.isArray(tokens.scopes)
115
+ || !tokens.scopes.every((scope) => string(scope, 256)) || typeof tokens.expiresAt !== "number" || !Number.isFinite(tokens.expiresAt)
116
+ || (tokens.scopes.includes(DIRECT_SCOPE) && !tokens.refresh)
117
+ || (tokens.earliestRefreshAt !== undefined && (typeof tokens.earliestRefreshAt !== "number" || !Number.isFinite(tokens.earliestRefreshAt)))) {
118
+ throw new Error("Saved ChatGPT credentials are invalid. Continue with ChatGPT again.");
119
+ }
120
+ }
121
+ return record;
122
+ }
123
+ save(record) {
124
+ writeFileAtomic(join(this.options.directory, "credentials.json"), JSON.stringify(record), { mode: 0o600 });
125
+ }
126
+ async hostId() {
127
+ const parent = dirname(this.options.directory);
128
+ return locked(parent, async () => {
129
+ const path = join(parent, "host-id.json");
130
+ const saved = readJson(path);
131
+ if (saved !== null) {
132
+ const value = object(saved).hostId;
133
+ if (!string(value) || !/^urn:uuid:[0-9a-f-]{36}$/i.test(value))
134
+ throw new Error("Saved ChatGPT host identity is invalid.");
135
+ return value;
136
+ }
137
+ const value = `urn:uuid:${randomUUID()}`;
138
+ writeFileAtomic(path, JSON.stringify({ hostId: value }), { mode: 0o600 });
139
+ return value;
140
+ });
141
+ }
142
+ async request(url, init = {}) {
143
+ try {
144
+ const response = await fetch(url, { ...init, redirect: "error", signal: init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000) });
145
+ if (Number(response.headers.get("content-length")) > 1_048_576)
146
+ throw new Error("Oversized response.");
147
+ const reader = response.body?.getReader();
148
+ let size = 0;
149
+ const chunks = [];
150
+ if (reader) {
151
+ for (;;) {
152
+ const { value, done } = await reader.read();
153
+ if (done)
154
+ break;
155
+ size += value.byteLength;
156
+ if (size > 1_048_576) {
157
+ await reader.cancel();
158
+ throw new Error("Oversized response.");
159
+ }
160
+ chunks.push(value);
161
+ }
162
+ }
163
+ const text = Buffer.concat(chunks).toString("utf8");
164
+ return { response, body: text ? object(JSON.parse(text)) : {} };
165
+ }
166
+ catch {
167
+ throw new Error("Could not securely contact ChatGPT. Check your connection and try again; your saved account has not been removed.");
168
+ }
169
+ }
170
+ async exchange(params, signal) {
171
+ return this.request(`${this.issuer}/api/accounts/oauth/token`, { method: "POST", body: params, signal });
172
+ }
173
+ tokenSet(body, previous) {
174
+ if (!string(body.access_token) || typeof body.token_type !== "string" || body.token_type.toLowerCase() !== "bearer"
175
+ || typeof body.expires_in !== "number" || !Number.isFinite(body.expires_in) || body.expires_in <= 0 || body.expires_in > 86_400
176
+ || (body.scope !== undefined && typeof body.scope !== "string")
177
+ || (body.id_token !== undefined && !string(body.id_token)))
178
+ throw new Error("ChatGPT returned incomplete credentials. Continue with ChatGPT again.");
179
+ const id = string(body.id_token) ? body.id_token : previous?.id;
180
+ if (!id)
181
+ throw new Error("ChatGPT did not return a verifiable identity. Continue with ChatGPT again.");
182
+ const scopes = typeof body.scope === "string" ? body.scope.split(/\s+/).filter(Boolean) : previous?.scopes;
183
+ if (!scopes)
184
+ throw new Error("ChatGPT did not confirm granted permissions. Continue with ChatGPT again.");
185
+ if (scopes.includes(DIRECT_SCOPE) && !string(body.refresh_token))
186
+ throw new Error("ChatGPT did not provide renewable plan access. Continue with ChatGPT again.");
187
+ return { access: body.access_token, refresh: string(body.refresh_token) ? body.refresh_token : "", id, scopes, expiresAt: Date.now() + body.expires_in * 1000,
188
+ ...(typeof body.earliest_refresh_at === "number" && Number.isFinite(body.earliest_refresh_at) ? { earliestRefreshAt: body.earliest_refresh_at * 1000 } : {}) };
189
+ }
190
+ async identityKeys(signal) {
191
+ const { response, body } = await this.request(`${this.issuer}/.well-known/jwks.json`, { signal });
192
+ if (!response.ok)
193
+ throw new Error("ChatGPT identity keys are unavailable. Try signing in again later.");
194
+ try {
195
+ if (!Array.isArray(body.keys) || body.keys.length === 0)
196
+ throw new Error("Empty identity keys.");
197
+ return createLocalJWKSet(body);
198
+ }
199
+ catch {
200
+ throw new Error("ChatGPT identity keys could not be verified. Try signing in again later.");
201
+ }
202
+ }
203
+ async identity(id, clientId, keys, nonce) {
204
+ try {
205
+ const { payload } = await jwtVerify(id, keys, {
206
+ issuer: this.issuer, audience: clientId, algorithms: ["RS256", "ES256"], requiredClaims: ["sub", "exp", "iat"], clockTolerance: 30,
207
+ });
208
+ if (!string(payload.sub, 512) || typeof payload.iat !== "number" || payload.iat > Date.now() / 1000 + 30
209
+ || (Array.isArray(payload.aud) && payload.aud.length > 1 && payload.azp !== clientId)
210
+ || (nonce !== undefined && payload.nonce !== nonce))
211
+ throw new Error("Identity mismatch.");
212
+ return { subject: payload.sub, ...(string(payload.email, 320) ? { email: payload.email } : {}) };
213
+ }
214
+ catch {
215
+ throw new Error("ChatGPT identity could not be verified. No account was replaced. Start sign-in again.");
216
+ }
217
+ }
218
+ async start() {
219
+ if (this.starting)
220
+ return this.starting;
221
+ this.starting = this.begin();
222
+ try {
223
+ return await this.starting;
224
+ }
225
+ finally {
226
+ this.starting = null;
227
+ }
228
+ }
229
+ async begin() {
230
+ if (this.disposed)
231
+ throw new Error("This provider was removed. Refresh Settings before signing in.");
232
+ if (this.flow?.status.phase === "waiting")
233
+ return { ...this.flow.status, phase: "waiting" };
234
+ this.prepare();
235
+ const previous = this.read();
236
+ const clientId = previous?.clientId ?? this.retryClientId;
237
+ const hostId = await this.hostId();
238
+ if (previous && previous.hostId !== hostId)
239
+ throw new Error("This ChatGPT registration belongs to another host. Sign in using a new account entry on this host.");
240
+ const state = randomBytes(32).toString("base64url");
241
+ const nonce = randomBytes(32).toString("base64url");
242
+ const verifier = randomBytes(32).toString("base64url");
243
+ const server = createServer((request, response) => {
244
+ response.setHeader("Cache-Control", "no-store");
245
+ response.setHeader("Content-Type", "text/plain; charset=utf-8");
246
+ response.setHeader("Referrer-Policy", "no-referrer");
247
+ const flow = this.flow;
248
+ let callback;
249
+ try {
250
+ if (!flow || request.headers.host !== new URL(flow.redirectUri).host || !request.url?.startsWith("/"))
251
+ throw new Error("Unexpected callback host.");
252
+ callback = new URL(request.url, flow.redirectUri);
253
+ if (callback.origin !== new URL(flow.redirectUri).origin)
254
+ throw new Error("Unexpected callback origin.");
255
+ }
256
+ catch {
257
+ response.writeHead(400).end("Invalid sign-in callback. Return to OpenMausBot and try again.");
258
+ return;
259
+ }
260
+ const actual = callback.searchParams.get("state") ?? "";
261
+ if (request.method !== "GET" || callback.pathname !== "/auth/callback" || !flow || flow.server !== server || flow.consumed || flow.status.phase !== "waiting"
262
+ || Buffer.byteLength(actual) !== Buffer.byteLength(flow.state) || !timingSafeEqual(Buffer.from(actual), Buffer.from(flow.state))
263
+ || [...callback.searchParams.keys()].some((key) => callback.searchParams.getAll(key).length !== 1)) {
264
+ response.writeHead(400).end("Invalid or expired sign-in. Return to OpenMausBot and try again.");
265
+ return;
266
+ }
267
+ flow.consumed = true;
268
+ response.end("Finishing sign-in. You can return to OpenMausBot.");
269
+ server.close();
270
+ void this.complete(flow, callback.searchParams);
271
+ });
272
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); });
273
+ if (this.disposed) {
274
+ server.close();
275
+ throw new Error("This provider was removed.");
276
+ }
277
+ const address = server.address();
278
+ if (!address || typeof address === "string") {
279
+ server.close();
280
+ throw new Error("ChatGPT sign-in listener could not start.");
281
+ }
282
+ const redirectUri = `http://127.0.0.1:${address.port}/auth/callback`;
283
+ const url = new URL(`${this.issuer}/api/accounts/authorize`);
284
+ url.search = new URLSearchParams({ client_id: clientId ?? "dynamic_agent_client", ext_agent_host_id: hostId,
285
+ ...(clientId ? {} : { agent_name_hint: "openmausbot" }), ...(previous?.email ? { login_hint: previous.email } : {}),
286
+ ...(previous?.tokens && !previous.tokens.scopes.includes(DIRECT_SCOPE) ? { prompt: "consent" } : {}),
287
+ response_type: "code", redirect_uri: redirectUri, scope: SCOPES, resource: this.resource, state, nonce,
288
+ code_challenge_method: "S256", code_challenge: createHash("sha256").update(verifier).digest("base64url"),
289
+ }).toString();
290
+ const status = { phase: "waiting", flowId: randomUUID(), authorizationUrl: url.href, expiresAt: new Date(Date.now() + LIFETIME).toISOString() };
291
+ const flow = { status, state, nonce, verifier, redirectUri, previous, clientId, hostId, consumed: false, server, abort: new AbortController(),
292
+ expiry: setTimeout(() => this.finish(flow, "expired", "ChatGPT sign-in expired. Start again."), LIFETIME) };
293
+ flow.expiry.unref();
294
+ this.flow = flow;
295
+ return { ...status, phase: "waiting" };
296
+ }
297
+ finish(flow, phase, message) {
298
+ if (flow.status.phase !== "waiting")
299
+ return;
300
+ clearTimeout(flow.expiry);
301
+ flow.server.close();
302
+ flow.abort.abort();
303
+ flow.status = { ...flow.status, phase, authorizationUrl: null, ...(message ? { message } : {}) };
304
+ }
305
+ async complete(flow, params) {
306
+ try {
307
+ if (params.has("error"))
308
+ throw new Error("ChatGPT sign-in was not approved. You can try again in Settings.");
309
+ const clientId = params.get("client_id") ?? flow.clientId;
310
+ const code = params.get("code");
311
+ if (!string(clientId, 256) || clientId === "dynamic_agent_client" || (flow.clientId && flow.clientId !== clientId) || !string(code, 4096)) {
312
+ throw new Error("ChatGPT registration was incomplete or did not match the selected account. Start sign-in again.");
313
+ }
314
+ const keys = await this.identityKeys(flow.abort.signal);
315
+ const { response, body } = await this.exchange(new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, code,
316
+ code_verifier: flow.verifier, redirect_uri: flow.redirectUri, resource: this.resource }), flow.abort.signal);
317
+ if (!response.ok) {
318
+ // A used/expired code must not create another client on retry. Keep
319
+ // this issued ID only in the pending attempt until identity is verified.
320
+ if (!flow.previous && body.error === "invalid_grant")
321
+ this.retryClientId = clientId;
322
+ throw new Error("ChatGPT could not finish this sign-in. Start again with a fresh authorization.");
323
+ }
324
+ const tokens = this.tokenSet(body);
325
+ const identity = await this.identity(tokens.id, clientId, keys, flow.nonce);
326
+ if (flow.previous && identity.subject !== flow.previous.subject)
327
+ throw new Error("ChatGPT returned a different account. Use a separate account entry; the saved account was not replaced.");
328
+ await locked(this.options.directory, async () => {
329
+ if (this.disposed || flow.status.phase !== "waiting" || flow.abort.signal.aborted)
330
+ return;
331
+ if (JSON.stringify(this.read()) !== JSON.stringify(flow.previous))
332
+ throw new Error("The selected ChatGPT account changed during sign-in. Start again.");
333
+ this.save({ clientId, hostId: flow.hostId, ...identity, tokens });
334
+ this.retryClientId = null;
335
+ this.finish(flow, tokens.scopes.includes(DIRECT_SCOPE) ? "succeeded" : "failed", tokens.scopes.includes(DIRECT_SCOPE) ? undefined : CONSENT);
336
+ });
337
+ if (flow.status.phase === "succeeded")
338
+ await this.options.onAuthenticated?.().catch(() => { });
339
+ }
340
+ catch (error) {
341
+ this.finish(flow, "failed", error instanceof Error ? error.message : "ChatGPT sign-in could not finish. Try again.");
342
+ }
343
+ }
344
+ async get(flowId) {
345
+ if (!flowId || this.flow?.status.flowId !== flowId)
346
+ throw new Error("This sign-in is no longer available. Start again.");
347
+ return { ...this.flow.status };
348
+ }
349
+ async cancel() {
350
+ await this.starting?.catch(() => { });
351
+ if (this.flow)
352
+ this.finish(this.flow, "cancelled", "ChatGPT sign-in cancelled.");
353
+ }
354
+ async dispose() { this.disposed = true; await this.cancel(); this.flow = null; }
355
+ async snapshot() {
356
+ const record = this.read();
357
+ return { authenticated: !!record?.tokens?.scopes.includes(DIRECT_SCOPE),
358
+ ...(record ? { account: { method: "login", ...(record.email ? { email: record.email } : {}), organization: `ChatGPT · ${record.clientId.slice(-8)}` } } : {}),
359
+ ...(!record?.tokens ? { reason: SIGN_IN } : !record.tokens.scopes.includes(DIRECT_SCOPE) ? { reason: CONSENT } : {}), };
360
+ }
361
+ async accessToken() {
362
+ if (this.disposed)
363
+ throw new Error("This ChatGPT provider was removed.");
364
+ this.prepare();
365
+ return locked(this.options.directory, async () => {
366
+ if (this.disposed)
367
+ throw new Error("This ChatGPT provider was removed.");
368
+ const record = this.read();
369
+ if (!record?.tokens)
370
+ throw new Error(SIGN_IN);
371
+ if (!record.tokens.scopes.includes(DIRECT_SCOPE))
372
+ throw new Error(CONSENT);
373
+ if (record.tokens.expiresAt > Date.now() + 60_000)
374
+ return record.tokens.access;
375
+ if ((record.tokens.earliestRefreshAt ?? 0) > Date.now()) {
376
+ if (record.tokens.expiresAt > Date.now())
377
+ return record.tokens.access;
378
+ throw new Error("ChatGPT credentials cannot refresh yet. Please try again shortly.");
379
+ }
380
+ // Fetch validation keys before consuming a rotating refresh token. There
381
+ // must be no fallible network dependency between receiving and saving its successor.
382
+ const keys = await this.identityKeys();
383
+ const { response, body } = await this.exchange(new URLSearchParams({ grant_type: "refresh_token", client_id: record.clientId,
384
+ refresh_token: record.tokens.refresh, resource: this.resource }));
385
+ if (!response.ok) {
386
+ const code = typeof body.error === "string" ? body.error : (body.error && typeof body.error === "object" ? body.error.code : undefined);
387
+ if (typeof code === "string" && TERMINAL_REFRESH.has(code)) {
388
+ delete record.tokens;
389
+ this.save(record);
390
+ throw new Error(SIGN_IN);
391
+ }
392
+ if (code === "invalid_client")
393
+ throw new Error("ChatGPT did not accept this app registration. Check the integration configuration before trying again.");
394
+ throw new Error("ChatGPT could not renew access right now. Try again later; your saved account has not been removed.");
395
+ }
396
+ let tokens;
397
+ try {
398
+ tokens = this.tokenSet(body, record.tokens);
399
+ if (body.id_token !== undefined) {
400
+ const identity = await this.identity(tokens.id, record.clientId, keys);
401
+ if (identity.subject !== record.subject)
402
+ throw new Error("ChatGPT returned a different account during renewal.");
403
+ }
404
+ }
405
+ catch {
406
+ // The old refresh token was consumed. Never retry it after an invalid
407
+ // successor, even though the selected account/client mapping is retained.
408
+ delete record.tokens;
409
+ this.save(record);
410
+ throw new Error("ChatGPT renewal could not be verified. Continue with ChatGPT again in Settings.");
411
+ }
412
+ this.save({ ...record, tokens });
413
+ if (this.disposed)
414
+ throw new Error("This ChatGPT provider was removed.");
415
+ if (!tokens.scopes.includes(DIRECT_SCOPE))
416
+ throw new Error(CONSENT);
417
+ return tokens.access;
418
+ });
419
+ }
420
+ async models() {
421
+ if (!this.read()?.tokens?.scopes.includes(DIRECT_SCOPE))
422
+ return { default: "", options: [] };
423
+ const token = await this.accessToken();
424
+ const { response, body } = await this.request(`${this.resource}/models`, { headers: { Authorization: `Bearer ${token}` } });
425
+ if (!response.ok)
426
+ throw new Error(response.status === 401 || response.status === 403
427
+ ? "ChatGPT did not authorize model access. Check this account's plan permissions in ChatGPT Settings."
428
+ : "ChatGPT models are unavailable right now. Try refreshing later.");
429
+ if (!Array.isArray(body.models))
430
+ throw new Error("ChatGPT returned an invalid model catalog. No fallback models were selected.");
431
+ const options = [];
432
+ for (const entry of body.models) {
433
+ if (!entry || typeof entry !== "object")
434
+ continue;
435
+ const row = entry;
436
+ if (row.visibility !== "list" || !string(row.slug, 256) || /[\s\p{Cc}]/u.test(row.slug) || !string(row.display_name, 256) || options.some((item) => item.id === row.slug))
437
+ continue;
438
+ options.push({ id: row.slug, label: row.display_name });
439
+ }
440
+ return { default: options[0]?.id ?? "", options };
441
+ }
442
+ async signOut() {
443
+ if (this.disposed)
444
+ throw new Error("This provider was removed.");
445
+ await this.cancel();
446
+ this.prepare();
447
+ await locked(this.options.directory, async () => {
448
+ const record = this.read();
449
+ if (!record?.tokens)
450
+ return;
451
+ let revoked = !record.tokens.refresh;
452
+ try {
453
+ if (!record.tokens.refresh) {
454
+ delete record.tokens;
455
+ this.save(record);
456
+ return;
457
+ }
458
+ const discovery = await this.request(`${this.issuer}/.well-known/openid-configuration`);
459
+ const endpoint = discovery.body.revocation_endpoint;
460
+ if (discovery.response.ok && discovery.body.issuer === this.issuer && typeof endpoint === "string" && new URL(endpoint).origin === this.issuer) {
461
+ for (let attempt = 0; attempt < 2 && !revoked; attempt++) {
462
+ try {
463
+ const result = await this.request(endpoint, { method: "POST", body: new URLSearchParams({ token: record.tokens.refresh, token_type_hint: "refresh_token", client_id: record.clientId }) });
464
+ revoked = result.response.status === 200;
465
+ if (result.response.status < 500)
466
+ break;
467
+ }
468
+ catch { /* A network failure still clears local credentials below. */ }
469
+ if (!revoked && attempt === 0)
470
+ await delay(250);
471
+ }
472
+ }
473
+ }
474
+ catch { /* Sign-out is local even if OpenAI is temporarily unavailable. */ }
475
+ delete record.tokens;
476
+ this.save(record);
477
+ if (!revoked)
478
+ throw Object.assign(new Error("Signed out locally, but remote revocation was not confirmed. Disconnect OpenMausBot in ChatGPT Settings → Usage to end access there."), { code: "chatgpt_revocation_unconfirmed" });
479
+ });
480
+ }
481
+ }