@cortexkit/opencode-openai-auth 0.7.2 → 0.9.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.
- package/README.md +9 -5
- package/dist/index-301y4qa1.js +2 -0
- package/dist/index-338z7171.js +211 -0
- package/dist/index-5y4vzdf8.js +2 -0
- package/dist/{index-zxqb5ydj.js → index-9ehhb051.js} +1 -1
- package/dist/{index-zg1x8sty.js → index-jwn4r3sw.js} +1 -1
- package/dist/{index-9jpc8yct.js → index-sdt2d2ra.js} +1 -1
- package/dist/index-w42s0g6m.js +2 -0
- package/dist/index.d.ts +213 -36
- package/dist/index.js +59 -154
- package/dist/internal-Y7QNVDJT.d.ts +908 -0
- package/dist/quota-normalize-3qffywcm.js +1 -0
- package/dist/rpc/port-file.d.ts +11 -9
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +2 -21
- package/dist/rpc/rpc-client.d.ts +7 -4
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.d.ts +7 -5
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state-B98sERYt.d.ts +219 -0
- package/dist/sidebar-state.d.ts +2 -171
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-bvp0y166.js → token-9823y0pc.js} +1 -1
- package/dist/token-util-ypgz7rge.js +1 -0
- package/dist/tui-preferences.d.ts +27 -26
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +42 -19
- package/package.json +8 -14
- package/src/tui/entry.mjs +2 -2
- package/src/tui-compiled/{core/account-paths.ts → raw/core-account-paths.ts} +29 -18
- package/src/tui-compiled/raw/core-custody-state.ts +243 -0
- package/src/tui-compiled/raw/logger.ts +53 -0
- package/src/tui-compiled/{rpc/port-file.ts → raw/rpc-port-file.ts} +1 -1
- package/src/tui-compiled/raw/rpc-protocol.ts +12 -0
- package/src/{rpc/rpc-client.ts → tui-compiled/raw/rpc-rpc-client.ts} +2 -2
- package/src/{sidebar-state.ts → tui-compiled/raw/sidebar-state.ts} +142 -13
- package/src/{tui/command-dialogs.tsx → tui-compiled/raw/tui-command-dialogs.tsx} +94 -44
- package/src/{tui.tsx → tui-compiled/raw/tui.tsx} +149 -28
- package/src/{core/account-paths.ts → tui-compiled/runtime/core-account-paths.ts} +29 -18
- package/src/tui-compiled/runtime/core-custody-state.ts +243 -0
- package/src/tui-compiled/runtime/logger.ts +53 -0
- package/src/{rpc/port-file.ts → tui-compiled/runtime/rpc-port-file.ts} +1 -1
- package/src/tui-compiled/runtime/rpc-protocol.ts +12 -0
- package/src/tui-compiled/{rpc/rpc-client.ts → runtime/rpc-rpc-client.ts} +2 -2
- package/src/tui-compiled/{sidebar-state.ts → runtime/sidebar-state.ts} +142 -13
- package/src/tui-compiled/{tui/command-dialogs.tsx → runtime/tui-command-dialogs.tsx} +73 -37
- package/src/tui-compiled/{tui.tsx → runtime/tui.tsx} +251 -153
- package/src/tui-compiled/shared/internal.ts +6 -0
- package/src/{logger.ts → tui-compiled/shared/logger.ts} +91 -23
- package/src/tui-compiled/shared/paths.ts +33 -0
- package/src/{core → tui-compiled/shared}/refresh-file-lock.ts +7 -4
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -15
- package/dist/codex-http.d.ts +0 -3
- package/dist/commands.d.ts +0 -88
- package/dist/config.d.ts +0 -47
- package/dist/core/account-paths.d.ts +0 -25
- package/dist/core/accounts.d.ts +0 -330
- package/dist/core/atomic-write.d.ts +0 -1
- package/dist/core/background-quota-refresh.d.ts +0 -46
- package/dist/core/backoff.d.ts +0 -53
- package/dist/core/cachekeep.d.ts +0 -118
- package/dist/core/oauth.d.ts +0 -138
- package/dist/core/provider.d.ts +0 -70
- package/dist/core/quota-manager.d.ts +0 -192
- package/dist/core/refresh-all-quota.d.ts +0 -86
- package/dist/core/refresh-file-lock.d.ts +0 -12
- package/dist/core/reset-credits.d.ts +0 -116
- package/dist/core/sticky-routing.d.ts +0 -43
- package/dist/dump.d.ts +0 -20
- package/dist/hosted-web-search.d.ts +0 -5
- package/dist/index-87145jkw.js +0 -2
- package/dist/index-8f1200qx.js +0 -6
- package/dist/index-cfjed1st.js +0 -89
- package/dist/index-nrvjr97q.js +0 -3
- package/dist/index-qaggaze1.js +0 -2
- package/dist/index-sdcpzbnk.js +0 -2
- package/dist/logger.d.ts +0 -22
- package/dist/model-costs.d.ts +0 -29
- package/dist/prompt-context.d.ts +0 -19
- package/dist/quota-normalize-dvwtmy7f.js +0 -1
- package/dist/quota-normalize.d.ts +0 -43
- package/dist/raw-ws-bun.d.ts +0 -40
- package/dist/raw-ws-node.d.ts +0 -37
- package/dist/raw-ws-upgrade.d.ts +0 -2
- package/dist/raw-ws.d.ts +0 -3
- package/dist/response-stream-error.d.ts +0 -12
- package/dist/rpc/notifications.d.ts +0 -5
- package/dist/rpc/rpc-server.d.ts +0 -17
- package/dist/token-util-jcj7k7pa.js +0 -1
- package/dist/tui/command-dialogs.d.ts +0 -21
- package/dist/util/error.d.ts +0 -1
- package/dist/util/open-url.d.ts +0 -6
- package/dist/util/proxy-env.d.ts +0 -4
- package/dist/util/record.d.ts +0 -1
- package/dist/util/stable-json.d.ts +0 -1
- package/dist/util/uuid-v7.d.ts +0 -1
- package/dist/version.d.ts +0 -1
- package/dist/ws-pool.d.ts +0 -57
- package/dist/ws.d.ts +0 -71
- package/src/rpc/notifications.ts +0 -54
- package/src/rpc/protocol.ts +0 -33
- package/src/rpc/rpc-server.ts +0 -172
- package/src/tui-compiled/core/refresh-file-lock.ts +0 -381
- package/src/tui-compiled/logger.ts +0 -196
- package/src/tui-compiled/rpc/protocol.ts +0 -33
- package/src/util/error.ts +0 -9
- package/src/util/open-url.ts +0 -30
- package/src/util/proxy-env.ts +0 -42
- package/src/util/record.ts +0 -3
- package/src/util/stable-json.ts +0 -15
- package/src/util/uuid-v7.ts +0 -18
- /package/src/{rpc/rpc-dir.ts → tui-compiled/raw/rpc-rpc-dir.ts} +0 -0
- /package/src/tui-compiled/{tui-preferences.ts → raw/tui-preferences.ts} +0 -0
- /package/src/tui-compiled/{rpc/rpc-dir.ts → runtime/rpc-rpc-dir.ts} +0 -0
- /package/src/{tui-preferences.ts → tui-compiled/runtime/tui-preferences.ts} +0 -0
- /package/src/tui-compiled/{util → shared/util}/error.ts +0 -0
- /package/src/tui-compiled/{util → shared/util}/open-url.ts +0 -0
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
4
|
-
import { getAccountStoragePath } from './account-paths'
|
|
5
|
-
|
|
6
|
-
const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis)
|
|
7
|
-
const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis)
|
|
8
|
-
|
|
9
|
-
// A concurrent contender renaming the freshly-created eviction-marker directory
|
|
10
|
-
// away surfaces the vanished parent differently per platform: ENOENT on Linux,
|
|
11
|
-
// EINVAL or ENOTDIR on macOS/APFS. All three mean the marker is no longer ours
|
|
12
|
-
// to hold — a lost race the caller should retry, not a fatal lock error.
|
|
13
|
-
export function isLostMarkerRaceError(error: unknown): boolean {
|
|
14
|
-
const code = (error as NodeJS.ErrnoException | null)?.code
|
|
15
|
-
return code === 'ENOENT' || code === 'EINVAL' || code === 'ENOTDIR'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function acquireRefreshFileLock(options: {
|
|
19
|
-
name: string
|
|
20
|
-
ttlMs: number
|
|
21
|
-
path?: string
|
|
22
|
-
now?: () => number
|
|
23
|
-
renew?: boolean
|
|
24
|
-
renewIntervalMs?: number
|
|
25
|
-
onStep?: (
|
|
26
|
-
step:
|
|
27
|
-
| 'stale-marker-stat'
|
|
28
|
-
| 'stale-marker-claimed'
|
|
29
|
-
| 'stale-lock-confirmed'
|
|
30
|
-
| 'eviction-marker-acquired'
|
|
31
|
-
| 'renewal-owner-confirmed'
|
|
32
|
-
| 'renewal-marker-unavailable'
|
|
33
|
-
| 'renewal-write-fenced'
|
|
34
|
-
| 'renewal-write-ready'
|
|
35
|
-
| 'relinquish-read'
|
|
36
|
-
| 'renewal-finished'
|
|
37
|
-
| 'release-owner-confirmed',
|
|
38
|
-
) => void | Promise<void>
|
|
39
|
-
}): Promise<{ release: () => Promise<void> } | null> {
|
|
40
|
-
const accountPath = options.path ?? getAccountStoragePath()
|
|
41
|
-
const lockPath = `${accountPath}.${options.name}.lock`
|
|
42
|
-
const legacyOwnerPath = join(lockPath, 'owner.json')
|
|
43
|
-
const ownerId = randomUUID()
|
|
44
|
-
const now = options.now ?? Date.now
|
|
45
|
-
let renewTimer: ReturnType<typeof setTimeout> | null = null
|
|
46
|
-
let released = false
|
|
47
|
-
let renewalInFlight: Promise<void> | null = null
|
|
48
|
-
// Fencing-token eviction: a directory-based marker (mkdir O_EXCL) serializes
|
|
49
|
-
// destructive removal and generation-sensitive owner mutations. The marker
|
|
50
|
-
// holds an owner file so ownership survives a stale-marker recovery rename:
|
|
51
|
-
// the recovering contender renames the stale directory, then must re-check
|
|
52
|
-
// ownership before acting — preventing the 3rd interleaving where a stale
|
|
53
|
-
// observer renames the FRESH marker the mkdir-winner created.
|
|
54
|
-
const evictPath = `${lockPath}.evicting`
|
|
55
|
-
const evictOwnerPath = join(evictPath, 'owner.json')
|
|
56
|
-
const evictOwnerId = randomUUID()
|
|
57
|
-
const EVICT_TTL = 5_000
|
|
58
|
-
const MAX_STEAL_ATTEMPTS = 8
|
|
59
|
-
|
|
60
|
-
async function readOwner() {
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(await readFile(lockPath, 'utf8'))
|
|
63
|
-
} catch (error) {
|
|
64
|
-
const code = (error as NodeJS.ErrnoException).code
|
|
65
|
-
if (code !== 'EISDIR') throw error
|
|
66
|
-
return JSON.parse(await readFile(legacyOwnerPath, 'utf8'))
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function writeOwner() {
|
|
71
|
-
await writeFile(
|
|
72
|
-
lockPath,
|
|
73
|
-
`${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
|
|
74
|
-
{ encoding: 'utf8', mode: 0o600 },
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function tryAcquire() {
|
|
79
|
-
try {
|
|
80
|
-
await writeFile(
|
|
81
|
-
lockPath,
|
|
82
|
-
`${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
|
|
83
|
-
{ encoding: 'utf8', mode: 0o600, flag: 'wx' },
|
|
84
|
-
)
|
|
85
|
-
return true
|
|
86
|
-
} catch (error) {
|
|
87
|
-
const code = (error as NodeJS.ErrnoException).code
|
|
88
|
-
if (code === 'EEXIST' || code === 'EISDIR') return false
|
|
89
|
-
if (code === 'ENOENT') {
|
|
90
|
-
// Reboots can clear runtime temp directories before the next lock acquisition.
|
|
91
|
-
await mkdir(dirname(lockPath), { recursive: true })
|
|
92
|
-
try {
|
|
93
|
-
await writeFile(
|
|
94
|
-
lockPath,
|
|
95
|
-
`${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`,
|
|
96
|
-
{ encoding: 'utf8', mode: 0o600, flag: 'wx' },
|
|
97
|
-
)
|
|
98
|
-
return true
|
|
99
|
-
} catch (retryError) {
|
|
100
|
-
const retryCode = (retryError as NodeJS.ErrnoException).code
|
|
101
|
-
if (retryCode === 'EEXIST' || retryCode === 'EISDIR') return false
|
|
102
|
-
throw retryError
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
throw error
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function backoff() {
|
|
110
|
-
await new Promise((resolve) =>
|
|
111
|
-
setTimeout(resolve, Math.floor(Math.random() * 4)),
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function lockIsLive() {
|
|
116
|
-
try {
|
|
117
|
-
const currentOwner = await readOwner()
|
|
118
|
-
return Number(currentOwner?.expiresAt) > now()
|
|
119
|
-
} catch {
|
|
120
|
-
try {
|
|
121
|
-
const current = await stat(lockPath)
|
|
122
|
-
return current.mtimeMs + options.ttlMs > now()
|
|
123
|
-
} catch {
|
|
124
|
-
// Lock doesn't exist — safe to acquire.
|
|
125
|
-
return false
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Fail-closed: any read error means we do NOT own the marker.
|
|
131
|
-
async function ownsEvictionMarker() {
|
|
132
|
-
try {
|
|
133
|
-
const owner = JSON.parse(await readFile(evictOwnerPath, 'utf8'))
|
|
134
|
-
return owner?.ownerId === evictOwnerId
|
|
135
|
-
} catch {
|
|
136
|
-
return false
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function releaseEvictionMarker() {
|
|
141
|
-
if (await ownsEvictionMarker()) {
|
|
142
|
-
await rm(evictPath, { recursive: true, force: true }).catch(() => {})
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function tryAcquireEvictionMarker() {
|
|
147
|
-
await mkdir(evictPath)
|
|
148
|
-
try {
|
|
149
|
-
await writeFile(
|
|
150
|
-
evictOwnerPath,
|
|
151
|
-
`${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`,
|
|
152
|
-
{ encoding: 'utf8', mode: 0o600, flag: 'wx' },
|
|
153
|
-
)
|
|
154
|
-
} catch (error) {
|
|
155
|
-
// A competing contender can rename our just-created marker directory
|
|
156
|
-
// away between the mkdir above and this write (the stale-marker steal
|
|
157
|
-
// path below does exactly that). That is a lost race, not a failure, so
|
|
158
|
-
// report it as such and let the caller back off and retry rather than
|
|
159
|
-
// failing the whole lock acquisition.
|
|
160
|
-
if (isLostMarkerRaceError(error)) return false
|
|
161
|
-
await releaseEvictionMarker()
|
|
162
|
-
throw error
|
|
163
|
-
}
|
|
164
|
-
if (options.onStep) await options.onStep('eviction-marker-acquired')
|
|
165
|
-
return true
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function recoverStaleEvictionMarker(): Promise<
|
|
169
|
-
'fresh' | 'missing' | 'recovered'
|
|
170
|
-
> {
|
|
171
|
-
let evictStat: Awaited<ReturnType<typeof stat>>
|
|
172
|
-
try {
|
|
173
|
-
evictStat = await stat(evictPath)
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing'
|
|
176
|
-
throw error
|
|
177
|
-
}
|
|
178
|
-
if (evictStat.mtimeMs + EVICT_TTL > now()) return 'fresh'
|
|
179
|
-
|
|
180
|
-
if (options.onStep) await options.onStep('stale-marker-stat')
|
|
181
|
-
const claimedPath = `${evictPath}.${randomUUID()}`
|
|
182
|
-
try {
|
|
183
|
-
await rename(evictPath, claimedPath)
|
|
184
|
-
} catch (error) {
|
|
185
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing'
|
|
186
|
-
throw error
|
|
187
|
-
}
|
|
188
|
-
if (options.onStep) await options.onStep('stale-marker-claimed')
|
|
189
|
-
await rm(claimedPath, { recursive: true, force: true }).catch(() => {})
|
|
190
|
-
return 'recovered'
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function withEvictionMarker(action: () => Promise<void>) {
|
|
194
|
-
try {
|
|
195
|
-
if (!(await tryAcquireEvictionMarker())) return false
|
|
196
|
-
} catch (error) {
|
|
197
|
-
if ((error as NodeJS.ErrnoException).code === 'EEXIST') return false
|
|
198
|
-
throw error
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
await action()
|
|
203
|
-
} finally {
|
|
204
|
-
await releaseEvictionMarker()
|
|
205
|
-
}
|
|
206
|
-
return true
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Marker loss after a write may mean our record replaced a successor's.
|
|
210
|
-
// Delete only a record still owned by us; a concurrent successor write can
|
|
211
|
-
// then yield zero winners, never two.
|
|
212
|
-
async function relinquishLockAfterMarkerLoss() {
|
|
213
|
-
for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
|
|
214
|
-
if (options.onStep) await options.onStep('relinquish-read')
|
|
215
|
-
let owner: { ownerId?: string } | undefined
|
|
216
|
-
try {
|
|
217
|
-
owner = await readOwner()
|
|
218
|
-
} catch {
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
if (owner?.ownerId !== ownerId) return
|
|
222
|
-
try {
|
|
223
|
-
await rm(lockPath, { recursive: true, force: true })
|
|
224
|
-
return
|
|
225
|
-
} catch {
|
|
226
|
-
await backoff()
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function scheduleRenewal() {
|
|
232
|
-
if (!options.renew || released) return
|
|
233
|
-
const intervalMs =
|
|
234
|
-
options.renewIntervalMs ?? Math.max(1_000, Math.floor(options.ttlMs / 3))
|
|
235
|
-
renewTimer = setRefreshLockRenewalTimeout(() => {
|
|
236
|
-
const renewal = (async () => {
|
|
237
|
-
let shouldReschedule = !released
|
|
238
|
-
try {
|
|
239
|
-
const markerAcquired = await withEvictionMarker(async () => {
|
|
240
|
-
const owner = await readOwner()
|
|
241
|
-
const currentNow = now()
|
|
242
|
-
if (released || owner?.ownerId !== ownerId) {
|
|
243
|
-
shouldReschedule = false
|
|
244
|
-
return
|
|
245
|
-
}
|
|
246
|
-
// An expired lease is no longer ours to extend; a contender may
|
|
247
|
-
// already be eligible to acquire it.
|
|
248
|
-
if (Number(owner?.expiresAt) <= currentNow) {
|
|
249
|
-
shouldReschedule = false
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
if (options.onStep) await options.onStep('renewal-owner-confirmed')
|
|
253
|
-
if (released) {
|
|
254
|
-
shouldReschedule = false
|
|
255
|
-
return
|
|
256
|
-
}
|
|
257
|
-
if (!(await ownsEvictionMarker())) return
|
|
258
|
-
if (options.onStep) await options.onStep('renewal-write-fenced')
|
|
259
|
-
if (released) {
|
|
260
|
-
shouldReschedule = false
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
if (!(await ownsEvictionMarker())) return
|
|
264
|
-
if (options.onStep) await options.onStep('renewal-write-ready')
|
|
265
|
-
await writeOwner()
|
|
266
|
-
if (!(await ownsEvictionMarker())) {
|
|
267
|
-
// Marker read errors fail closed: prompt relinquish avoids ambiguity
|
|
268
|
-
// instead of waiting for TTL; both outcomes keep zero or one winner.
|
|
269
|
-
shouldReschedule = false
|
|
270
|
-
await relinquishLockAfterMarkerLoss()
|
|
271
|
-
return
|
|
272
|
-
}
|
|
273
|
-
})
|
|
274
|
-
if (!markerAcquired && options.onStep) {
|
|
275
|
-
await options.onStep('renewal-marker-unavailable')
|
|
276
|
-
}
|
|
277
|
-
} catch {
|
|
278
|
-
// Transient marker and filesystem failures retry on the next interval.
|
|
279
|
-
} finally {
|
|
280
|
-
if (options.onStep) {
|
|
281
|
-
try {
|
|
282
|
-
await options.onStep('renewal-finished')
|
|
283
|
-
} catch {
|
|
284
|
-
// Test seams must not turn an otherwise-safe renewal into a rejection.
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (shouldReschedule && !released) scheduleRenewal()
|
|
288
|
-
}
|
|
289
|
-
})()
|
|
290
|
-
renewalInFlight = renewal
|
|
291
|
-
void renewal.finally(() => {
|
|
292
|
-
if (renewalInFlight === renewal) renewalInFlight = null
|
|
293
|
-
})
|
|
294
|
-
}, intervalMs)
|
|
295
|
-
if ('unref' in renewTimer) renewTimer.unref()
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
let acquired = await tryAcquire()
|
|
299
|
-
if (!acquired) {
|
|
300
|
-
for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
|
|
301
|
-
acquired = await tryAcquire()
|
|
302
|
-
if (acquired) break
|
|
303
|
-
if (await lockIsLive()) return null
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
if (!(await tryAcquireEvictionMarker())) {
|
|
307
|
-
await backoff()
|
|
308
|
-
continue
|
|
309
|
-
}
|
|
310
|
-
} catch (evictError) {
|
|
311
|
-
const code = (evictError as NodeJS.ErrnoException).code
|
|
312
|
-
if (code !== 'EEXIST') throw evictError
|
|
313
|
-
|
|
314
|
-
const recovered = await recoverStaleEvictionMarker()
|
|
315
|
-
if (recovered === 'fresh') return null
|
|
316
|
-
await backoff()
|
|
317
|
-
continue
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
if (await lockIsLive()) return null
|
|
322
|
-
// Fence check 1: verify we still own the marker before acting on the
|
|
323
|
-
// stale-lock-confirmed decision.
|
|
324
|
-
if (!(await ownsEvictionMarker())) return null
|
|
325
|
-
if (options.onStep) await options.onStep('stale-lock-confirmed')
|
|
326
|
-
// Fence check 2: re-verify ownership after the seam (another contender
|
|
327
|
-
// may have renamed our fresh marker while we were paused here).
|
|
328
|
-
if (!(await ownsEvictionMarker())) return null
|
|
329
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => {})
|
|
330
|
-
// Fence check 3: re-verify ownership after removing the stale lock.
|
|
331
|
-
if (!(await ownsEvictionMarker())) return null
|
|
332
|
-
acquired = await tryAcquire()
|
|
333
|
-
if (!acquired) return null
|
|
334
|
-
// Fence check 4: re-verify ownership after acquiring the lock. If the
|
|
335
|
-
// marker was stolen between tryAcquire and this check, release the
|
|
336
|
-
// just-acquired lock and return null (fail-closed).
|
|
337
|
-
if (!(await ownsEvictionMarker())) {
|
|
338
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => {})
|
|
339
|
-
acquired = false
|
|
340
|
-
return null
|
|
341
|
-
}
|
|
342
|
-
break
|
|
343
|
-
} finally {
|
|
344
|
-
await releaseEvictionMarker()
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!acquired) return null
|
|
350
|
-
|
|
351
|
-
scheduleRenewal()
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
release: async () => {
|
|
355
|
-
released = true
|
|
356
|
-
if (renewTimer) {
|
|
357
|
-
clearRefreshLockRenewalTimeout(renewTimer)
|
|
358
|
-
renewTimer = null
|
|
359
|
-
}
|
|
360
|
-
await renewalInFlight
|
|
361
|
-
for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
|
|
362
|
-
try {
|
|
363
|
-
const markerAcquired = await withEvictionMarker(async () => {
|
|
364
|
-
const owner = await readOwner()
|
|
365
|
-
if (owner?.ownerId !== ownerId) return
|
|
366
|
-
if (options.onStep) await options.onStep('release-owner-confirmed')
|
|
367
|
-
if (!(await ownsEvictionMarker())) return
|
|
368
|
-
await rm(lockPath, { recursive: true, force: true }).catch(() => {})
|
|
369
|
-
})
|
|
370
|
-
if (markerAcquired) return
|
|
371
|
-
await recoverStaleEvictionMarker()
|
|
372
|
-
} catch {
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
await backoff()
|
|
376
|
-
}
|
|
377
|
-
// Do not delete by pathname without the marker: bounded retries leave the
|
|
378
|
-
// lease to expire rather than risking removal of a successor's lock.
|
|
379
|
-
},
|
|
380
|
-
}
|
|
381
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
appendFileSync,
|
|
3
|
-
chmodSync,
|
|
4
|
-
existsSync,
|
|
5
|
-
renameSync,
|
|
6
|
-
statSync,
|
|
7
|
-
} from 'node:fs'
|
|
8
|
-
import { tmpdir } from 'node:os'
|
|
9
|
-
import { join } from 'node:path'
|
|
10
|
-
|
|
11
|
-
type Level = 'error' | 'warn' | 'info' | 'debug' | 'trace'
|
|
12
|
-
const ORDER: Record<Level, number> = {
|
|
13
|
-
error: 0,
|
|
14
|
-
warn: 1,
|
|
15
|
-
info: 2,
|
|
16
|
-
debug: 3,
|
|
17
|
-
trace: 4,
|
|
18
|
-
}
|
|
19
|
-
const isTestEnv = process.env.NODE_ENV === 'test'
|
|
20
|
-
const MAX_BYTES = 5 * 1024 * 1024
|
|
21
|
-
|
|
22
|
-
function logFile(): string {
|
|
23
|
-
return (
|
|
24
|
-
process.env.OPENCODE_OPENAI_AUTH_LOG_FILE ??
|
|
25
|
-
join(tmpdir(), 'opencode-openai-auth.log')
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
let runtimeLevel: Level | undefined
|
|
29
|
-
export function setLogLevel(l: Level | undefined) {
|
|
30
|
-
if (l === undefined || l in ORDER) runtimeLevel = l
|
|
31
|
-
}
|
|
32
|
-
function configuredLevel(): Level {
|
|
33
|
-
if (runtimeLevel) return runtimeLevel
|
|
34
|
-
const env = process.env.OPENCODE_OPENAI_AUTH_LOG_LEVEL as Level | undefined
|
|
35
|
-
if (env && env in ORDER) return env
|
|
36
|
-
return 'info'
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const SECRET_KEY_EXACT =
|
|
40
|
-
/^(authorization|x-api-key|cookie|set-cookie|refresh|access|token)$/i
|
|
41
|
-
function isSecretKey(key: string): boolean {
|
|
42
|
-
if (SECRET_KEY_EXACT.test(key)) return true
|
|
43
|
-
const k = key.toLowerCase().replace(/[-_]/g, '')
|
|
44
|
-
// Only the unambiguous ChatGPT stable id (chatgpt-account-id / chatgptAccountId)
|
|
45
|
-
// is sensitive. A bare `accountId` field is overloaded — most diagnostic logs put
|
|
46
|
-
// the INTERNAL account id/key ('main' or a fallback id) there, which is safe and
|
|
47
|
-
// needed for debugging — so it is intentionally NOT redacted by name. The genuine
|
|
48
|
-
// ChatGPT id is kept out of logs at the source instead.
|
|
49
|
-
if (k === 'chatgptaccountid') return true
|
|
50
|
-
if (k.includes('apikey')) return true
|
|
51
|
-
if (k.endsWith('secret') || k.endsWith('password')) return true
|
|
52
|
-
if (k.endsWith('token') && !k.endsWith('tokens')) return true
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
55
|
-
const TOKEN_VALUE = /\b(Bearer\s+[\w.-]+|sk-[\w-]+|eyJ[\w.-]+)\b/g
|
|
56
|
-
export function redact(value: unknown): unknown {
|
|
57
|
-
return redactInner(value, new WeakSet<object>())
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Scrub credential-shaped strings without redacting by key name.
|
|
62
|
-
*
|
|
63
|
-
* For regions where a key names a piece of data rather than holding a secret —
|
|
64
|
-
* a JSON Schema property called `api_key` describes an argument, it does not
|
|
65
|
-
* carry one — replacing the node by name destroys the structure while removing
|
|
66
|
-
* nothing sensitive. A credential pasted into a description is still caught,
|
|
67
|
-
* because that is a string.
|
|
68
|
-
*/
|
|
69
|
-
export function redactStrings(value: unknown): unknown {
|
|
70
|
-
return redactStringsInner(value, new WeakSet<object>())
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function redactStringsInner(value: unknown, seen: WeakSet<object>): unknown {
|
|
74
|
-
if (typeof value === 'string')
|
|
75
|
-
return value.replace(TOKEN_VALUE, '***REDACTED***')
|
|
76
|
-
if (Array.isArray(value)) {
|
|
77
|
-
if (seen.has(value)) return '[Circular]'
|
|
78
|
-
seen.add(value)
|
|
79
|
-
const arr = value.map((v) => redactStringsInner(v, seen))
|
|
80
|
-
seen.delete(value)
|
|
81
|
-
return arr
|
|
82
|
-
}
|
|
83
|
-
if (value && typeof value === 'object') {
|
|
84
|
-
if (seen.has(value)) return '[Circular]'
|
|
85
|
-
seen.add(value)
|
|
86
|
-
const out: Record<string, unknown> = {}
|
|
87
|
-
for (const [k, v] of Object.entries(value))
|
|
88
|
-
out[k] = redactStringsInner(v, seen)
|
|
89
|
-
seen.delete(value)
|
|
90
|
-
return out
|
|
91
|
-
}
|
|
92
|
-
return value
|
|
93
|
-
}
|
|
94
|
-
function redactInner(value: unknown, seen: WeakSet<object>): unknown {
|
|
95
|
-
if (typeof value === 'string')
|
|
96
|
-
return value.replace(TOKEN_VALUE, '***REDACTED***')
|
|
97
|
-
if (Array.isArray(value)) {
|
|
98
|
-
if (seen.has(value)) return '[Circular]'
|
|
99
|
-
seen.add(value)
|
|
100
|
-
const arr = value.map((v) => redactInner(v, seen))
|
|
101
|
-
seen.delete(value)
|
|
102
|
-
return arr
|
|
103
|
-
}
|
|
104
|
-
if (value && typeof value === 'object') {
|
|
105
|
-
if (seen.has(value)) return '[Circular]'
|
|
106
|
-
seen.add(value)
|
|
107
|
-
const out: Record<string, unknown> = {}
|
|
108
|
-
for (const [k, v] of Object.entries(value))
|
|
109
|
-
out[k] = isSecretKey(k) ? '***REDACTED***' : redactInner(v, seen)
|
|
110
|
-
seen.delete(value)
|
|
111
|
-
return out
|
|
112
|
-
}
|
|
113
|
-
return value
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let buffer: string[] = []
|
|
117
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
118
|
-
const ROTATE_KEEP = 3
|
|
119
|
-
function chmodPrivate(path: string) {
|
|
120
|
-
try {
|
|
121
|
-
chmodSync(path, 0o600)
|
|
122
|
-
} catch {
|
|
123
|
-
/* never throw */
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function rotateIfNeeded() {
|
|
127
|
-
try {
|
|
128
|
-
const f = logFile()
|
|
129
|
-
if (!(existsSync(f) && statSync(f).size > MAX_BYTES)) return
|
|
130
|
-
for (let i = ROTATE_KEEP - 1; i >= 1; i--) {
|
|
131
|
-
if (existsSync(`${f}.${i}`)) {
|
|
132
|
-
const rotated = `${f}.${i + 1}`
|
|
133
|
-
renameSync(`${f}.${i}`, rotated)
|
|
134
|
-
chmodPrivate(rotated)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
const rotated = `${f}.1`
|
|
138
|
-
renameSync(f, rotated)
|
|
139
|
-
chmodPrivate(rotated)
|
|
140
|
-
} catch {
|
|
141
|
-
/* never throw */
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function flush() {
|
|
145
|
-
if (!buffer.length) return
|
|
146
|
-
const text = buffer.join('')
|
|
147
|
-
buffer = []
|
|
148
|
-
try {
|
|
149
|
-
rotateIfNeeded()
|
|
150
|
-
const file = logFile()
|
|
151
|
-
if (existsSync(file)) chmodPrivate(file)
|
|
152
|
-
appendFileSync(file, text, { encoding: 'utf8', mode: 0o600 })
|
|
153
|
-
} catch {
|
|
154
|
-
/* never throw */
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
function schedule() {
|
|
158
|
-
if (!timer)
|
|
159
|
-
timer = setTimeout(() => {
|
|
160
|
-
timer = undefined
|
|
161
|
-
flush()
|
|
162
|
-
}, 500)
|
|
163
|
-
}
|
|
164
|
-
if (!isTestEnv) process.on('exit', flush)
|
|
165
|
-
|
|
166
|
-
function safeSerialize(data: unknown): string {
|
|
167
|
-
try {
|
|
168
|
-
return ` ${JSON.stringify(redact(data))}`
|
|
169
|
-
} catch {
|
|
170
|
-
return ' [unserializable]'
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function emit(channel: string, level: Level, message: string, data?: unknown) {
|
|
175
|
-
if (ORDER[level] > ORDER[configuredLevel()]) return
|
|
176
|
-
const line =
|
|
177
|
-
`[${new Date().toISOString()}] ${level.toUpperCase()} [${channel}] ${message}` +
|
|
178
|
-
(data === undefined ? '' : safeSerialize(data)) +
|
|
179
|
-
'\n'
|
|
180
|
-
buffer.push(line)
|
|
181
|
-
if (buffer.length >= 50) flush()
|
|
182
|
-
else schedule()
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function createLogger(channel: string) {
|
|
186
|
-
return {
|
|
187
|
-
error: (m: string, d?: unknown) => emit(channel, 'error', m, d),
|
|
188
|
-
warn: (m: string, d?: unknown) => emit(channel, 'warn', m, d),
|
|
189
|
-
info: (m: string, d?: unknown) => emit(channel, 'info', m, d),
|
|
190
|
-
debug: (m: string, d?: unknown) => emit(channel, 'debug', m, d),
|
|
191
|
-
trace: (m: string, d?: unknown) => emit(channel, 'trace', m, d),
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
export async function flushForTest() {
|
|
195
|
-
flush()
|
|
196
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export type CommandModalName =
|
|
2
|
-
| 'openai-quota'
|
|
3
|
-
| 'openai-account'
|
|
4
|
-
| 'openai-routing'
|
|
5
|
-
| 'openai-killswitch'
|
|
6
|
-
| 'openai-dump'
|
|
7
|
-
| 'openai-logging'
|
|
8
|
-
| 'openai-cachekeep'
|
|
9
|
-
| 'openai-reset'
|
|
10
|
-
|
|
11
|
-
export interface OpenDialogPayload {
|
|
12
|
-
command: CommandModalName
|
|
13
|
-
text: string
|
|
14
|
-
knobs: Record<string, unknown>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface RpcNotification {
|
|
18
|
-
id: number
|
|
19
|
-
type: 'open-dialog'
|
|
20
|
-
payload: OpenDialogPayload
|
|
21
|
-
sessionId?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ApplyRequest {
|
|
25
|
-
command: CommandModalName
|
|
26
|
-
arguments: string
|
|
27
|
-
sessionId?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ApplyResult {
|
|
31
|
-
text: string
|
|
32
|
-
knobs: Record<string, unknown>
|
|
33
|
-
}
|
package/src/util/error.ts
DELETED
package/src/util/open-url.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { execFileSync as defaultExecFileSync } from 'node:child_process'
|
|
2
|
-
|
|
3
|
-
type BrowserExec = (
|
|
4
|
-
file: string,
|
|
5
|
-
args: string[],
|
|
6
|
-
options: { stdio: 'ignore'; timeout: number },
|
|
7
|
-
) => unknown
|
|
8
|
-
|
|
9
|
-
export function openUrl(
|
|
10
|
-
url: string,
|
|
11
|
-
platform: NodeJS.Platform = process.platform,
|
|
12
|
-
execFileSync: BrowserExec = defaultExecFileSync,
|
|
13
|
-
) {
|
|
14
|
-
try {
|
|
15
|
-
if (platform === 'win32') {
|
|
16
|
-
execFileSync('cmd', ['/c', 'start', '', url], {
|
|
17
|
-
stdio: 'ignore',
|
|
18
|
-
timeout: 3000,
|
|
19
|
-
})
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
execFileSync(platform === 'darwin' ? 'open' : 'xdg-open', [url], {
|
|
24
|
-
stdio: 'ignore',
|
|
25
|
-
timeout: 3000,
|
|
26
|
-
})
|
|
27
|
-
} catch {
|
|
28
|
-
// Browser launch is best effort; the printed URL remains the fallback.
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/util/proxy-env.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
function proxyEnv(name: string) {
|
|
2
|
-
return process.env[name] ?? process.env[name.toLowerCase()]
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function shouldBypassProxy(url: URL): boolean {
|
|
6
|
-
const noProxy = proxyEnv('NO_PROXY')
|
|
7
|
-
if (!noProxy) return false
|
|
8
|
-
|
|
9
|
-
const host = url.hostname.toLowerCase()
|
|
10
|
-
const entries = noProxy
|
|
11
|
-
.split(',')
|
|
12
|
-
.map((s) => s.trim().toLowerCase())
|
|
13
|
-
.filter(Boolean)
|
|
14
|
-
|
|
15
|
-
for (const entry of entries) {
|
|
16
|
-
if (entry === '*') return true
|
|
17
|
-
|
|
18
|
-
if (entry.startsWith('.')) {
|
|
19
|
-
if (host === entry.slice(1) || host.endsWith(entry)) {
|
|
20
|
-
return true
|
|
21
|
-
}
|
|
22
|
-
} else {
|
|
23
|
-
if (host === entry || host.endsWith(`.${entry}`)) {
|
|
24
|
-
return true
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return false
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function getProxyForUrl(url: string) {
|
|
33
|
-
const parsed = new URL(url)
|
|
34
|
-
if (shouldBypassProxy(parsed)) return undefined
|
|
35
|
-
|
|
36
|
-
if (parsed.protocol === 'http:') return proxyEnv('HTTP_PROXY')
|
|
37
|
-
if (parsed.protocol === 'https:')
|
|
38
|
-
return proxyEnv('HTTPS_PROXY') ?? proxyEnv('HTTP_PROXY')
|
|
39
|
-
return undefined
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const ProxyEnv = { getProxyForUrl }
|
package/src/util/record.ts
DELETED