@cortexkit/opencode-openai-auth 0.3.3 → 0.4.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 (57) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +1 -1
  3. package/dist/codex-http.d.ts +3 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/core/accounts.d.ts +9 -3
  6. package/dist/core/oauth.d.ts +1 -1
  7. package/dist/core/quota-manager.d.ts +2 -2
  8. package/dist/{index-k5zwf3j1.js → index-1anzb8jh.js} +1 -1
  9. package/dist/index-1r02s323.js +6 -0
  10. package/dist/index-2ng03cby.js +2 -0
  11. package/dist/{index-e7m7qbne.js → index-6y6ss1kc.js} +1 -1
  12. package/dist/{index-1xmasv0x.js → index-b6kp9ydc.js} +1 -1
  13. package/dist/index-cz1bkmjz.js +2 -0
  14. package/dist/index-dzg88nw2.js +89 -0
  15. package/dist/index-y67qn95a.js +2 -0
  16. package/dist/index.d.ts +8 -1
  17. package/dist/index.js +65 -65
  18. package/dist/model-costs.d.ts +29 -0
  19. package/dist/prompt-context.d.ts +1 -1
  20. package/dist/quota-normalize-8yzxqy78.js +1 -0
  21. package/dist/quota-normalize.d.ts +13 -4
  22. package/dist/rpc/port-file.js +1 -1
  23. package/dist/rpc/rpc-client.js +1 -1
  24. package/dist/rpc/rpc-dir.js +1 -1
  25. package/dist/sidebar-state.d.ts +47 -2
  26. package/dist/sidebar-state.js +1 -1
  27. package/dist/token-util-h46jb3eg.js +1 -0
  28. package/dist/token-yzcb1p3e.js +1 -0
  29. package/dist/tui/command-dialogs.d.ts +8 -1
  30. package/dist/tui-preferences.js +1 -1
  31. package/dist/tui.d.ts +18 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/ws-pool.d.ts +1 -0
  34. package/package.json +11 -8
  35. package/src/core/refresh-file-lock.ts +293 -0
  36. package/src/sidebar-state.ts +392 -13
  37. package/src/tui/command-dialogs.tsx +43 -25
  38. package/src/tui/entry.mjs +37 -0
  39. package/src/tui-compiled/logger.ts +161 -0
  40. package/src/tui-compiled/rpc/port-file.ts +70 -0
  41. package/src/tui-compiled/rpc/protocol.ts +31 -0
  42. package/src/tui-compiled/rpc/rpc-client.ts +78 -0
  43. package/src/tui-compiled/rpc/rpc-dir.ts +22 -0
  44. package/src/tui-compiled/sidebar-state.ts +695 -0
  45. package/src/tui-compiled/tui/command-dialogs.tsx +645 -0
  46. package/src/tui-compiled/tui-preferences.ts +328 -0
  47. package/src/tui-compiled/tui.tsx +1098 -0
  48. package/src/tui-compiled/util/open-url.ts +30 -0
  49. package/src/tui.tsx +158 -77
  50. package/dist/index-5s67238f.js +0 -2
  51. package/dist/index-d9hq0tmc.js +0 -2
  52. package/dist/index-mrg2fms9.js +0 -2
  53. package/dist/index-phrx93zy.js +0 -92
  54. package/dist/index-qdh7pt5a.js +0 -3
  55. package/dist/quota-normalize-dfdpwwm3.js +0 -1
  56. package/dist/token-01dcf92a.js +0 -1
  57. package/dist/token-util-rkqd82q9.js +0 -1
