dsh-codex-community 0.0.1

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 (53) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/CONTRIBUTING.en.md +54 -0
  3. package/CONTRIBUTING.md +54 -0
  4. package/LICENSE +201 -0
  5. package/NOTICE +12 -0
  6. package/README.en.md +98 -0
  7. package/README.md +98 -0
  8. package/SECURITY.md +41 -0
  9. package/SUPPORT.md +17 -0
  10. package/THIRD_PARTY_NOTICES.md +25 -0
  11. package/codex-community.patch.yml +13 -0
  12. package/dist/client/index.js +1037 -0
  13. package/dist/host/index.mjs +98 -0
  14. package/dist/internal/authorization-bridge.mjs +662 -0
  15. package/dist/internal/authorization-commit-tracker.mjs +49 -0
  16. package/dist/internal/codex-authorization.mjs +202 -0
  17. package/dist/internal/codex-credential-store.mjs +164 -0
  18. package/dist/internal/codex-identifiers.mjs +4 -0
  19. package/dist/internal/codex-pi-provider.mjs +137 -0
  20. package/dist/internal/codex-provider-runtime.mjs +256 -0
  21. package/dist/internal/codex-route-adapter.mjs +133 -0
  22. package/dist/internal/codex-session-resources.mjs +64 -0
  23. package/dist/internal/failure-normalizer.mjs +456 -0
  24. package/dist/internal/image-policy.mjs +45 -0
  25. package/dist/internal/quota-observer.mjs +142 -0
  26. package/dist/internal/reliability.mjs +12 -0
  27. package/dist/internal/remote-image-input.mjs +801 -0
  28. package/dist/internal/session-preference-command.mjs +52 -0
  29. package/dist/internal/session-preferences.mjs +93 -0
  30. package/dist/internal/stream-resilience.mjs +273 -0
  31. package/docs/README.en.md +15 -0
  32. package/docs/README.md +15 -0
  33. package/docs/architecture.en.md +106 -0
  34. package/docs/architecture.md +106 -0
  35. package/docs/compatibility.en.md +53 -0
  36. package/docs/compatibility.md +53 -0
  37. package/docs/configuration.en.md +61 -0
  38. package/docs/configuration.md +61 -0
  39. package/docs/contribution-sources.en.md +35 -0
  40. package/docs/contribution-sources.md +35 -0
  41. package/docs/github-about.md +15 -0
  42. package/docs/releases/v0.0.1.acceptance.json +160 -0
  43. package/docs/releases/v0.0.1.md +174 -0
  44. package/docs/releasing.en.md +290 -0
  45. package/docs/releasing.md +290 -0
  46. package/docs/testing.en.md +85 -0
  47. package/docs/testing.md +85 -0
  48. package/docs/troubleshooting.en.md +47 -0
  49. package/docs/troubleshooting.md +47 -0
  50. package/package.json +144 -0
  51. package/types/client.d.ts +160 -0
  52. package/types/index.d.ts +22 -0
  53. package/types/reliability.d.ts +78 -0
