codex-overleaf-link 1.9.13 → 2.1.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 (122) hide show
  1. package/README.md +24 -23
  2. package/extension/bootstrap/manifest.template.json +2 -1
  3. package/extension/runtime-manifest.json +13 -6
  4. package/extension/src/background.js +7 -0
  5. package/extension/src/content/composerPanel.js +13 -3
  6. package/extension/src/content/contentRuntime.js +130 -65
  7. package/extension/src/content/contextTray.js +173 -9
  8. package/extension/src/content/markdownText.js +16 -1
  9. package/extension/src/content/mathText.js +233 -0
  10. package/extension/src/content/modelPicker.js +117 -106
  11. package/extension/src/content/modelPickerSupport.js +115 -0
  12. package/extension/src/content/nativeChannel.js +20 -1
  13. package/extension/src/content/panelMaintenance.js +2 -0
  14. package/extension/src/content/projectSettingsCoordinator.js +2 -0
  15. package/extension/src/content/providerSettingsCoordinator.js +413 -0
  16. package/extension/src/content/providerSettingsDialog.js +981 -0
  17. package/extension/src/content/runTimelineView.js +24 -5
  18. package/extension/src/content/settingsPanel.js +33 -2
  19. package/extension/src/content/writebackOrchestrator.js +3 -1
  20. package/extension/src/page/overleafProjectSnapshot.js +29 -33
  21. package/extension/src/page/snapshotRouter.js +75 -8
  22. package/extension/src/page/trackedChangesLifecycle.js +154 -26
  23. package/extension/src/page/writebackRouter.js +31 -60
  24. package/extension/src/pageBridge.js +1 -1
  25. package/extension/src/shared/agentTranscript.js +130 -0
  26. package/extension/src/shared/compatibility.js +10 -7
  27. package/extension/src/shared/failureReasons.js +75 -0
  28. package/extension/src/shared/i18n.js +22 -0
  29. package/extension/src/shared/models.js +1 -0
  30. package/extension/src/shared/providerProfiles.js +283 -0
  31. package/extension/src/shared/sessionState.js +12 -2
  32. package/extension/src/shared/storageDb.js +27 -19
  33. package/extension/src/shared/storageRunActions.js +99 -0
  34. package/extension/styles/panel.css +587 -1
  35. package/extension/vendor/katex/LICENSE +21 -0
  36. package/extension/vendor/katex/VERSION +2 -0
  37. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  38. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  39. package/extension/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  40. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  41. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  42. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  43. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  44. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  45. package/extension/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  46. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  47. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  48. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  49. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  50. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  51. package/extension/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  52. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  53. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  54. package/extension/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  55. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  56. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  57. package/extension/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  58. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  59. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  60. package/extension/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  61. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  62. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  63. package/extension/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  64. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  65. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  66. package/extension/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  67. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  68. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  69. package/extension/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  70. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  71. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  72. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  73. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  74. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  75. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  76. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  77. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  78. package/extension/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  79. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  80. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  81. package/extension/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  82. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  83. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  84. package/extension/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  85. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  86. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  87. package/extension/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  88. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  89. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  90. package/extension/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  91. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  92. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  93. package/extension/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  94. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  95. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  96. package/extension/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  97. package/extension/vendor/katex/katex.min.css +1 -0
  98. package/extension/vendor/katex/katex.min.js +1 -0
  99. package/native-host/src/anthropicBridgeRequest.js +435 -0
  100. package/native-host/src/anthropicBridgeResponse.js +431 -0
  101. package/native-host/src/anthropicMessagesBridge.js +281 -0
  102. package/native-host/src/chatBridgeRequest.js +274 -0
  103. package/native-host/src/chatBridgeResponse.js +594 -0
  104. package/native-host/src/chatCompletionsBridge.js +485 -0
  105. package/native-host/src/codexCommand.js +25 -0
  106. package/native-host/src/codexPromptAssembly.js +9 -0
  107. package/native-host/src/codexProviderLaunch.js +213 -0
  108. package/native-host/src/codexProviderTest.js +196 -0
  109. package/native-host/src/codexSessionRunner.js +29 -25
  110. package/native-host/src/managedInstall.js +1 -0
  111. package/native-host/src/providerBridgeRoutes.js +11 -0
  112. package/native-host/src/providerModelCatalog.js +107 -0
  113. package/native-host/src/providerProfile.js +454 -0
  114. package/native-host/src/providerReasoning.js +235 -0
  115. package/native-host/src/providerRedaction.js +18 -0
  116. package/native-host/src/providerRuntime.js +365 -0
  117. package/native-host/src/providerStore.js +691 -0
  118. package/native-host/src/readProgressGuard.js +360 -0
  119. package/native-host/src/taskRunnerRuntime.js +40 -4
  120. package/native-host/src/updateArchive.js +1 -0
  121. package/native-host/src/updateTrust.js +2 -1
  122. package/package.json +2 -1