@@ -0,0 +1,293 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
3
+ import { homedir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+
6
+ const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis)
7
+ const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis)
8
+
9
+ export const ACCOUNT_FILE_NAME = 'openai-auth.json'
10
+ export const ACCOUNT_STATE_FILE_NAME = 'openai-auth-state.json'
11
+
12
+ function getConfigDir() {
13
+ if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
14
+ return process.env.OPENCODE_CONFIG_DIR.trim()
15
+ }
16
+ return join(
17
+ process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
18
+ 'opencode',
19
+ )
20
+ }
21
+
22
+ // A concurrent contender renaming the freshly-created eviction-marker directory
23
+ // away surfaces the vanished parent differently per platform: ENOENT on Linux,
24
+ // EINVAL or ENOTDIR on macOS/APFS. All three mean the marker is no longer ours
25
+ // to hold — a lost race the caller should retry, not a fatal lock error.
26
+ export function isLostMarkerRaceError(error: unknown): boolean {
27
+ const code = (error as NodeJS.ErrnoException | null)?.code
28
+ return code === 'ENOENT' || code === 'EINVAL' || code === 'ENOTDIR'
29
+ }
30
+
31
+ export function getAccountStoragePath() {
32
+ return (
33
+ process.env.OPENCODE_OPENAI_AUTH_FILE?.trim() ||
34
+ join(getConfigDir(), ACCOUNT_FILE_NAME)
35
+ )
36
+ }
37
+
38
+ export function getAccountStatePath(configPath = getAccountStoragePath()) {
39
+ const explicit = process.env.OPENCODE_OPENAI_AUTH_STATE_FILE?.trim()
40
+ if (explicit) return explicit
41
+ return configPath.endsWith(ACCOUNT_FILE_NAME)
42
+ ? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
43
+ : `${configPath}.state.json`
44
+ }
45
+
46
+ export async function acquireRefreshFileLock(options: {
47
+ name: string
48
+ ttlMs: number
49
+ path?: string
50
+ now?: () => number
51
+ renew?: boolean
52
+ renewIntervalMs?: number
53
+ onStep?: (
54
+ step:
55
+ | 'stale-marker-stat'
56
+ | 'stale-marker-claimed'
57
+ | 'stale-lock-confirmed'
58
+ | 'eviction-marker-acquired',
59
+ ) => void | Promise<void>
60
+ }): Promise<{ release: () => Promise<void> } | null> {
61
+ const accountPath = options.path ?? getAccountStoragePath()
62
+ const lockPath = `${accountPath}.${options.name}.lock`
63
+ const legacyOwnerPath = join(lockPath, 'owner.json')
64
+ const ownerId = randomUUID()
65
+ const now = options.now ?? Date.now
66
+ let renewTimer: ReturnType<typeof setTimeout> | null = null
67
+ let released = false
68
+
69
+ async function readOwner() {
70
+ try {
71
+ return JSON.parse(await readFile(lockPath, 'utf8'))
72
+ } catch (error) {
73
+ const code = (error as NodeJS.ErrnoException).code
74
+ if (code !== 'EISDIR') throw error
75
+ return JSON.parse(await readFile(legacyOwnerPath, 'utf8'))
76
+ }
77
+ }
78
+
79
+ async function writeOwner() {
80
+ await writeFile(
81
+ lockPath,
82
+ `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
83
+ { encoding: 'utf8', mode: 0o600 },
84
+ )
85
+ }
86
+
87
+ async function tryAcquire() {
88
+ try {
89
+ await writeFile(
90
+ lockPath,
91
+ `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
92
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' },
93
+ )
94
+ return true
95
+ } catch (error) {
96
+ const code = (error as NodeJS.ErrnoException).code
97
+ if (code === 'EEXIST' || code === 'EISDIR') return false
98
+ throw error
99
+ }
100
+ }
101
+
102
+ function scheduleRenewal() {
103
+ if (!options.renew || released) return
104
+ const intervalMs =
105
+ options.renewIntervalMs ?? Math.max(1_000, Math.floor(options.ttlMs / 3))
106
+ renewTimer = setRefreshLockRenewalTimeout(() => {
107
+ void (async () => {
108
+ try {
109
+ const owner = await readOwner()
110
+ const currentNow = now()
111
+ if (
112
+ released ||
113
+ owner?.ownerId !== ownerId ||
114
+ Number(owner?.expiresAt) <= currentNow
115
+ ) {
116
+ return
117
+ }
118
+ await writeOwner()
119
+ scheduleRenewal()
120
+ } catch {
121
+ // If renewal fails, contenders will wait until the last written expiry.
122
+ }
123
+ })()
124
+ }, intervalMs)
125
+ if ('unref' in renewTimer) renewTimer.unref()
126
+ }
127
+
128
+ let acquired = await tryAcquire()
129
+ if (!acquired) {
130
+ // Fencing-token eviction: a directory-based marker (mkdir O_EXCL) serializes
131
+ // destructive removal to one contender at a time. The marker holds an owner
132
+ // file so ownership survives a stale-marker recovery rename: the recovering
133
+ // contender renames the stale directory, then must re-check ownership before
134
+ // acting — preventing the 3rd interleaving where a stale observer renames the
135
+ // FRESH marker the mkdir-winner created.
136
+ const evictPath = `${lockPath}.evicting`
137
+ const evictOwnerPath = join(evictPath, 'owner.json')
138
+ const evictOwnerId = randomUUID()
139
+ const EVICT_TTL = 5_000
140
+ const MAX_STEAL_ATTEMPTS = 8
141
+
142
+ async function backoff() {
143
+ await new Promise((resolve) =>
144
+ setTimeout(resolve, Math.floor(Math.random() * 4)),
145
+ )
146
+ }
147
+
148
+ async function lockIsLive() {
149
+ try {
150
+ const currentOwner = await readOwner()
151
+ return Number(currentOwner?.expiresAt) > now()
152
+ } catch {
153
+ try {
154
+ const current = await stat(lockPath)
155
+ return current.mtimeMs + options.ttlMs > now()
156
+ } catch {
157
+ // Lock doesn't exist — safe to acquire.
158
+ return false
159
+ }
160
+ }
161
+ }
162
+
163
+ // Fail-closed: any read error means we do NOT own the marker.
164
+ async function ownsEvictionMarker() {
165
+ try {
166
+ const owner = JSON.parse(await readFile(evictOwnerPath, 'utf8'))
167
+ return owner?.ownerId === evictOwnerId
168
+ } catch {
169
+ return false
170
+ }
171
+ }
172
+
173
+ async function tryAcquireEvictionMarker() {
174
+ await mkdir(evictPath)
175
+ try {
176
+ await writeFile(
177
+ evictOwnerPath,
178
+ `${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`,
179
+ { encoding: 'utf8', mode: 0o600, flag: 'wx' },
180
+ )
181
+ } catch (error) {
182
+ // A competing contender can rename our just-created marker directory
183
+ // away between the mkdir above and this write (the stale-marker steal
184
+ // path below does exactly that). That is a lost race, not a failure, so
185
+ // report it as such and let the caller back off and retry rather than
186
+ // failing the whole lock acquisition.
187
+ if (isLostMarkerRaceError(error)) return false
188
+ await releaseEvictionMarker()
189
+ throw error
190
+ }
191
+ await options.onStep?.('eviction-marker-acquired')
192
+ return true
193
+ }
194
+
195
+ async function releaseEvictionMarker() {
196
+ if (await ownsEvictionMarker()) {
197
+ await rm(evictPath, { recursive: true, force: true }).catch(() => {})
198
+ }
199
+ }
200
+
201
+ for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
202
+ acquired = await tryAcquire()
203
+ if (acquired) break
204
+ if (await lockIsLive()) return null
205
+
206
+ try {
207
+ if (!(await tryAcquireEvictionMarker())) {
208
+ await backoff()
209
+ continue
210
+ }
211
+ } catch (evictError) {
212
+ const code = (evictError as NodeJS.ErrnoException).code
213
+ if (code !== 'EEXIST') throw evictError
214
+
215
+ let evictStat: Awaited<ReturnType<typeof stat>>
216
+ try {
217
+ evictStat = await stat(evictPath)
218
+ } catch (statError) {
219
+ if ((statError as NodeJS.ErrnoException).code === 'ENOENT') {
220
+ await backoff()
221
+ continue
222
+ }
223
+ throw statError
224
+ }
225
+ if (evictStat.mtimeMs + EVICT_TTL > now()) return null
226
+
227
+ await options.onStep?.('stale-marker-stat')
228
+ const claimedPath = `${evictPath}.${randomUUID()}`
229
+ try {
230
+ await rename(evictPath, claimedPath)
231
+ } catch (renameError) {
232
+ if ((renameError as NodeJS.ErrnoException).code === 'ENOENT') {
233
+ await backoff()
234
+ continue
235
+ }
236
+ throw renameError
237
+ }
238
+ await options.onStep?.('stale-marker-claimed')
239
+ await rm(claimedPath, { recursive: true, force: true }).catch(() => {})
240
+ await backoff()
241
+ continue
242
+ }
243
+
244
+ try {
245
+ if (await lockIsLive()) return null
246
+ // Fence check 1: verify we still own the marker before acting on the
247
+ // stale-lock-confirmed decision.
248
+ if (!(await ownsEvictionMarker())) return null
249
+ await options.onStep?.('stale-lock-confirmed')
250
+ // Fence check 2: re-verify ownership after the seam (another contender
251
+ // may have renamed our fresh marker while we were paused here).
252
+ if (!(await ownsEvictionMarker())) return null
253
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {})
254
+ // Fence check 3: re-verify ownership after removing the stale lock.
255
+ if (!(await ownsEvictionMarker())) return null
256
+ acquired = await tryAcquire()
257
+ if (!acquired) return null
258
+ // Fence check 4: re-verify ownership after acquiring the lock. If the
259
+ // marker was stolen between tryAcquire and this check, release the
260
+ // just-acquired lock and return null (fail-closed).
261
+ if (!(await ownsEvictionMarker())) {
262
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {})
263
+ acquired = false
264
+ return null
265
+ }
266
+ break
267
+ } finally {
268
+ await releaseEvictionMarker()
269
+ }
270
+ }
271
+ }
272
+
273
+ if (!acquired) return null
274
+
275
+ scheduleRenewal()
276
+
277
+ return {
278
+ release: async () => {
279
+ released = true
280
+ if (renewTimer) {
281
+ clearRefreshLockRenewalTimeout(renewTimer)
282
+ renewTimer = null
283
+ }
284
+ try {
285
+ const owner = await readOwner()
286
+ if (owner?.ownerId !== ownerId) return
287
+ } catch {
288
+ return
289
+ }
290
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {})
291
+ },
292
+ }
293
+ }