@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.
- package/README.md +1 -0
- package/dist/cli.js +1 -1
- package/dist/codex-http.d.ts +3 -0
- package/dist/config.d.ts +3 -0
- package/dist/core/accounts.d.ts +9 -3
- package/dist/core/oauth.d.ts +1 -1
- package/dist/core/quota-manager.d.ts +2 -2
- package/dist/{index-k5zwf3j1.js → index-1anzb8jh.js} +1 -1
- package/dist/index-1r02s323.js +6 -0
- package/dist/index-2ng03cby.js +2 -0
- package/dist/{index-e7m7qbne.js → index-6y6ss1kc.js} +1 -1
- package/dist/{index-1xmasv0x.js → index-b6kp9ydc.js} +1 -1
- package/dist/index-cz1bkmjz.js +2 -0
- package/dist/index-dzg88nw2.js +89 -0
- package/dist/index-y67qn95a.js +2 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +65 -65
- package/dist/model-costs.d.ts +29 -0
- package/dist/prompt-context.d.ts +1 -1
- package/dist/quota-normalize-8yzxqy78.js +1 -0
- package/dist/quota-normalize.d.ts +13 -4
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.d.ts +47 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/token-util-h46jb3eg.js +1 -0
- package/dist/token-yzcb1p3e.js +1 -0
- package/dist/tui/command-dialogs.d.ts +8 -1
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +18 -1
- package/dist/version.d.ts +1 -1
- package/dist/ws-pool.d.ts +1 -0
- package/package.json +11 -8
- package/src/core/refresh-file-lock.ts +293 -0
- package/src/sidebar-state.ts +392 -13
- package/src/tui/command-dialogs.tsx +43 -25
- package/src/tui/entry.mjs +37 -0
- package/src/tui-compiled/logger.ts +161 -0
- package/src/tui-compiled/rpc/port-file.ts +70 -0
- package/src/tui-compiled/rpc/protocol.ts +31 -0
- package/src/tui-compiled/rpc/rpc-client.ts +78 -0
- package/src/tui-compiled/rpc/rpc-dir.ts +22 -0
- package/src/tui-compiled/sidebar-state.ts +695 -0
- package/src/tui-compiled/tui/command-dialogs.tsx +645 -0
- package/src/tui-compiled/tui-preferences.ts +328 -0
- package/src/tui-compiled/tui.tsx +1098 -0
- package/src/tui-compiled/util/open-url.ts +30 -0
- package/src/tui.tsx +158 -77
- package/dist/index-5s67238f.js +0 -2
- package/dist/index-d9hq0tmc.js +0 -2
- package/dist/index-mrg2fms9.js +0 -2
- package/dist/index-phrx93zy.js +0 -92
- package/dist/index-qdh7pt5a.js +0 -3
- package/dist/quota-normalize-dfdpwwm3.js +0 -1
- package/dist/token-01dcf92a.js +0 -1
- 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 }
|