@@ -0,0 +1,662 @@
1
+ import { randomUUID } from "node:crypto"
2
+
3
+ import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization"
4
+
5
+ import {
6
+ CODEX_CREDENTIAL_KEY,
7
+ CODEX_PROVIDER_ID,
8
+ createCodexCredentialStore,
9
+ } from "./codex-credential-store.mjs"
10
+
11
+ export const AUTHORIZATION_RPC_CHANNEL = "/dsh-codex"
12
+ export const CODEX_AUTHORIZATION_KEY = CODEX_CREDENTIAL_KEY
13
+
14
+ const ATTEMPT_RETENTION_MS = 5 * 60_000
15
+ const STATUS_WAIT_MS = 25_000
16
+ const MAX_EVENTS = 128
17
+ const MAX_NOTICES = 32
18
+ const MAX_ATTEMPTS = 8
19
+ const MAX_RETAINED_ATTEMPTS = 64
20
+ const MAX_WAITERS_PER_ATTEMPT = 8
21
+ const MAX_METHODS = 16
22
+ const MAX_OPTIONS = 64
23
+ const MAX_ID_CHARS = 128
24
+ const MAX_ANSWER_CHARS = 16_384
25
+ const MAX_MESSAGE_CHARS = 2_048
26
+ const MAX_PLACEHOLDER_CHARS = 512
27
+ const MAX_CODE_CHARS = 256
28
+ const MAX_URL_CHARS = 2_048
29
+
30
+ class RpcInputError extends Error {
31
+ constructor(message) {
32
+ super(message)
33
+ this.name = "RpcInputError"
34
+ }
35
+ }
36
+
37
+ /**
38
+ * A short-lived interaction adapter around the public authorization seam.
39
+ * It never reads a credential value: only describeRecord() presence metadata
40
+ * crosses the interface, while notices and prompt answers live in memory for
41
+ * the duration of one loopback browser interaction.
42
+ */
43
+ export class CodexAuthorizationBridge {
44
+ #authorization
45
+ #credentials
46
+ #commitTracker
47
+ #quotaObserver
48
+ #attempts = new Map()
49
+ #closed = false
50
+ #retentionMs
51
+ #waitMs
52
+
53
+ constructor({ authorization, credentials }, options = {}) {
54
+ this.#authorization = authorization
55
+ this.#credentials = credentials
56
+ this.#commitTracker = options.commitTracker
57
+ this.#quotaObserver = options.quotaObserver
58
+ this.#retentionMs = options.retentionMs ?? ATTEMPT_RETENTION_MS
59
+ this.#waitMs = options.waitMs ?? STATUS_WAIT_MS
60
+ }
61
+
62
+ async status(payload = {}, signal) {
63
+ const input = objectInput(payload)
64
+ assertOnlyKeys(input, ["attemptId", "after"])
65
+ if (input.attemptId === undefined) {
66
+ if (input.after !== undefined) throw new RpcInputError("after requires attemptId")
67
+ return this.#publicStatus()
68
+ }
69
+
70
+ const attemptId = requiredString(input, "attemptId", { maxLength: MAX_ID_CHARS })
71
+ const after = optionalSequence(input.after)
72
+ const attempt = this.#attempt(attemptId)
73
+ await waitForAttempt(attempt, after, signal, this.#waitMs)
74
+ return {
75
+ attemptId,
76
+ events: attempt.events.filter((event) => event.seq > after),
77
+ nextSeq: attempt.events.at(-1)?.seq ?? after,
78
+ done: attempt.done,
79
+ }
80
+ }
81
+
82
+ async start(payload = {}) {
83
+ this.#assertOpen()
84
+ const input = objectInput(payload)
85
+ assertOnlyKeys(input, ["method"])
86
+ const activeAttempts = [...this.#attempts.values()].filter((attempt) => !attempt.done).length
87
+ if (activeAttempts >= MAX_ATTEMPTS) {
88
+ throw new RpcInputError("Too many authorization attempts")
89
+ }
90
+ if (this.#attempts.size >= MAX_RETAINED_ATTEMPTS) {
91
+ throw new RpcInputError("Too many recent authorization attempts")
92
+ }
93
+ const method = optionalString(input, "method", { maxLength: MAX_ID_CHARS })
94
+ const entry = this.#authorization.describe(CODEX_AUTHORIZATION_KEY)
95
+ if (entry === undefined) throw new RpcInputError("Codex authorization is unavailable")
96
+ if (method !== undefined && !entry.methods.some((candidate) => candidate.id === method)) {
97
+ throw new RpcInputError("Unknown Codex authorization method")
98
+ }
99
+
100
+ const attempt = {
101
+ id: randomUUID(),
102
+ controller: new AbortController(),
103
+ events: [],
104
+ nextSeq: 1,
105
+ noticeCount: 0,
106
+ prompt: undefined,
107
+ waiters: new Set(),
108
+ done: false,
109
+ cleanup: undefined,
110
+ }
111
+ this.#attempts.set(attempt.id, attempt)
112
+
113
+ void Promise.resolve().then(() => this.#authorization.begin({
114
+ key: CODEX_AUTHORIZATION_KEY,
115
+ ...(method === undefined ? {} : { method }),
116
+ signal: attempt.controller.signal,
117
+ interaction: {
118
+ notify: (notice) => {
119
+ if (!attempt.done && attempt.noticeCount < MAX_NOTICES) {
120
+ attempt.noticeCount += 1
121
+ this.#emit(attempt, {
122
+ type: "notice",
123
+ notice: serializeNotice(notice),
124
+ })
125
+ }
126
+ },
127
+ prompt: (prompt) => this.#prompt(attempt, prompt),
128
+ },
129
+ })).then(
130
+ (outcome) => this.#settle(attempt, {
131
+ type: "settled",
132
+ status: outcome.status,
133
+ }),
134
+ (error) => this.#settle(attempt, {
135
+ type: "failed",
136
+ error: safeAuthorizationFailure(error),
137
+ }),
138
+ )
139
+
140
+ return { attemptId: attempt.id }
141
+ }
142
+
143
+ respond(payload = {}) {
144
+ const input = objectInput(payload)
145
+ assertOnlyKeys(input, ["attemptId", "promptId", "action", "value"])
146
+ const attempt = this.#attempt(requiredString(input, "attemptId", { maxLength: MAX_ID_CHARS }))
147
+ const promptId = requiredString(input, "promptId", { maxLength: MAX_ID_CHARS })
148
+ const action = requiredString(input, "action", { maxLength: 16 })
149
+ const pending = attempt.prompt
150
+ if (pending === undefined || pending.id !== promptId) {
151
+ throw new RpcInputError("The authorization prompt is no longer active")
152
+ }
153
+
154
+ if (action === "answer") {
155
+ const value = requiredString(input, "value", {
156
+ allowEmpty: true,
157
+ maxLength: MAX_ANSWER_CHARS,
158
+ })
159
+ const answer = pending.mapAnswer(value)
160
+ this.#closePrompt(attempt, pending)
161
+ pending.resolve(answer)
162
+ return { accepted: true }
163
+ }
164
+ if (action === "decline") {
165
+ this.#closePrompt(attempt, pending)
166
+ pending.reject(new AuthorizationDeclinedError())
167
+ return { accepted: true }
168
+ }
169
+ throw new RpcInputError("Unknown prompt response action")
170
+ }
171
+
172
+ cancel(payload = {}) {
173
+ const input = objectInput(payload)
174
+ assertOnlyKeys(input, ["attemptId"])
175
+ const attempt = input.attemptId === undefined
176
+ ? undefined
177
+ : this.#attempt(requiredString(input, "attemptId", { maxLength: MAX_ID_CHARS }))
178
+ if (attempt?.done === true) return { accepted: false }
179
+ const cancel = () => {
180
+ if (attempt !== undefined) {
181
+ attempt.controller.abort("cancelled by the interaction surface")
182
+ this.#rejectPrompt(attempt, new Error("authorization cancelled"))
183
+ }
184
+ this.#authorization.cancel(CODEX_AUTHORIZATION_KEY)
185
+ }
186
+ if (this.#commitTracker?.tryCancel(cancel) === false) {
187
+ return { accepted: false, reason: "commit-in-progress" }
188
+ }
189
+ if (this.#commitTracker === undefined) cancel()
190
+ return { accepted: true }
191
+ }
192
+
193
+ async logout(payload = {}) {
194
+ const input = objectInput(payload)
195
+ assertOnlyKeys(input, [])
196
+ for (const attempt of this.#attempts.values()) {
197
+ if (attempt.done) continue
198
+ attempt.controller.abort("signed out by the interaction surface")
199
+ this.#rejectPrompt(attempt, new Error("authorization signed out"))
200
+ }
201
+ this.#authorization.cancel(CODEX_AUTHORIZATION_KEY)
202
+ await this.#credentials.deleteRecord(CODEX_AUTHORIZATION_KEY)
203
+ return { signedOut: true }
204
+ }
205
+
206
+ dispatch(endpoint, payload, signal) {
207
+ switch (endpoint) {
208
+ case "status": return this.status(payload, signal)
209
+ case "start": return this.start(payload)
210
+ case "respond": return this.respond(payload)
211
+ case "cancel": return this.cancel(payload)
212
+ case "logout": return this.logout(payload)
213
+ default: throw new RpcInputError("Unknown dsh-codex RPC endpoint")
214
+ }
215
+ }
216
+
217
+ dispose() {
218
+ if (this.#closed) return
219
+ this.#closed = true
220
+ const cancel = () => {
221
+ for (const attempt of this.#attempts.values()) {
222
+ if (!attempt.done) attempt.controller.abort("authorization bridge disposed")
223
+ }
224
+ this.#authorization.cancel(CODEX_AUTHORIZATION_KEY)
225
+ }
226
+ // A disposed interaction surface may detach from an irreversible commit,
227
+ // but must not make AuthorizationService report that commit as cancelled.
228
+ if (this.#commitTracker === undefined) cancel()
229
+ else this.#commitTracker.tryCancel(cancel)
230
+ for (const attempt of this.#attempts.values()) {
231
+ if (attempt.cleanup !== undefined) clearTimeout(attempt.cleanup)
232
+ this.#rejectPrompt(attempt, new Error("authorization bridge disposed"))
233
+ wakeAttempt(attempt)
234
+ }
235
+ this.#attempts.clear()
236
+ }
237
+
238
+ async #publicStatus() {
239
+ const entry = this.#authorization.describe(CODEX_AUTHORIZATION_KEY)
240
+ const credential = await describeCodexCredential(this.#credentials)
241
+ return {
242
+ flow: entry === undefined ? undefined : {
243
+ key: CODEX_AUTHORIZATION_KEY,
244
+ label: boundedText(entry.label, MAX_MESSAGE_CHARS),
245
+ methods: entry.methods
246
+ .filter(({ id }) => typeof id === "string" && id.length > 0 && id.length <= MAX_ID_CHARS)
247
+ .slice(0, MAX_METHODS)
248
+ .map(({ id, label }) => ({ id, label: boundedText(label, MAX_MESSAGE_CHARS) })),
249
+ inFlight: entry.inFlight,
250
+ },
251
+ credential,
252
+ quota: this.#quotaObserver?.snapshot() ?? Object.freeze({ status: "unknown" }),
253
+ }
254
+ }
255
+
256
+ #attempt(id) {
257
+ const attempt = this.#attempts.get(id)
258
+ if (attempt === undefined) throw new RpcInputError("Unknown authorization attempt")
259
+ return attempt
260
+ }
261
+
262
+ #assertOpen() {
263
+ if (this.#closed) throw new RpcInputError("Authorization bridge is closed")
264
+ }
265
+
266
+ #prompt(attempt, prompt) {
267
+ if (attempt.done || attempt.controller.signal.aborted) {
268
+ return Promise.reject(new Error("authorization attempt is no longer active"))
269
+ }
270
+ if (attempt.prompt !== undefined) {
271
+ return Promise.reject(new Error("authorization flow opened overlapping prompts"))
272
+ }
273
+ if (prompt.signal?.aborted === true) {
274
+ return Promise.reject(new Error("authorization prompt was already withdrawn"))
275
+ }
276
+
277
+ return new Promise((resolve, reject) => {
278
+ const serialized = serializePrompt(prompt)
279
+ const pending = {
280
+ id: randomUUID(),
281
+ resolve,
282
+ reject,
283
+ mapAnswer: serialized.mapAnswer,
284
+ signal: prompt.signal,
285
+ onAbort: undefined,
286
+ }
287
+ if (prompt.signal !== undefined) {
288
+ pending.onAbort = () => {
289
+ if (attempt.prompt !== pending) return
290
+ this.#closePrompt(attempt, pending)
291
+ reject(new Error("authorization prompt withdrawn"))
292
+ }
293
+ prompt.signal.addEventListener("abort", pending.onAbort, { once: true })
294
+ }
295
+ attempt.prompt = pending
296
+ this.#emit(attempt, {
297
+ type: "prompt",
298
+ promptId: pending.id,
299
+ prompt: serialized.prompt,
300
+ })
301
+ })
302
+ }
303
+
304
+ #closePrompt(attempt, pending) {
305
+ if (pending.signal !== undefined && pending.onAbort !== undefined) {
306
+ pending.signal.removeEventListener("abort", pending.onAbort)
307
+ }
308
+ if (attempt.prompt === pending) attempt.prompt = undefined
309
+ this.#emit(attempt, { type: "prompt-closed", promptId: pending.id })
310
+ }
311
+
312
+ #rejectPrompt(attempt, error) {
313
+ const pending = attempt.prompt
314
+ if (pending === undefined) return
315
+ this.#closePrompt(attempt, pending)
316
+ pending.reject(error)
317
+ }
318
+
319
+ #settle(attempt, terminal) {
320
+ if (attempt.done) return
321
+ attempt.done = true
322
+ this.#rejectPrompt(attempt, new Error("authorization attempt settled"))
323
+ this.#emit(attempt, terminal)
324
+ attempt.cleanup = setTimeout(() => {
325
+ this.#attempts.delete(attempt.id)
326
+ }, this.#retentionMs)
327
+ attempt.cleanup.unref?.()
328
+ }
329
+
330
+ #emit(attempt, event) {
331
+ attempt.events.push(Object.freeze({ seq: attempt.nextSeq++, ...event }))
332
+ if (attempt.events.length > MAX_EVENTS) attempt.events.shift()
333
+ wakeAttempt(attempt)
334
+ }
335
+ }
336
+
337
+ export function installAuthorizationRpc(ctx, options) {
338
+ const bridge = new CodexAuthorizationBridge({
339
+ authorization: ctx.authorization,
340
+ credentials: ctx.credentials,
341
+ }, options)
342
+ ctx.connection.rpc.handle(
343
+ AUTHORIZATION_RPC_CHANNEL,
344
+ createAuthorizationRpcHandler(bridge),
345
+ { authority: "loopback" },
346
+ )
347
+ ctx.effect(() => () => bridge.dispose(), "dsh-codex: authorization RPC state")
348
+ return bridge
349
+ }
350
+
351
+ export function createAuthorizationRpcHandler(bridge) {
352
+ return async (endpoint, payload, signal) => {
353
+ try {
354
+ return { ok: true, value: await bridge.dispatch(endpoint, payload, signal) }
355
+ } catch (error) {
356
+ if (signal?.aborted === true) {
357
+ return {
358
+ ok: false,
359
+ error: { code: "cancelled", message: "Request cancelled", details: {} },
360
+ }
361
+ }
362
+ if (error instanceof RpcInputError) {
363
+ return {
364
+ ok: false,
365
+ error: { code: "bad-request", message: error.message, details: { issues: [] } },
366
+ }
367
+ }
368
+ return {
369
+ ok: false,
370
+ error: { code: "internal", message: "Authorization request failed", details: {} },
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ export function registerCodexLoginCommand(ctx, options = {}) {
377
+ const commitTracker = options.commitTracker
378
+ ctx.commands.register({
379
+ name: "codex-login",
380
+ description: "管理 Codex 登录 / Manage Codex sign-in",
381
+ input: { hint: "[status|cancel|logout]" },
382
+ recordInput: false,
383
+ handler: async ({ rawInput }) => {
384
+ const action = rawInput.trim()
385
+ try {
386
+ if (action === "cancel") {
387
+ const cancel = () => ctx.authorization.cancel(CODEX_AUTHORIZATION_KEY)
388
+ if (commitTracker?.tryCancel(cancel) === false) {
389
+ return {
390
+ kind: "success",
391
+ text: "Codex 凭据已开始提交,当前无法取消;请稍候检查登录状态。 / Codex credential commit has started and can no longer be cancelled; check sign-in status shortly.",
392
+ }
393
+ }
394
+ if (commitTracker === undefined) cancel()
395
+ return {
396
+ kind: "success",
397
+ text: "已请求取消 Codex 登录。请在 Codex 设置页重试。 / Codex sign-in cancellation requested. Retry from Codex Settings.",
398
+ }
399
+ }
400
+ if (action === "logout") {
401
+ ctx.authorization.cancel(CODEX_AUTHORIZATION_KEY)
402
+ await ctx.credentials.deleteRecord(CODEX_AUTHORIZATION_KEY)
403
+ return {
404
+ kind: "success",
405
+ text: "已退出 Codex 登录。 / Signed out of Codex.",
406
+ }
407
+ }
408
+ if (action !== "" && action !== "status") {
409
+ return {
410
+ kind: "error",
411
+ text: "用法:/codex-login [status|cancel|logout] / Usage: /codex-login [status|cancel|logout]",
412
+ }
413
+ }
414
+
415
+ const flow = ctx.authorization.describe(CODEX_AUTHORIZATION_KEY)
416
+ const credential = await describeCodexCredential(ctx.credentials)
417
+ if (flow?.inFlight === true) {
418
+ return {
419
+ kind: "success",
420
+ text: "Codex 登录正在进行;请在 Codex 设置页继续。 / Codex sign-in is in progress; continue in Codex Settings.",
421
+ }
422
+ }
423
+ if (credential.state === "signed-in") {
424
+ return {
425
+ kind: "success",
426
+ text: "Codex 已登录。 / Codex is signed in.",
427
+ }
428
+ }
429
+ if (credential.state === "invalid") {
430
+ return {
431
+ kind: "success",
432
+ text: "已保存的 Codex 凭据无效;请重新登录或退出后重试。 / The saved Codex credential is invalid; sign in again or sign out before retrying.",
433
+ }
434
+ }
435
+ return {
436
+ kind: "success",
437
+ text: flow === undefined
438
+ ? "Codex 登录当前不可用。 / Codex sign-in is currently unavailable."
439
+ : "Codex 尚未登录;请打开 Codex 设置页。 / Codex is not signed in; open Codex Settings.",
440
+ }
441
+ } catch {
442
+ return {
443
+ kind: "error",
444
+ text: "Codex 登录操作失败,请在设置页重试。 / Codex sign-in action failed. Retry from Settings.",
445
+ }
446
+ }
447
+ },
448
+ })
449
+ }
450
+
451
+ async function describeCodexCredential(credentials) {
452
+ const description = await credentials.describeRecord(CODEX_AUTHORIZATION_KEY)
453
+ let state = "signed-out"
454
+ if (description.configured === true) {
455
+ if (description.kind !== "grant") {
456
+ state = "invalid"
457
+ } else {
458
+ try {
459
+ const credential = await createCodexCredentialStore(credentials).read(CODEX_PROVIDER_ID)
460
+ state = credential === undefined ? "signed-out" : "signed-in"
461
+ } catch {
462
+ state = "invalid"
463
+ }
464
+ }
465
+ }
466
+ return {
467
+ configured: description.configured === true,
468
+ state,
469
+ ...(description.kind === undefined ? {} : { kind: description.kind }),
470
+ writable: description.writable === true,
471
+ }
472
+ }
473
+
474
+ export function registerCodexUsageCommand(ctx, quotaObserver) {
475
+ ctx.commands.register({
476
+ name: "codex-usage",
477
+ description: "查看最近观测到的 Codex 额度状态 / Show the last observed Codex quota state",
478
+ input: { hint: "[status]" },
479
+ recordInput: false,
480
+ handler: async ({ rawInput }) => {
481
+ const action = rawInput.trim()
482
+ if (action !== "" && action !== "status") {
483
+ return {
484
+ kind: "error",
485
+ text: "用法:/codex-usage [status] / Usage: /codex-usage [status]",
486
+ }
487
+ }
488
+
489
+ try {
490
+ return {
491
+ kind: "success",
492
+ text: formatQuotaSnapshot(quotaObserver.snapshot()),
493
+ }
494
+ } catch {
495
+ return {
496
+ kind: "error",
497
+ text: "暂时无法读取 Codex 额度观测。 / Codex quota observation is temporarily unavailable.",
498
+ }
499
+ }
500
+ },
501
+ })
502
+ }
503
+
504
+ function formatQuotaSnapshot(snapshot) {
505
+ if (snapshot.status === "recent-success") {
506
+ return `最近一次 Codex 请求成功于 ${new Date(snapshot.observedAt).toISOString()};这不代表账户剩余额度。 / Last Codex request succeeded at ${new Date(snapshot.observedAt).toISOString()}; this does not represent remaining account quota.`
507
+ }
508
+ if (snapshot.status === "exhausted") {
509
+ if (snapshot.resetAt === undefined) {
510
+ return "最近观测到 Codex 账户额度耗尽,但未获得通过校验的重置时间。 / Codex account quota was exhausted in the latest observation, but no reset time passed validation."
511
+ }
512
+ const reset = new Date(snapshot.resetAt).toISOString()
513
+ return `最近观测到 Codex 账户额度耗尽;观测到的重置时间:${reset}。 / Codex account quota was exhausted in the latest observation; observed reset time: ${reset}.`
514
+ }
515
+ return "暂无近期 Codex 额度观测;插件不会调用未公开的账户额度接口。 / No recent Codex quota observation; the plugin does not call undocumented account-quota endpoints."
516
+ }
517
+
518
+ function objectInput(value) {
519
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
520
+ throw new RpcInputError("RPC payload must be an object")
521
+ }
522
+ return value
523
+ }
524
+
525
+ function requiredString(value, key, options = {}) {
526
+ const field = value[key]
527
+ if (typeof field !== "string" || (!options.allowEmpty && field.length === 0)) {
528
+ throw new RpcInputError(`${key} must be a ${options.allowEmpty ? "string" : "non-empty string"}`)
529
+ }
530
+ if (options.maxLength !== undefined && field.length > options.maxLength) {
531
+ throw new RpcInputError(`${key} is too long`)
532
+ }
533
+ return field
534
+ }
535
+
536
+ function optionalString(value, key, options) {
537
+ if (value[key] === undefined) return undefined
538
+ return requiredString(value, key, options)
539
+ }
540
+
541
+ function assertOnlyKeys(value, allowed) {
542
+ const keys = new Set(allowed)
543
+ if (Object.keys(value).some((key) => !keys.has(key))) {
544
+ throw new RpcInputError("RPC payload contains an unknown field")
545
+ }
546
+ }
547
+
548
+ function optionalSequence(value) {
549
+ if (value === undefined) return 0
550
+ if (!Number.isSafeInteger(value) || value < 0) {
551
+ throw new RpcInputError("after must be a non-negative safe integer")
552
+ }
553
+ return value
554
+ }
555
+
556
+ function serializeNotice(notice) {
557
+ const url = safeAuthorizationUrl(notice.url)
558
+ return {
559
+ message: boundedText(notice.message, MAX_MESSAGE_CHARS),
560
+ ...(url === undefined ? {} : { url }),
561
+ ...(notice.code === undefined ? {} : { code: boundedText(notice.code, MAX_CODE_CHARS) }),
562
+ }
563
+ }
564
+
565
+ function serializePrompt(prompt) {
566
+ if (prompt.kind === "select") {
567
+ const answers = new Map()
568
+ const options = prompt.options.slice(0, MAX_OPTIONS).map((option, index) => {
569
+ const id = `option-${index + 1}`
570
+ answers.set(id, String(option.id))
571
+ return {
572
+ id,
573
+ label: boundedText(option.label, MAX_MESSAGE_CHARS),
574
+ ...(option.description === undefined
575
+ ? {}
576
+ : { description: boundedText(option.description, MAX_MESSAGE_CHARS) }),
577
+ }
578
+ })
579
+ return {
580
+ prompt: {
581
+ kind: "select",
582
+ message: boundedText(prompt.message, MAX_MESSAGE_CHARS),
583
+ options,
584
+ },
585
+ mapAnswer: (answer) => {
586
+ const selected = answers.get(answer)
587
+ if (selected === undefined) throw new RpcInputError("Unknown prompt option")
588
+ return selected
589
+ },
590
+ }
591
+ }
592
+ return {
593
+ prompt: {
594
+ kind: prompt.kind === "secret" ? "secret" : "text",
595
+ message: boundedText(prompt.message, MAX_MESSAGE_CHARS),
596
+ ...(prompt.placeholder === undefined
597
+ ? {}
598
+ : { placeholder: boundedText(prompt.placeholder, MAX_PLACEHOLDER_CHARS) }),
599
+ },
600
+ mapAnswer: (answer) => answer,
601
+ }
602
+ }
603
+
604
+ function boundedText(value, maxLength) {
605
+ const text = String(value)
606
+ return text.length <= maxLength ? text : text.slice(0, maxLength)
607
+ }
608
+
609
+ function safeAuthorizationUrl(value) {
610
+ if (value === undefined) return undefined
611
+ const text = String(value)
612
+ if (text.length === 0 || text.length > MAX_URL_CHARS) return undefined
613
+ try {
614
+ const url = new URL(text)
615
+ if (url.username !== "" || url.password !== "") return undefined
616
+ if (url.protocol === "https:") return url.href
617
+ if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname)) return undefined
618
+ return url.href
619
+ } catch {
620
+ return undefined
621
+ }
622
+ }
623
+
624
+ function isLoopbackHostname(hostname) {
625
+ return hostname === "localhost"
626
+ || hostname === "[::1]"
627
+ || /^127(?:\.\d{1,3}){3}$/u.test(hostname)
628
+ }
629
+
630
+ function safeAuthorizationFailure(error) {
631
+ const candidate = typeof error?.code === "string" ? error.code : "AUTHORIZATION_FAILED"
632
+ const code = /^[A-Z0-9_-]{1,64}$/u.test(candidate) ? candidate : "AUTHORIZATION_FAILED"
633
+ return { code, message: "Authorization failed" }
634
+ }
635
+
636
+ function waitForAttempt(attempt, after, signal, waitMs) {
637
+ if (attempt.done || attempt.events.some((event) => event.seq > after)) return Promise.resolve()
638
+ if (signal?.aborted === true) return Promise.reject(signal.reason ?? new Error("request cancelled"))
639
+ if (attempt.waiters.size >= MAX_WAITERS_PER_ATTEMPT) {
640
+ return Promise.reject(new RpcInputError("Too many status waiters"))
641
+ }
642
+ return new Promise((resolve, reject) => {
643
+ let timer
644
+ const finish = (error) => {
645
+ attempt.waiters.delete(wake)
646
+ if (timer !== undefined) clearTimeout(timer)
647
+ signal?.removeEventListener("abort", abort)
648
+ if (error === undefined) resolve()
649
+ else reject(error)
650
+ }
651
+ const wake = () => finish()
652
+ const abort = () => finish(signal.reason ?? new Error("request cancelled"))
653
+ attempt.waiters.add(wake)
654
+ signal?.addEventListener("abort", abort, { once: true })
655
+ timer = setTimeout(wake, waitMs)
656
+ timer.unref?.()
657
+ })
658
+ }
659
+
660
+ function wakeAttempt(attempt) {
661
+ for (const wake of [...attempt.waiters]) wake()
662
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Coordinate the user-facing cancellation boundary around one local Codex
3
+ * authorization flow. No credential material enters this state machine.
4
+ */
5
+ export function createAuthorizationCommitTracker() {
6
+ let generation = 0
7
+ let active
8
+
9
+ return Object.freeze({
10
+ begin() {
11
+ if (active !== undefined) {
12
+ throw new Error("a Codex authorization attempt is already tracked")
13
+ }
14
+ const id = ++generation
15
+ active = { id, phase: "cancellable" }
16
+ let finished = false
17
+
18
+ return Object.freeze({
19
+ selectCommit() {
20
+ if (finished || active?.id !== id || active.phase !== "cancellable") return false
21
+ active = { id, phase: "committing" }
22
+ return true
23
+ },
24
+ finish() {
25
+ if (finished) return
26
+ finished = true
27
+ if (active?.id === id && active.phase === "cancellable") active = undefined
28
+ },
29
+ })
30
+ },
31
+
32
+ /** Check and run cancellation in one synchronous call stack. */
33
+ tryCancel(cancel) {
34
+ if (typeof cancel !== "function") throw new TypeError("cancel must be a function")
35
+ if (active?.phase === "committing") return false
36
+ cancel()
37
+ return true
38
+ },
39
+
40
+ isCommitPending() {
41
+ return active?.phase === "committing"
42
+ },
43
+
44
+ /** Called when the public authorization attempt reaches its terminal state. */
45
+ settle() {
46
+ active = undefined
47
+ },
48
+ })
49
+ }