@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,161 @@
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
+ function redactInner(value: unknown, seen: WeakSet<object>): unknown {
60
+ if (typeof value === 'string')
61
+ return value.replace(TOKEN_VALUE, '***REDACTED***')
62
+ if (Array.isArray(value)) {
63
+ if (seen.has(value)) return '[Circular]'
64
+ seen.add(value)
65
+ const arr = value.map((v) => redactInner(v, seen))
66
+ seen.delete(value)
67
+ return arr
68
+ }
69
+ if (value && typeof value === 'object') {
70
+ if (seen.has(value)) return '[Circular]'
71
+ seen.add(value)
72
+ const out: Record<string, unknown> = {}
73
+ for (const [k, v] of Object.entries(value))
74
+ out[k] = isSecretKey(k) ? '***REDACTED***' : redactInner(v, seen)
75
+ seen.delete(value)
76
+ return out
77
+ }
78
+ return value
79
+ }
80
+
81
+ let buffer: string[] = []
82
+ let timer: ReturnType<typeof setTimeout> | undefined
83
+ const ROTATE_KEEP = 3
84
+ function chmodPrivate(path: string) {
85
+ try {
86
+ chmodSync(path, 0o600)
87
+ } catch {
88
+ /* never throw */
89
+ }
90
+ }
91
+ function rotateIfNeeded() {
92
+ try {
93
+ const f = logFile()
94
+ if (!(existsSync(f) && statSync(f).size > MAX_BYTES)) return
95
+ for (let i = ROTATE_KEEP - 1; i >= 1; i--) {
96
+ if (existsSync(`${f}.${i}`)) {
97
+ const rotated = `${f}.${i + 1}`
98
+ renameSync(`${f}.${i}`, rotated)
99
+ chmodPrivate(rotated)
100
+ }
101
+ }
102
+ const rotated = `${f}.1`
103
+ renameSync(f, rotated)
104
+ chmodPrivate(rotated)
105
+ } catch {
106
+ /* never throw */
107
+ }
108
+ }
109
+ function flush() {
110
+ if (!buffer.length) return
111
+ const text = buffer.join('')
112
+ buffer = []
113
+ try {
114
+ rotateIfNeeded()
115
+ const file = logFile()
116
+ if (existsSync(file)) chmodPrivate(file)
117
+ appendFileSync(file, text, { encoding: 'utf8', mode: 0o600 })
118
+ } catch {
119
+ /* never throw */
120
+ }
121
+ }
122
+ function schedule() {
123
+ if (!timer)
124
+ timer = setTimeout(() => {
125
+ timer = undefined
126
+ flush()
127
+ }, 500)
128
+ }
129
+ if (!isTestEnv) process.on('exit', flush)
130
+
131
+ function safeSerialize(data: unknown): string {
132
+ try {
133
+ return ` ${JSON.stringify(redact(data))}`
134
+ } catch {
135
+ return ' [unserializable]'
136
+ }
137
+ }
138
+
139
+ function emit(channel: string, level: Level, message: string, data?: unknown) {
140
+ if (ORDER[level] > ORDER[configuredLevel()]) return
141
+ const line =
142
+ `[${new Date().toISOString()}] ${level.toUpperCase()} [${channel}] ${message}` +
143
+ (data === undefined ? '' : safeSerialize(data)) +
144
+ '\n'
145
+ buffer.push(line)
146
+ if (buffer.length >= 50) flush()
147
+ else schedule()
148
+ }
149
+
150
+ export function createLogger(channel: string) {
151
+ return {
152
+ error: (m: string, d?: unknown) => emit(channel, 'error', m, d),
153
+ warn: (m: string, d?: unknown) => emit(channel, 'warn', m, d),
154
+ info: (m: string, d?: unknown) => emit(channel, 'info', m, d),
155
+ debug: (m: string, d?: unknown) => emit(channel, 'debug', m, d),
156
+ trace: (m: string, d?: unknown) => emit(channel, 'trace', m, d),
157
+ }
158
+ }
159
+ export async function flushForTest() {
160
+ flush()
161
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ mkdir,
3
+ readdir,
4
+ readFile,
5
+ rename,
6
+ unlink,
7
+ writeFile,
8
+ } from 'node:fs/promises'
9
+ import { join } from 'node:path'
10
+
11
+ export interface PortFileEntry {
12
+ port: number
13
+ token: string
14
+ pid: number
15
+ startedAt: number
16
+ }
17
+
18
+ function pidAlive(pid: number): boolean {
19
+ try {
20
+ process.kill(pid, 0)
21
+ return true
22
+ } catch (error) {
23
+ return (error as NodeJS.ErrnoException).code === 'EPERM'
24
+ }
25
+ }
26
+
27
+ export async function writePortFile(
28
+ dir: string,
29
+ entry: { port: number; token: string; pid: number },
30
+ ): Promise<string> {
31
+ await mkdir(dir, { recursive: true })
32
+ const full: PortFileEntry = { ...entry, startedAt: Date.now() }
33
+ const target = join(dir, `port-${entry.pid}.json`)
34
+ const tmp = `${target}.${process.pid}.tmp`
35
+ await writeFile(tmp, JSON.stringify(full), { encoding: 'utf8', mode: 0o600 })
36
+ await rename(tmp, target)
37
+ return target
38
+ }
39
+
40
+ export async function discoverPortFile(
41
+ dir: string,
42
+ expectedPid?: number,
43
+ ): Promise<PortFileEntry | null> {
44
+ let names: string[]
45
+ try {
46
+ names = await readdir(dir)
47
+ } catch {
48
+ return null
49
+ }
50
+ const live: PortFileEntry[] = []
51
+ for (const name of names) {
52
+ if (!name.startsWith('port-') || !name.endsWith('.json')) continue
53
+ try {
54
+ const parsed = JSON.parse(
55
+ await readFile(join(dir, name), 'utf8'),
56
+ ) as PortFileEntry
57
+ if (Number.isFinite(parsed.port)) {
58
+ if (pidAlive(parsed.pid)) live.push(parsed)
59
+ else unlink(join(dir, name)).catch(() => {})
60
+ }
61
+ } catch {}
62
+ }
63
+ if (live.length === 0) return null
64
+ const candidates =
65
+ expectedPid !== undefined && expectedPid >= 1
66
+ ? live.filter((entry) => entry.pid === expectedPid)
67
+ : []
68
+ const entries = candidates.length > 0 ? candidates : live
69
+ return entries.sort((a, b) => b.startedAt - a.startedAt)[0] ?? null
70
+ }
@@ -0,0 +1,31 @@
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
+
10
+ export interface OpenDialogPayload {
11
+ command: CommandModalName
12
+ text: string
13
+ knobs: Record<string, unknown>
14
+ }
15
+
16
+ export interface RpcNotification {
17
+ id: number
18
+ type: 'open-dialog'
19
+ payload: OpenDialogPayload
20
+ sessionId?: string
21
+ }
22
+
23
+ export interface ApplyRequest {
24
+ command: CommandModalName
25
+ arguments: string
26
+ }
27
+
28
+ export interface ApplyResult {
29
+ text: string
30
+ knobs: Record<string, unknown>
31
+ }
@@ -0,0 +1,78 @@
1
+ import { discoverPortFile, type PortFileEntry } from './port-file'
2
+ import type { ApplyRequest, ApplyResult, RpcNotification } from './protocol'
3
+
4
+ export interface RpcClient {
5
+ pending: (
6
+ lastReceivedId: number,
7
+ sessionId?: string,
8
+ ) => Promise<RpcNotification[]>
9
+ apply: (request: ApplyRequest) => Promise<ApplyResult>
10
+ }
11
+
12
+ async function call<T>(
13
+ dir: string,
14
+ expectedPid: number | undefined,
15
+ onSelected: ((entry: PortFileEntry | null) => void) | undefined,
16
+ method: string,
17
+ params: Record<string, unknown>,
18
+ ): Promise<T | null> {
19
+ const entry = await discoverPortFile(dir, expectedPid)
20
+ onSelected?.(entry)
21
+ if (!entry) return null
22
+ const controller = new AbortController()
23
+ const timer = setTimeout(() => controller.abort(), 2_000)
24
+ try {
25
+ const res = await fetch(`http://127.0.0.1:${entry.port}/rpc/${method}`, {
26
+ method: 'POST',
27
+ headers: {
28
+ 'content-type': 'application/json',
29
+ authorization: `Bearer ${entry.token}`,
30
+ },
31
+ body: JSON.stringify(params),
32
+ signal: controller.signal,
33
+ })
34
+ if (!res.ok) return null
35
+ return (await res.json()) as T
36
+ } catch {
37
+ return null
38
+ } finally {
39
+ clearTimeout(timer)
40
+ }
41
+ }
42
+
43
+ export function createRpcClient(
44
+ dir: string,
45
+ expectedPid?: number,
46
+ onSelected?: (entry: PortFileEntry | null) => void,
47
+ ): RpcClient {
48
+ let reportedSelection = false
49
+ const reportSelected = (entry: PortFileEntry | null) => {
50
+ if (reportedSelection) return
51
+ reportedSelection = true
52
+ onSelected?.(entry)
53
+ }
54
+ return {
55
+ async pending(lastReceivedId, sessionId) {
56
+ const out = await call<{ messages: RpcNotification[] }>(
57
+ dir,
58
+ expectedPid,
59
+ reportSelected,
60
+ 'pending-notifications',
61
+ { lastReceivedId, sessionId },
62
+ )
63
+ return out?.messages ?? []
64
+ },
65
+ async apply(request) {
66
+ const out = await call<ApplyResult>(
67
+ dir,
68
+ expectedPid,
69
+ reportSelected,
70
+ 'apply',
71
+ {
72
+ ...request,
73
+ },
74
+ )
75
+ return out ?? { text: 'apply failed', knobs: {} }
76
+ },
77
+ }
78
+ }
@@ -0,0 +1,22 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { homedir, tmpdir } from 'node:os'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ const RPC_DIR_ENV = 'OPENCODE_OPENAI_AUTH_RPC_DIR'
6
+
7
+ // Both processes must resolve the SAME dir from the SAME project directory.
8
+ export function getRpcDir(projectDirectory: string): string {
9
+ const override = process.env[RPC_DIR_ENV]?.trim()
10
+ // A relative override is anchored to projectDirectory (shared by both processes)
11
+ // so server and TUI halves always resolve the same dir. An absolute override is
12
+ // used as-is (resolve(base, absolute) returns the absolute path unchanged).
13
+ if (override) return resolve(projectDirectory, override)
14
+ const hash = createHash('sha256')
15
+ .update(projectDirectory)
16
+ .digest('hex')
17
+ .slice(0, 16)
18
+ const base = process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state')
19
+ return join(base, 'cortexkit', 'openai-auth', 'rpc', hash)
20
+ }
21
+
22
+ export { tmpdir }