threadwire 0.1.6 → 0.1.9

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.
@@ -0,0 +1,267 @@
1
+ // @ts-check
2
+
3
+ import {randomBytes} from "node:crypto"
4
+ import {constants} from "node:fs"
5
+ import {chmod, lstat, open, rename, unlink} from "node:fs/promises"
6
+ import {dirname, isAbsolute, join} from "node:path"
7
+
8
+ const MAX_CREDENTIAL_BYTES = 65_536
9
+ const MAX_TOKEN_BYTES = 8_192
10
+ const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098"
11
+ const TOKEN_URL = "https://auth.kimi.com/api/oauth/token"
12
+ const EXACT_KEYS = ["access_token", "expires_at", "expires_in", "refresh_token", "scope", "token_type"]
13
+
14
+ /** @typedef {{access_token: string, expires_at: number, expires_in: number, refresh_token: string, scope: string, token_type: "Bearer"}} KimiCredential */
15
+
16
+ /**
17
+ * @param {{
18
+ * file: string,
19
+ * expectedUid?: number,
20
+ * now?: () => number,
21
+ * fetchImplementation?: typeof fetch,
22
+ * maxRefreshAttempts?: number,
23
+ * sleep?: (milliseconds: number) => Promise<void>,
24
+ * durabilityObserver?: (phase: string) => void | Promise<void>
25
+ * }} options
26
+ */
27
+ export async function openKimiOAuthStore(options) {
28
+ if (!isAbsolute(options.file) || options.file.includes("\0")) throw unavailable()
29
+ const expectedUid = options.expectedUid ?? process.getuid?.() ?? -1
30
+ if (!Number.isSafeInteger(expectedUid) || expectedUid < 0) throw unavailable()
31
+ const now = options.now ?? (() => Math.floor(Date.now() / 1000))
32
+ const fetchImplementation = options.fetchImplementation ?? fetch
33
+ const maxRefreshAttempts = options.maxRefreshAttempts ?? 3
34
+ if (!Number.isSafeInteger(maxRefreshAttempts) || maxRefreshAttempts < 1 || maxRefreshAttempts > 3) throw unavailable()
35
+ const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve) => {
36
+ const timer = setTimeout(resolve, milliseconds)
37
+ timer.unref()
38
+ }))
39
+ let credential = await readCredential(options.file, expectedUid)
40
+ /** @type {Promise<string> | undefined} */
41
+ let refreshInFlight
42
+
43
+ /** @param {AbortSignal | undefined} signal */
44
+ const refresh = async (signal) => {
45
+ if (credential.access_token.length === 0 || credential.refresh_token.length === 0) throw needsLogin()
46
+ /** @type {unknown} */
47
+ let lastFailure
48
+ for (let attempt = 0; attempt < maxRefreshAttempts; attempt++) {
49
+ if (signal?.aborted) throw signal.reason
50
+ let response
51
+ try {
52
+ const signals = [AbortSignal.timeout(30_000)]
53
+ if (signal !== undefined) signals.push(signal)
54
+ response = await fetchImplementation(TOKEN_URL, {
55
+ method: "POST",
56
+ redirect: "error",
57
+ headers: {accept: "application/json", "content-type": "application/x-www-form-urlencoded"},
58
+ body: new URLSearchParams({
59
+ client_id: CLIENT_ID,
60
+ grant_type: "refresh_token",
61
+ refresh_token: credential.refresh_token
62
+ }).toString(),
63
+ signal: AbortSignal.any(signals)
64
+ })
65
+ } catch (error) {
66
+ if (signal?.aborted) throw signal.reason
67
+ lastFailure = error
68
+ if (attempt + 1 < maxRefreshAttempts) { await sleep(2 ** attempt * 100); continue }
69
+ throw refreshUnavailable(lastFailure)
70
+ }
71
+ const bytes = await readCappedResponse(response, signal).catch((error) => {
72
+ lastFailure = error
73
+ return undefined
74
+ })
75
+ if (bytes === undefined) {
76
+ if (attempt + 1 < maxRefreshAttempts) { await sleep(2 ** attempt * 100); continue }
77
+ throw refreshUnavailable(lastFailure)
78
+ }
79
+ /** @type {Record<string, unknown>} */
80
+ let body = {}
81
+ try {
82
+ const parsed = JSON.parse(bytes.toString("utf8"))
83
+ if (record(parsed)) body = parsed
84
+ } catch { /* Invalid response bodies remain untrusted. */ }
85
+ if (response.status === 401 || response.status === 403 || body.error === "invalid_grant") {
86
+ credential = {
87
+ access_token: "", refresh_token: "", expires_at: 0,
88
+ scope: credential.scope, token_type: credential.token_type, expires_in: 0
89
+ }
90
+ await persistCredential(options.file, expectedUid, credential, options.durabilityObserver)
91
+ throw needsLogin()
92
+ }
93
+ if (response.ok) {
94
+ const replacement = tokenResponse(body, credential, now())
95
+ await persistCredential(options.file, expectedUid, replacement, options.durabilityObserver)
96
+ credential = replacement
97
+ return credential.access_token
98
+ }
99
+ if (![429, 500, 502, 503, 504].includes(response.status) || attempt + 1 >= maxRefreshAttempts) {
100
+ throw refreshUnavailable()
101
+ }
102
+ await sleep(2 ** attempt * 100)
103
+ }
104
+ throw refreshUnavailable(lastFailure)
105
+ }
106
+
107
+ return {
108
+ /** @param {{signal?: AbortSignal, force?: boolean}} [request] */
109
+ async getAccessToken(request = {}) {
110
+ if (credential.access_token.length === 0) throw needsLogin()
111
+ const threshold = Math.max(300, credential.expires_in * 0.5)
112
+ if (request.force !== true && credential.expires_at - now() > threshold) return credential.access_token
113
+ if (refreshInFlight !== undefined) return refreshInFlight
114
+ const operation = refresh(request.signal).finally(() => {
115
+ if (refreshInFlight === operation) refreshInFlight = undefined
116
+ })
117
+ refreshInFlight = operation
118
+ return operation
119
+ },
120
+ async health() {
121
+ return credential.access_token.length === 0
122
+ ? {ready: false, needsLogin: true}
123
+ : {ready: true, needsLogin: false}
124
+ }
125
+ }
126
+ }
127
+
128
+ /** @param {string} file @param {number} expectedUid @returns {Promise<KimiCredential>} */
129
+ async function readCredential(file, expectedUid) {
130
+ const directory = dirname(file)
131
+ try {
132
+ const parent = await lstat(directory, {bigint: true})
133
+ if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== BigInt(expectedUid)
134
+ || (parent.mode & 0o077n) !== 0n) throw unavailable()
135
+ let handle
136
+ try {
137
+ handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)
138
+ const metadata = await handle.stat({bigint: true})
139
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n
140
+ || metadata.uid !== BigInt(expectedUid) || (metadata.mode & 0o777n) !== 0o600n
141
+ || metadata.size < 1n || metadata.size > BigInt(MAX_CREDENTIAL_BYTES)) throw unavailable()
142
+ const bytes = await handle.readFile()
143
+ return parseCredential(bytes)
144
+ } finally { await handle?.close().catch(() => {}) }
145
+ } catch (error) {
146
+ if (error instanceof Error && error.message === unavailable().message) throw error
147
+ throw unavailable()
148
+ }
149
+ }
150
+
151
+ /** @param {Buffer} bytes @returns {KimiCredential} */
152
+ function parseCredential(bytes) {
153
+ /** @type {unknown} */
154
+ let value
155
+ try { value = JSON.parse(bytes.toString("utf8")) } catch { throw unavailable() }
156
+ if (!record(value) || !exactKeys(value, EXACT_KEYS)
157
+ || !safeToken(value.access_token, true) || !safeToken(value.refresh_token, true)
158
+ || typeof value.expires_at !== "number" || !Number.isSafeInteger(value.expires_at) || value.expires_at < 0
159
+ || typeof value.expires_in !== "number" || !Number.isSafeInteger(value.expires_in) || value.expires_in < 0
160
+ || typeof value.scope !== "string" || value.scope.length > 2048 || /[\0\r\n]/u.test(value.scope)
161
+ || value.token_type !== "Bearer") throw unavailable()
162
+ const tombstone = value.access_token === "" && value.refresh_token === ""
163
+ && value.expires_at === 0 && value.expires_in === 0
164
+ if (!tombstone && (value.access_token === "" || value.refresh_token === ""
165
+ || value.expires_at < 1 || value.expires_in < 1)) throw unavailable()
166
+ return /** @type {KimiCredential} */ (value)
167
+ }
168
+
169
+ /** @param {unknown} value @param {KimiCredential} previous @param {number} currentTime @returns {KimiCredential} */
170
+ function tokenResponse(value, previous, currentTime) {
171
+ if (!record(value) || !safeToken(value.access_token, false) || !safeToken(value.refresh_token, false)
172
+ || !Number.isFinite(Number(value.expires_in)) || Number(value.expires_in) <= 0) throw refreshUnavailable()
173
+ const expiresIn = Number(value.expires_in)
174
+ if (!Number.isSafeInteger(expiresIn) || expiresIn > 604_800) throw refreshUnavailable()
175
+ const scope = value.scope === undefined ? previous.scope : value.scope
176
+ const tokenType = value.token_type === undefined ? "Bearer" : value.token_type
177
+ if (typeof scope !== "string" || scope.length > 2048 || /[\0\r\n]/u.test(scope) || tokenType !== "Bearer") {
178
+ throw refreshUnavailable()
179
+ }
180
+ return {
181
+ access_token: value.access_token,
182
+ refresh_token: value.refresh_token,
183
+ expires_at: currentTime + expiresIn,
184
+ scope,
185
+ token_type: "Bearer",
186
+ expires_in: expiresIn
187
+ }
188
+ }
189
+
190
+ /** @param {string} file @param {number} expectedUid @param {KimiCredential} credential @param {((phase: string) => void | Promise<void>) | undefined} observer */
191
+ async function persistCredential(file, expectedUid, credential, observer) {
192
+ const directory = dirname(file)
193
+ const temporary = join(directory, `.kimi-code.json.tmp.${randomBytes(12).toString("hex")}`)
194
+ let handle
195
+ try {
196
+ handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600)
197
+ await handle.writeFile(`${JSON.stringify(credential)}\n`)
198
+ await observer?.("before-file-sync")
199
+ await handle.sync()
200
+ await observer?.("after-file-sync")
201
+ await handle.close()
202
+ handle = undefined
203
+ await chmod(temporary, 0o600)
204
+ const metadata = await lstat(temporary, {bigint: true})
205
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n
206
+ || metadata.uid !== BigInt(expectedUid) || (metadata.mode & 0o777n) !== 0o600n) throw unavailable()
207
+ await observer?.("before-rename")
208
+ await rename(temporary, file)
209
+ await observer?.("after-rename")
210
+ const parent = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY)
211
+ try {
212
+ await observer?.("before-directory-sync")
213
+ await parent.sync()
214
+ await observer?.("after-directory-sync")
215
+ } finally { await parent.close() }
216
+ } catch (error) {
217
+ await handle?.close().catch(() => {})
218
+ await unlink(temporary).catch(() => {})
219
+ if (error instanceof Error && [needsLogin().message, refreshUnavailable().message].includes(error.message)) throw error
220
+ // Persistence errors may contain credential material or private paths; do not retain them as a cause.
221
+ // eslint-disable-next-line preserve-caught-error
222
+ throw new Error("Kimi OAuth credential persistence failed")
223
+ }
224
+ }
225
+
226
+ /** @param {Response} response @param {AbortSignal | undefined} signal @returns {Promise<Buffer>} */
227
+ async function readCappedResponse(response, signal) {
228
+ if (response.body === null) return Buffer.alloc(0)
229
+ const reader = response.body.getReader()
230
+ /** @type {Buffer[]} */
231
+ const chunks = []
232
+ let size = 0
233
+ try {
234
+ while (true) {
235
+ if (signal?.aborted) throw signal.reason
236
+ const {done, value} = await reader.read()
237
+ if (done) break
238
+ size += value.byteLength
239
+ if (size > MAX_CREDENTIAL_BYTES) throw refreshUnavailable()
240
+ chunks.push(Buffer.from(value))
241
+ }
242
+ } catch (error) {
243
+ await reader.cancel().catch(() => {})
244
+ throw error
245
+ } finally { reader.releaseLock() }
246
+ return Buffer.concat(chunks)
247
+ }
248
+
249
+ /** @param {unknown} value @param {boolean} allowEmpty @returns {value is string} */
250
+ function safeToken(value, allowEmpty) {
251
+ return typeof value === "string" && (allowEmpty || value.length > 0)
252
+ && value.length <= MAX_TOKEN_BYTES && !/[\0\r\n]/u.test(value)
253
+ }
254
+ /** @param {Record<string, unknown>} value @param {readonly string[]} expected @returns {boolean} */
255
+ function exactKeys(value, expected) {
256
+ const actual = Object.keys(value).sort()
257
+ const wanted = [...expected].sort()
258
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index])
259
+ }
260
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
261
+ function record(value) { return typeof value === "object" && value !== null && !Array.isArray(value) }
262
+ /** @returns {Error} */
263
+ function unavailable() { return new Error("Kimi OAuth credential file is unavailable") }
264
+ /** @returns {Error} */
265
+ function needsLogin() { return new Error("Kimi OAuth credential was rejected; re-login required") }
266
+ /** @param {unknown} [cause] @returns {Error} */
267
+ function refreshUnavailable(cause) { return new Error("Kimi OAuth refresh is unavailable", cause === undefined ? undefined : {cause}) }
@@ -0,0 +1,139 @@
1
+ // @ts-check
2
+
3
+ import {randomBytes} from "node:crypto"
4
+
5
+ const TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,128}$/u
6
+ const NETWORK_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u
7
+ const RUN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/u
8
+ const ALLOWED_HEADERS = new Set([
9
+ "authorization", "content-length", "content-type", "user-agent", "accept",
10
+ "accept-encoding", "accept-language", "sec-fetch-mode",
11
+ "x-stainless-arch", "x-stainless-lang", "x-stainless-os",
12
+ "x-stainless-package-version", "x-stainless-runtime",
13
+ "x-stainless-runtime-version", "x-stainless-retry-count", "x-stainless-timeout",
14
+ "x-stainless-async", "originator", "conversation_id", "session_id",
15
+ "version", "x-codex-turn-metadata"
16
+ ])
17
+
18
+ /** @param {{now?: () => number, random?: (bytes: number) => Buffer}} [options] */
19
+ export function createGrantStore(options = {}) {
20
+ const now = options.now ?? Date.now
21
+ const random = options.random ?? randomBytes
22
+ /** @type {Map<string, {runId: string, provider: "codex", networkId: string, model: string, expiresAt: number, active: boolean}>} */
23
+ const grants = new Map()
24
+ return {
25
+ /** @param {{runId: string, provider: "codex", networkId: string, model: string, ttlMs: number}} grant */
26
+ issue(grant) {
27
+ if (!RUN_PATTERN.test(grant.runId) || !NETWORK_PATTERN.test(grant.networkId)) throw new Error("Invalid broker grant")
28
+ if (grant.provider !== "codex") throw new Error("Invalid broker grant")
29
+ if (typeof grant.model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(grant.model)) throw new Error("Invalid broker grant")
30
+ if (!Number.isSafeInteger(grant.ttlMs) || grant.ttlMs < 1 || grant.ttlMs > 3_600_000) throw new Error("Invalid broker grant")
31
+ const token = random(32).toString("base64url")
32
+ grants.set(token, {...grant, expiresAt: now() + grant.ttlMs, active: false})
33
+ return token
34
+ },
35
+ /** @param {string} token */
36
+ activate(token) {
37
+ const grant = grants.get(token)
38
+ if (!grant) throw new Error("Broker grant unknown token")
39
+ if (grant.expiresAt < now()) {
40
+ grants.delete(token)
41
+ throw new Error("Broker grant expired")
42
+ }
43
+ if (grant.active) throw new Error("Broker grant already active")
44
+ grant.active = true
45
+ return {...grant}
46
+ },
47
+ /** @param {string} token @param {{provider: string, networkId: string, runId?: string}} context */
48
+ authorize(token, context) {
49
+ const grant = grants.get(token)
50
+ if (!grant) throw new Error("Broker grant unknown token")
51
+ if (grant.provider !== context.provider || grant.networkId !== context.networkId || (context.runId !== undefined && grant.runId !== context.runId)) throw new Error("Broker grant lineage denied")
52
+ if (!grant.active) throw new Error("Broker grant pending")
53
+ if (grant.expiresAt < now()) {
54
+ grants.delete(token)
55
+ throw new Error("Broker grant expired")
56
+ }
57
+ return {...grant}
58
+ },
59
+ /** @param {string} token */
60
+ revoke(token) {
61
+ grants.delete(token)
62
+ },
63
+ clear() {
64
+ grants.clear()
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * @param {{method?: string, path?: string, headers?: Record<string, string | string[] | undefined>}} request
71
+ */
72
+ export function validateBrokerRequest(request) {
73
+ if (request.method !== "POST" || request.path !== "/v1/responses") throw new Error("Broker request denied")
74
+ for (const header of Object.keys(request.headers ?? {})) {
75
+ if (!ALLOWED_HEADERS.has(header.toLowerCase())) throw new Error(`Broker request denied header: ${header.toLowerCase()}`)
76
+ }
77
+ return {method: "POST", path: "/v1/responses"}
78
+ }
79
+
80
+ const RESPONSE_KEYS = new Set([
81
+ "model", "input", "instructions", "stream", "tools", "tool_choice",
82
+ "parallel_tool_calls", "previous_response_id", "reasoning", "store",
83
+ "include", "metadata", "max_output_tokens", "temperature", "top_p", "truncation",
84
+ "prompt_cache_key"
85
+ ])
86
+
87
+ /** @param {Buffer} body @param {string} allowedModel */
88
+ export function validateResponsesBody(body, allowedModel) {
89
+ const text = body.toString("utf8")
90
+ if (topLevelKeyCount(text, "model") !== 1) throw new Error("Broker request denied")
91
+ let value
92
+ try {
93
+ value = JSON.parse(text)
94
+ } catch {
95
+ throw new Error("Broker request denied")
96
+ }
97
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Broker request denied")
98
+ const unexpected = Object.keys(value).find((key) => !RESPONSE_KEYS.has(key))
99
+ if (unexpected !== undefined) throw new Error(`Broker request denied field: ${unexpected}`)
100
+ if (value.model !== allowedModel || !("input" in value)) throw new Error("Broker request denied")
101
+ return value
102
+ }
103
+
104
+ /** @param {string} text @param {string} wanted */
105
+ function topLevelKeyCount(text, wanted) {
106
+ let depth = 0
107
+ let count = 0
108
+ let index = 0
109
+ while (index < text.length) {
110
+ const character = text[index]
111
+ if (character === "\"") {
112
+ let string = ""
113
+ index += 1
114
+ while (index < text.length && text[index] !== "\"") {
115
+ if (text[index] === "\\") {
116
+ index += 2
117
+ continue
118
+ }
119
+ string += text[index]
120
+ index += 1
121
+ }
122
+ if (depth === 1) {
123
+ let next = index + 1
124
+ while (/\s/u.test(text[next] ?? "")) next += 1
125
+ if (text[next] === ":" && string === wanted) count += 1
126
+ }
127
+ } else if (character === "{") depth += 1
128
+ else if (character === "}") depth -= 1
129
+ index += 1
130
+ }
131
+ return count
132
+ }
133
+
134
+ /** @param {string | undefined} authorization */
135
+ export function bearerToken(authorization) {
136
+ const match = /^Bearer ([A-Za-z0-9_-]{32,128})$/u.exec(authorization ?? "")
137
+ if (!match || !TOKEN_PATTERN.test(match[1] ?? "")) throw new Error("Broker grant format denied")
138
+ return /** @type {string} */ (match[1])
139
+ }