openmausbot 0.1.91 → 0.1.93

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 (164) hide show
  1. package/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
  7. package/dist/assets/channel-BU94VpxE.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
  34. package/dist/assets/index-Biji-bfL.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
  36. package/dist/assets/index-DZlhoES0.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.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 +112 -14
  63. package/dist-server/companion/src/routes.js +24 -1
  64. package/dist-server/container-mcp.js +201 -16
  65. package/dist-server/drivers/agents-proxy.js +7 -4
  66. package/dist-server/enterprise/server/index.js +5 -1
  67. package/dist-server/index.js +10698 -4737
  68. package/dist-server/local-computer-proxy.js +133 -8
  69. package/dist-server/mcp-gate.js +16 -6
  70. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  71. package/dist-server/openmausbot.js +4656 -1258
  72. package/dist-server/pair-cli.js +4656 -1258
  73. package/dist-server/server/boat.js +5 -5
  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 +118 -26
  80. package/dist-server/server/cloud-home.js +39 -3
  81. package/dist-server/server/cloud-lending.js +74 -10
  82. package/dist-server/server/cloud-owner.js +196 -0
  83. package/dist-server/server/cloud-secrets-boot.js +6 -0
  84. package/dist-server/server/cloud-secrets.js +65 -0
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/config.js +187 -14
  87. package/dist-server/server/container-computer.js +46 -34
  88. package/dist-server/server/desktop-viewer-targets.js +26 -0
  89. package/dist-server/server/digest.js +12 -2
  90. package/dist-server/server/drivers/acp/core.js +339 -33
  91. package/dist-server/server/drivers/acp/cursor.js +13 -1
  92. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  93. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  94. package/dist-server/server/drivers/acp/qwen.js +18 -0
  95. package/dist-server/server/drivers/agents-catalog.js +6 -6
  96. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  97. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  98. package/dist-server/server/drivers/antigravity.js +21 -0
  99. package/dist-server/server/drivers/builtIn.js +2 -0
  100. package/dist-server/server/drivers/cerebras.js +108 -0
  101. package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
  102. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  103. package/dist-server/server/drivers/claude.js +329 -37
  104. package/dist-server/server/drivers/codex.js +228 -18
  105. package/dist-server/server/drivers/grok.js +2 -2
  106. package/dist-server/server/drivers/mistral.js +4 -3
  107. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  108. package/dist-server/server/drivers/openai-chat.js +66 -4
  109. package/dist-server/server/drivers/openai-compat.js +73 -23
  110. package/dist-server/server/drivers/prompt-split.js +2 -0
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +21 -5
  113. package/dist-server/server/harness/registry.js +2 -2
  114. package/dist-server/server/index.js +1545 -238
  115. package/dist-server/server/lending-memory.js +479 -0
  116. package/dist-server/server/local-vm-idle.js +24 -6
  117. package/dist-server/server/local-vm-stop-reason.js +26 -0
  118. package/dist-server/server/managed-desktop.js +3 -0
  119. package/dist-server/server/mcp-bridge.js +5 -3
  120. package/dist-server/server/mcp-http.js +6 -3
  121. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  122. package/dist-server/server/mcp-oauth-store.js +70 -0
  123. package/dist-server/server/mcp-oauth.js +550 -0
  124. package/dist-server/server/mcp-probe.js +14 -1
  125. package/dist-server/server/mcp-registry.js +43 -0
  126. package/dist-server/server/memory-upkeep.js +94 -84
  127. package/dist-server/server/model-catalog/catalog.js +374 -0
  128. package/dist-server/server/model-catalog/presets.js +176 -0
  129. package/dist-server/server/model-catalog/trim.js +219 -0
  130. package/dist-server/server/provider-key-check.js +5 -2
  131. package/dist-server/server/recent-work.js +1 -1
  132. package/dist-server/server/request-auth.js +23 -0
  133. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  134. package/dist-server/server/routes/bot-memory.js +31 -6
  135. package/dist-server/server/routes/desktop-viewer.js +238 -0
  136. package/dist-server/server/routine-requests.js +1 -1
  137. package/dist-server/server/routines.js +6 -3
  138. package/dist-server/server/sessions.js +56 -9
  139. package/dist-server/server/skills.js +25 -10
  140. package/dist-server/server/store.js +116 -122
  141. package/dist-server/server/thread-events.js +2 -0
  142. package/dist-server/server/thread-starters.js +33 -0
  143. package/dist-server/server/thread-title.js +46 -0
  144. package/dist-server/server/tts/fish.js +4 -5
  145. package/dist-server/server/tts/index.js +5 -3
  146. package/dist-server/server/vps-computer.js +80 -13
  147. package/dist-server/server/webhooks.js +18 -1
  148. package/dist-server/server/workspace-backup-policy.js +3 -0
  149. package/dist-server/server/workspace-backup.js +25 -0
  150. package/dist-server/server/workspace.js +66 -16
  151. package/dist-server/shared/approval-mode.js +1 -1
  152. package/dist-server/shared/redact.js +1 -0
  153. package/dist-server/shared/tool-name.js +22 -0
  154. package/dist-server/shared/workspace-backup-client.js +1 -0
  155. package/dist-server/src/lib/citations.js +177 -0
  156. package/dist-server/src/lib/composer-attachments.js +7 -1
  157. package/dist-server/vps-container-mcp.js +202 -16
  158. package/dist-server/workspace-backup.worker.js +87 -9
  159. package/enterprise/server/index.js +5 -1
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DrfGMu7X.js +0 -1
  162. package/dist/assets/index-BlnCEcU6.js +0 -588
  163. package/dist/assets/index-DVxjOvID.css +0 -1
  164. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
@@ -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
+ }