@@ -0,0 +1,691 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const {
8
+ computeConnectionFingerprint,
9
+ computeDraftFingerprint,
10
+ getEndpointHost,
11
+ isResolvedWireApi,
12
+ normalizeProviderDraft,
13
+ normalizeSecret,
14
+ providerError,
15
+ sanitizeProfile
16
+ } = require('./providerProfile');
17
+
18
+ const SCHEMA_VERSION = 1;
19
+ const STORE_LOCK_TIMEOUT_MS = 2500;
20
+ const STORE_LOCK_STALE_MS = 30000;
21
+ const STORE_LOCK_RETRY_MS = 25;
22
+ const heldStoreLocks = new Set();
23
+
24
+ function listProviders(env = process.env) {
25
+ return withProviderStoreLock(env, () => listProvidersUnlocked(env));
26
+ }
27
+
28
+ function upsertProvider(params = {}, env = process.env) {
29
+ return withProviderStoreLock(env, () => upsertProviderUnlocked(params, env));
30
+ }
31
+
32
+ function activateProvider(params = {}, env = process.env) {
33
+ return withProviderStoreLock(env, () => activateProviderUnlocked(params, env));
34
+ }
35
+
36
+ function deleteProvider(params = {}, env = process.env) {
37
+ return withProviderStoreLock(env, () => deleteProviderUnlocked(params, env));
38
+ }
39
+
40
+ function clearProviderSecret(params = {}, env = process.env) {
41
+ return withProviderStoreLock(env, () => clearProviderSecretUnlocked(params, env));
42
+ }
43
+
44
+ function recordProviderDiagnostic(params = {}, env = process.env) {
45
+ return withProviderStoreLock(env, () => recordProviderDiagnosticUnlocked(params, env));
46
+ }
47
+
48
+ function resolveDraftSecret(params = {}, env = process.env) {
49
+ return withProviderStoreLock(env, () => resolveDraftSecretUnlocked(params, env));
50
+ }
51
+
52
+ function loadProviderState(env = process.env) {
53
+ return withProviderStoreLock(env, () => loadProviderStateUnlocked(env));
54
+ }
55
+
56
+ function listProvidersUnlocked(env = process.env) {
57
+ const state = loadProviderState(env);
58
+ return buildCatalog(state);
59
+ }
60
+
61
+ function upsertProviderUnlocked(params = {}, env = process.env) {
62
+ const state = loadProviderState(env);
63
+ const now = Date.now();
64
+ const requestedId = normalizeId(params.profileId);
65
+ const existing = requestedId ? state.public.profiles.find(profile => profile.id === requestedId) : null;
66
+ const expectedRevision = normalizeRevision(params.expectedRevision);
67
+ if (existing && existing.revision !== expectedRevision) {
68
+ throw providerError('provider_revision_conflict', 'Provider changed in another tab. Reload it and retry.');
69
+ }
70
+ if (!existing && requestedId && expectedRevision !== 0) {
71
+ throw providerError('provider_not_found', 'Provider no longer exists.');
72
+ }
73
+ const id = existing?.id || crypto.randomUUID();
74
+ const draft = normalizeProviderDraft(params.draft || {});
75
+ const currentSecret = state.secrets.secrets[id] || '';
76
+ const secret = applySecretMutation(currentSecret, params.secretMutation);
77
+ const providedDiagnostics = normalizeProvidedDiagnostics(params);
78
+ const modelDiagnostics = {};
79
+ for (const model of draft.models) {
80
+ const connectionFingerprint = computeConnectionFingerprint(draft, secret, model.id);
81
+ const provided = providedDiagnostics.get(model.id);
82
+ const previous = existing?.modelDiagnostics?.[model.id];
83
+ if (provided?.connectionFingerprint === connectionFingerprint && isResolvedWireApi(provided.wireApi)) {
84
+ modelDiagnostics[model.id] = buildStoredDiagnostic(provided, connectionFingerprint, now);
85
+ } else if (previous?.connectionFingerprint === connectionFingerprint && isResolvedWireApi(previous.wireApi)) {
86
+ modelDiagnostics[model.id] = { ...previous };
87
+ }
88
+ }
89
+ const defaultDiagnostic = modelDiagnostics[draft.defaultModelId];
90
+ const resolvedWireApi = draft.wireApiPreference === 'auto'
91
+ ? defaultDiagnostic?.wireApi || ''
92
+ : draft.wireApiPreference;
93
+ const models = draft.models.map(model => {
94
+ const diagnostic = modelDiagnostics[model.id];
95
+ return {
96
+ ...model,
97
+ resolvedUpstreamResponseMode: diagnostic?.upstreamResponseMode || ''
98
+ };
99
+ });
100
+ const endpointHost = getEndpointHost(draft.baseUrl);
101
+ const destinationUnchanged = Boolean(existing && existing.baseUrl === draft.baseUrl);
102
+ let endpointDisclosureHost = destinationUnchanged && existing?.endpointDisclosureHost === endpointHost
103
+ ? existing.endpointDisclosureHost
104
+ : '';
105
+ let endpointDisclosureBaseUrl = destinationUnchanged && existing?.endpointDisclosureBaseUrl === draft.baseUrl
106
+ ? existing.endpointDisclosureBaseUrl
107
+ : '';
108
+ let endpointDisclosureAcceptedAt = endpointDisclosureHost && endpointDisclosureBaseUrl
109
+ ? existing?.endpointDisclosureAcceptedAt || now
110
+ : 0;
111
+ if (params.activate === true) {
112
+ assertCanActivate({
113
+ profile: { ...draft, resolvedWireApi },
114
+ disclosureHost: params.disclosureHost,
115
+ disclosureBaseUrl: params.disclosureBaseUrl,
116
+ endpointHost
117
+ });
118
+ endpointDisclosureHost = endpointHost;
119
+ endpointDisclosureBaseUrl = draft.baseUrl;
120
+ endpointDisclosureAcceptedAt = now;
121
+ }
122
+ const revision = (existing?.revision || 0) + 1;
123
+ const secretMutationKind = params.secretMutation?.kind || 'unchanged';
124
+ const secretUpdatedAt = secret
125
+ ? (secretMutationKind === 'replace' ? now : existing?.secretUpdatedAt || 0)
126
+ : 0;
127
+ const profile = {
128
+ id,
129
+ revision,
130
+ ...draft,
131
+ models,
132
+ resolvedWireApi,
133
+ endpointDisclosureHost,
134
+ endpointDisclosureBaseUrl,
135
+ endpointDisclosureAcceptedAt,
136
+ secretUpdatedAt,
137
+ modelDiagnostics,
138
+ lastVerified: defaultDiagnostic
139
+ ? {
140
+ revision,
141
+ at: defaultDiagnostic.at,
142
+ modelId: draft.defaultModelId,
143
+ wireApi: defaultDiagnostic.wireApi,
144
+ upstreamResponseMode: defaultDiagnostic.upstreamResponseMode,
145
+ draftFingerprint: defaultDiagnostic.connectionFingerprint
146
+ }
147
+ : null,
148
+ createdAt: existing?.createdAt || now,
149
+ updatedAt: now
150
+ };
151
+ const profiles = existing
152
+ ? state.public.profiles.map(item => item.id === id ? profile : item)
153
+ : [...state.public.profiles, profile];
154
+ const secrets = { ...state.secrets.secrets };
155
+ if (secret) {
156
+ secrets[id] = secret;
157
+ } else {
158
+ delete secrets[id];
159
+ }
160
+ commitProviderState(state, {
161
+ public: {
162
+ ...state.public,
163
+ activeProviderId: params.activate === true
164
+ ? id
165
+ : state.public.activeProviderId === id && endpointDisclosureBaseUrl !== draft.baseUrl
166
+ ? 'builtin'
167
+ : state.public.activeProviderId,
168
+ profiles
169
+ },
170
+ secrets: { ...state.secrets, secrets }
171
+ });
172
+ const next = loadProviderState(env);
173
+ return { ...buildCatalog(next), savedProviderId: id };
174
+ }
175
+
176
+ function activateProviderUnlocked(params = {}, env = process.env) {
177
+ const state = loadProviderState(env);
178
+ const providerId = normalizeId(params.providerId) || 'builtin';
179
+ if (providerId === 'builtin') {
180
+ commitProviderState(state, {
181
+ public: { ...state.public, activeProviderId: 'builtin' },
182
+ secrets: state.secrets
183
+ });
184
+ return buildCatalog(loadProviderState(env));
185
+ }
186
+ const profile = state.public.profiles.find(item => item.id === providerId);
187
+ if (!profile) {
188
+ throw providerError('provider_not_found', 'Provider no longer exists.');
189
+ }
190
+ if (profile.revision !== normalizeRevision(params.expectedRevision)) {
191
+ throw providerError('provider_revision_conflict', 'Provider changed in another tab. Reload it and retry.');
192
+ }
193
+ const endpointHost = getEndpointHost(profile.baseUrl);
194
+ assertCanActivate({
195
+ profile,
196
+ disclosureHost: params.disclosureHost,
197
+ disclosureBaseUrl: params.disclosureBaseUrl,
198
+ endpointHost
199
+ });
200
+ const nextProfile = {
201
+ ...profile,
202
+ revision: profile.revision + 1,
203
+ lastVerified: profile.lastVerified
204
+ ? { ...profile.lastVerified, revision: profile.revision + 1 }
205
+ : null,
206
+ endpointDisclosureHost: endpointHost,
207
+ endpointDisclosureBaseUrl: profile.baseUrl,
208
+ endpointDisclosureAcceptedAt: Date.now(),
209
+ updatedAt: Date.now()
210
+ };
211
+ commitProviderState(state, {
212
+ public: {
213
+ ...state.public,
214
+ activeProviderId: providerId,
215
+ profiles: state.public.profiles.map(item => item.id === providerId ? nextProfile : item)
216
+ },
217
+ secrets: state.secrets
218
+ });
219
+ return buildCatalog(loadProviderState(env));
220
+ }
221
+
222
+ function clearProviderSecretUnlocked(params = {}, env = process.env) {
223
+ const state = loadProviderState(env);
224
+ const providerId = normalizeId(params.providerId);
225
+ const profile = state.public.profiles.find(item => item.id === providerId);
226
+ if (!profile) {
227
+ throw providerError('provider_not_found', 'Provider no longer exists.');
228
+ }
229
+ if (profile.revision !== normalizeRevision(params.expectedRevision)) {
230
+ throw providerError('provider_revision_conflict', 'Provider changed in another tab. Reload it and retry.');
231
+ }
232
+ const nextRevision = profile.revision + 1;
233
+ const nextProfile = {
234
+ ...profile,
235
+ revision: nextRevision,
236
+ resolvedWireApi: profile.wireApiPreference === 'auto' ? '' : profile.wireApiPreference,
237
+ models: profile.models.map(model => ({ ...model, resolvedUpstreamResponseMode: '' })),
238
+ modelDiagnostics: {},
239
+ lastVerified: null,
240
+ secretUpdatedAt: 0,
241
+ updatedAt: Date.now()
242
+ };
243
+ const secrets = { ...state.secrets.secrets };
244
+ delete secrets[providerId];
245
+ commitProviderState(state, {
246
+ public: {
247
+ ...state.public,
248
+ activeProviderId: state.public.activeProviderId === providerId ? 'builtin' : state.public.activeProviderId,
249
+ profiles: state.public.profiles.map(item => item.id === providerId ? nextProfile : item)
250
+ },
251
+ secrets: { ...state.secrets, secrets }
252
+ });
253
+ return buildCatalog(loadProviderState(env));
254
+ }
255
+
256
+ function recordProviderDiagnosticUnlocked(params = {}, env = process.env) {
257
+ const state = loadProviderState(env);
258
+ const providerId = normalizeId(params.profileId);
259
+ const profile = state.public.profiles.find(item => item.id === providerId);
260
+ if (!profile || profile.revision !== normalizeRevision(params.expectedRevision)) {
261
+ return { recorded: false };
262
+ }
263
+ const modelId = String(params.modelId || '').trim();
264
+ const secret = state.secrets.secrets[providerId] || '';
265
+ const connectionFingerprint = computeConnectionFingerprint(profile, secret, modelId);
266
+ if (connectionFingerprint !== String(params.connectionFingerprint || '') || !isResolvedWireApi(params.wireApi)) {
267
+ return { recorded: false };
268
+ }
269
+ const now = Date.now();
270
+ const diagnostic = buildStoredDiagnostic(params, connectionFingerprint, now);
271
+ const modelDiagnostics = {
272
+ ...(profile.modelDiagnostics || {}),
273
+ [modelId]: diagnostic
274
+ };
275
+ const nextProfile = {
276
+ ...profile,
277
+ resolvedWireApi: profile.wireApiPreference === 'auto' && modelId === profile.defaultModelId
278
+ ? diagnostic.wireApi
279
+ : profile.resolvedWireApi,
280
+ models: profile.models.map(model => model.id === modelId
281
+ ? { ...model, resolvedUpstreamResponseMode: diagnostic.upstreamResponseMode }
282
+ : model),
283
+ modelDiagnostics,
284
+ lastVerified: modelId === profile.defaultModelId
285
+ ? {
286
+ revision: profile.revision,
287
+ at: diagnostic.at,
288
+ modelId,
289
+ wireApi: diagnostic.wireApi,
290
+ upstreamResponseMode: diagnostic.upstreamResponseMode,
291
+ draftFingerprint: connectionFingerprint
292
+ }
293
+ : profile.lastVerified,
294
+ updatedAt: now
295
+ };
296
+ commitProviderState(state, {
297
+ public: {
298
+ ...state.public,
299
+ profiles: state.public.profiles.map(item => item.id === providerId ? nextProfile : item)
300
+ },
301
+ secrets: state.secrets
302
+ });
303
+ return { recorded: true };
304
+ }
305
+
306
+ function deleteProviderUnlocked(params = {}, env = process.env) {
307
+ const state = loadProviderState(env);
308
+ const providerId = normalizeId(params.providerId);
309
+ if (!providerId || providerId === 'builtin') {
310
+ throw providerError('provider_not_found', 'Built-in Codex cannot be deleted.');
311
+ }
312
+ const profile = state.public.profiles.find(item => item.id === providerId);
313
+ if (!profile) {
314
+ throw providerError('provider_not_found', 'Provider no longer exists.');
315
+ }
316
+ if (profile.revision !== normalizeRevision(params.expectedRevision)) {
317
+ throw providerError('provider_revision_conflict', 'Provider changed in another tab. Reload it and retry.');
318
+ }
319
+ const secrets = { ...state.secrets.secrets };
320
+ delete secrets[providerId];
321
+ commitProviderState(state, {
322
+ public: {
323
+ ...state.public,
324
+ activeProviderId: state.public.activeProviderId === providerId ? 'builtin' : state.public.activeProviderId,
325
+ profiles: state.public.profiles.filter(item => item.id !== providerId)
326
+ },
327
+ secrets: { ...state.secrets, secrets }
328
+ });
329
+ return buildCatalog(loadProviderState(env));
330
+ }
331
+
332
+ function resolveDraftSecretUnlocked(params = {}, env = process.env) {
333
+ const state = loadProviderState(env);
334
+ const id = normalizeId(params.profileId);
335
+ const existing = id ? state.public.profiles.find(profile => profile.id === id) : null;
336
+ if (existing && existing.revision !== normalizeRevision(params.expectedRevision)) {
337
+ throw providerError('provider_revision_conflict', 'Provider changed in another tab. Reload it and retry.');
338
+ }
339
+ return applySecretMutation(id ? state.secrets.secrets[id] || '' : '', params.secretMutation);
340
+ }
341
+
342
+ function loadProviderStateUnlocked(env = process.env) {
343
+ const paths = getProviderStorePaths(env);
344
+ ensurePrivateDirectory(paths.root);
345
+ recoverInterruptedTransaction(paths);
346
+ const publicStore = readJson(paths.publicFile, defaultPublicStore());
347
+ const secretStore = readJson(paths.secretFile, defaultSecretStore());
348
+ if (publicStore.storeRevision !== secretStore.storeRevision) {
349
+ throw providerError('provider_store_corrupt', 'Provider profile and secret stores are out of sync.');
350
+ }
351
+ const profiles = Array.isArray(publicStore.profiles)
352
+ ? publicStore.profiles.map(normalizeStoredProfile)
353
+ : [];
354
+ return {
355
+ paths,
356
+ public: {
357
+ schemaVersion: SCHEMA_VERSION,
358
+ storeRevision: normalizeRevision(publicStore.storeRevision),
359
+ activeProviderId: normalizeId(publicStore.activeProviderId) || 'builtin',
360
+ profiles
361
+ },
362
+ secrets: {
363
+ schemaVersion: SCHEMA_VERSION,
364
+ storeRevision: normalizeRevision(secretStore.storeRevision),
365
+ secrets: normalizeSecretMap(secretStore.secrets)
366
+ }
367
+ };
368
+ }
369
+
370
+ function commitProviderState(previous, next) {
371
+ const revision = previous.public.storeRevision + 1;
372
+ const nextPublic = { ...next.public, schemaVersion: SCHEMA_VERSION, storeRevision: revision };
373
+ const nextSecrets = { ...next.secrets, schemaVersion: SCHEMA_VERSION, storeRevision: revision };
374
+ const journal = {
375
+ schemaVersion: SCHEMA_VERSION,
376
+ nextRevision: revision,
377
+ previousPublic: previous.public,
378
+ previousSecrets: previous.secrets
379
+ };
380
+ atomicWriteJson(previous.paths.transactionFile, journal);
381
+ try {
382
+ atomicWriteJson(previous.paths.secretFile, nextSecrets);
383
+ atomicWriteJson(previous.paths.publicFile, nextPublic);
384
+ fs.rmSync(previous.paths.transactionFile, { force: true });
385
+ } catch (error) {
386
+ try {
387
+ atomicWriteJson(previous.paths.secretFile, previous.secrets);
388
+ atomicWriteJson(previous.paths.publicFile, previous.public);
389
+ fs.rmSync(previous.paths.transactionFile, { force: true });
390
+ } catch (_restoreError) {
391
+ // The journal remains the recovery source on the next Native Host start.
392
+ }
393
+ throw providerError('provider_store_unavailable', 'Provider settings could not be saved.', { cause: error });
394
+ }
395
+ }
396
+
397
+ function recoverInterruptedTransaction(paths) {
398
+ if (!fs.existsSync(paths.transactionFile)) {
399
+ return;
400
+ }
401
+ const journal = readJson(paths.transactionFile, null);
402
+ if (!journal?.previousPublic || !journal?.previousSecrets) {
403
+ throw providerError('provider_store_corrupt', 'Provider transaction journal is invalid.');
404
+ }
405
+ const currentPublic = readJson(paths.publicFile, defaultPublicStore());
406
+ const currentSecrets = readJson(paths.secretFile, defaultSecretStore());
407
+ if (currentPublic.storeRevision === journal.nextRevision && currentSecrets.storeRevision === journal.nextRevision) {
408
+ fs.rmSync(paths.transactionFile, { force: true });
409
+ return;
410
+ }
411
+ atomicWriteJson(paths.secretFile, journal.previousSecrets);
412
+ atomicWriteJson(paths.publicFile, journal.previousPublic);
413
+ fs.rmSync(paths.transactionFile, { force: true });
414
+ }
415
+
416
+ function buildCatalog(state) {
417
+ const providers = [
418
+ {
419
+ id: 'builtin',
420
+ kind: 'builtin',
421
+ revision: 0,
422
+ name: 'Built-in Codex',
423
+ editable: false,
424
+ deletable: false,
425
+ models: [],
426
+ hasSecret: false
427
+ },
428
+ ...state.public.profiles.map(profile => sanitizeProfile(profile, Boolean(state.secrets.secrets[profile.id])))
429
+ ];
430
+ const activeProviderId = providers.some(provider => provider.id === state.public.activeProviderId)
431
+ ? state.public.activeProviderId
432
+ : 'builtin';
433
+ return {
434
+ activeProviderId,
435
+ providers,
436
+ storeRevision: state.public.storeRevision
437
+ };
438
+ }
439
+
440
+ function assertCanActivate({ profile, disclosureHost, disclosureBaseUrl, endpointHost }) {
441
+ if (!endpointHost
442
+ || endpointHost !== String(disclosureHost || '').trim()
443
+ || profile.baseUrl !== String(disclosureBaseUrl || '').trim()) {
444
+ throw providerError('provider_disclosure_required', 'Confirm the provider endpoint before activation.');
445
+ }
446
+ }
447
+
448
+ function normalizeProvidedDiagnostics(params = {}) {
449
+ const result = new Map();
450
+ const values = Array.isArray(params.diagnostics) ? params.diagnostics : [];
451
+ for (const value of values) {
452
+ const modelId = String(value?.modelId || value?.testedModelId || '').trim();
453
+ if (modelId) {
454
+ result.set(modelId, {
455
+ ...value,
456
+ connectionFingerprint: String(value.connectionFingerprint || value.draftFingerprint || '')
457
+ });
458
+ }
459
+ }
460
+ if (params.verifiedDraftFingerprint && params.draft?.defaultModelId) {
461
+ result.set(String(params.draft.defaultModelId), {
462
+ modelId: String(params.draft.defaultModelId),
463
+ connectionFingerprint: String(params.verifiedDraftFingerprint),
464
+ wireApi: params.verifiedWireApi,
465
+ upstreamResponseMode: params.verifiedUpstreamResponseMode
466
+ });
467
+ }
468
+ return result;
469
+ }
470
+
471
+ function buildStoredDiagnostic(value, connectionFingerprint, now) {
472
+ const upstreamResponseMode = String(value?.upstreamResponseMode || value?.resolvedUpstreamResponseMode || '').toLowerCase();
473
+ return {
474
+ status: 'tested',
475
+ at: normalizeRevision(value?.at) || now,
476
+ wireApi: String(value?.wireApi || value?.resolvedWireApi || '').toLowerCase(),
477
+ upstreamResponseMode: upstreamResponseMode === 'buffered' ? 'buffered' : 'streaming',
478
+ durationMs: normalizeRevision(value?.durationMs),
479
+ connectionFingerprint
480
+ };
481
+ }
482
+
483
+ function applySecretMutation(currentSecret, mutation = {}) {
484
+ const kind = mutation?.kind || 'unchanged';
485
+ if (kind === 'unchanged') {
486
+ return currentSecret;
487
+ }
488
+ if (kind === 'clear') {
489
+ return '';
490
+ }
491
+ if (kind === 'replace') {
492
+ return normalizeSecret(mutation.value);
493
+ }
494
+ throw providerError('provider_secret_invalid', 'Unknown API key mutation.');
495
+ }
496
+
497
+ function normalizeStoredProfile(value) {
498
+ if (!value || typeof value !== 'object' || !normalizeId(value.id)) {
499
+ throw providerError('provider_store_corrupt', 'Provider store contains an invalid profile.');
500
+ }
501
+ const normalized = normalizeProviderDraft(value);
502
+ return {
503
+ id: normalizeId(value.id),
504
+ revision: Math.max(1, normalizeRevision(value.revision)),
505
+ ...normalized,
506
+ resolvedWireApi: isResolvedWireApi(value.resolvedWireApi) ? value.resolvedWireApi : '',
507
+ endpointDisclosureHost: String(value.endpointDisclosureHost || ''),
508
+ endpointDisclosureBaseUrl: String(value.endpointDisclosureBaseUrl || ''),
509
+ endpointDisclosureAcceptedAt: normalizeRevision(value.endpointDisclosureAcceptedAt),
510
+ secretUpdatedAt: normalizeRevision(value.secretUpdatedAt),
511
+ modelDiagnostics: normalizeStoredDiagnostics(value.modelDiagnostics),
512
+ lastVerified: value.lastVerified && typeof value.lastVerified === 'object' ? { ...value.lastVerified } : null,
513
+ createdAt: normalizeRevision(value.createdAt),
514
+ updatedAt: normalizeRevision(value.updatedAt)
515
+ };
516
+ }
517
+
518
+ function normalizeStoredDiagnostics(value) {
519
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
520
+ return {};
521
+ }
522
+ const result = {};
523
+ for (const [modelId, diagnostic] of Object.entries(value).slice(0, 32)) {
524
+ if (!modelId || !diagnostic || typeof diagnostic !== 'object') continue;
525
+ result[modelId] = {
526
+ status: diagnostic.status === 'failed' ? 'failed' : 'tested',
527
+ at: normalizeRevision(diagnostic.at),
528
+ wireApi: isResolvedWireApi(diagnostic.wireApi) ? String(diagnostic.wireApi).toLowerCase() : '',
529
+ upstreamResponseMode: diagnostic.upstreamResponseMode === 'buffered' ? 'buffered' : 'streaming',
530
+ durationMs: normalizeRevision(diagnostic.durationMs),
531
+ errorCode: String(diagnostic.errorCode || ''),
532
+ connectionFingerprint: String(diagnostic.connectionFingerprint || '')
533
+ };
534
+ }
535
+ return result;
536
+ }
537
+
538
+ function normalizeSecretMap(value) {
539
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
540
+ return {};
541
+ }
542
+ const result = {};
543
+ for (const [id, secret] of Object.entries(value)) {
544
+ const normalizedId = normalizeId(id);
545
+ if (normalizedId) {
546
+ result[normalizedId] = normalizeSecret(secret);
547
+ }
548
+ }
549
+ return result;
550
+ }
551
+
552
+ function getProviderStorePaths(env = process.env) {
553
+ const root = path.resolve(env.CODEX_OVERLEAF_PROVIDER_STORE_DIR || path.join(os.homedir(), '.codex-overleaf'));
554
+ return {
555
+ root,
556
+ publicFile: path.join(root, 'providers.json'),
557
+ secretFile: path.join(root, 'provider-secrets.json'),
558
+ transactionFile: path.join(root, 'provider-transaction.json'),
559
+ lockFile: path.join(root, 'provider-store.lock')
560
+ };
561
+ }
562
+
563
+ function withProviderStoreLock(env, callback) {
564
+ const paths = getProviderStorePaths(env);
565
+ ensurePrivateDirectory(paths.root);
566
+ if (heldStoreLocks.has(paths.lockFile)) {
567
+ return callback();
568
+ }
569
+ const lock = acquireProviderStoreLock(paths.lockFile);
570
+ heldStoreLocks.add(paths.lockFile);
571
+ try {
572
+ return callback();
573
+ } finally {
574
+ heldStoreLocks.delete(paths.lockFile);
575
+ releaseProviderStoreLock(paths.lockFile, lock);
576
+ }
577
+ }
578
+
579
+ function acquireProviderStoreLock(lockFile) {
580
+ const deadline = Date.now() + STORE_LOCK_TIMEOUT_MS;
581
+ const token = `${process.pid}:${Date.now()}:${crypto.randomBytes(8).toString('hex')}`;
582
+ while (true) {
583
+ try {
584
+ const fd = fs.openSync(lockFile, 'wx', 0o600);
585
+ fs.writeFileSync(fd, token, 'utf8');
586
+ fs.fsyncSync(fd);
587
+ return { fd, token };
588
+ } catch (error) {
589
+ if (error?.code !== 'EEXIST') {
590
+ throw providerError('provider_store_unavailable', 'Provider settings could not be locked.', { cause: error });
591
+ }
592
+ try {
593
+ if (Date.now() - fs.statSync(lockFile).mtimeMs > STORE_LOCK_STALE_MS) {
594
+ fs.rmSync(lockFile, { force: true });
595
+ continue;
596
+ }
597
+ } catch (statError) {
598
+ if (statError?.code === 'ENOENT') continue;
599
+ }
600
+ if (Date.now() >= deadline) {
601
+ throw providerError('provider_store_busy', 'Provider settings are being updated by another Native Host. Retry shortly.');
602
+ }
603
+ sleepSync(STORE_LOCK_RETRY_MS);
604
+ }
605
+ }
606
+ }
607
+
608
+ function releaseProviderStoreLock(lockFile, lock) {
609
+ try {
610
+ fs.closeSync(lock.fd);
611
+ } catch (_error) {}
612
+ try {
613
+ if (fs.readFileSync(lockFile, 'utf8') === lock.token) {
614
+ fs.rmSync(lockFile, { force: true });
615
+ }
616
+ } catch (_error) {}
617
+ }
618
+
619
+ function sleepSync(durationMs) {
620
+ const signal = new Int32Array(new SharedArrayBuffer(4));
621
+ Atomics.wait(signal, 0, 0, durationMs);
622
+ }
623
+
624
+ function ensurePrivateDirectory(directory) {
625
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
626
+ try {
627
+ fs.chmodSync(directory, 0o700);
628
+ } catch (_error) {
629
+ // Windows and restricted filesystems may not expose POSIX chmod semantics.
630
+ }
631
+ }
632
+
633
+ function atomicWriteJson(target, value) {
634
+ const temp = `${target}.tmp-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
635
+ const fd = fs.openSync(temp, 'w', 0o600);
636
+ try {
637
+ fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
638
+ fs.fsyncSync(fd);
639
+ } finally {
640
+ fs.closeSync(fd);
641
+ }
642
+ try {
643
+ fs.chmodSync(temp, 0o600);
644
+ } catch (_error) {}
645
+ fs.renameSync(temp, target);
646
+ }
647
+
648
+ function readJson(target, fallback) {
649
+ if (!fs.existsSync(target)) {
650
+ return fallback;
651
+ }
652
+ try {
653
+ const stat = fs.statSync(target);
654
+ if (!stat.isFile() || stat.size > 1024 * 1024) {
655
+ throw new Error('invalid provider store file');
656
+ }
657
+ return JSON.parse(fs.readFileSync(target, 'utf8'));
658
+ } catch (error) {
659
+ throw providerError('provider_store_corrupt', `Provider store ${path.basename(target)} is invalid.`, { cause: error });
660
+ }
661
+ }
662
+
663
+ function defaultPublicStore() {
664
+ return { schemaVersion: SCHEMA_VERSION, storeRevision: 0, activeProviderId: 'builtin', profiles: [] };
665
+ }
666
+
667
+ function defaultSecretStore() {
668
+ return { schemaVersion: SCHEMA_VERSION, storeRevision: 0, secrets: {} };
669
+ }
670
+
671
+ function normalizeId(value) {
672
+ const text = typeof value === 'string' ? value.trim() : '';
673
+ return /^[A-Za-z0-9_-]{1,80}$/.test(text) ? text : '';
674
+ }
675
+
676
+ function normalizeRevision(value) {
677
+ const number = Number(value);
678
+ return Number.isFinite(number) && number >= 0 ? Math.floor(number) : 0;
679
+ }
680
+
681
+ module.exports = {
682
+ activateProvider,
683
+ clearProviderSecret,
684
+ deleteProvider,
685
+ getProviderStorePaths,
686
+ listProviders,
687
+ loadProviderState,
688
+ recordProviderDiagnostic,
689
+ resolveDraftSecret,
690
+ upsertProvider
691
+ };