@narumitw/pi-usage 0.60.7 → 0.60.8

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.
@@ -1,16 +1,8 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { readStoredCredential } from "@earendil-works/pi-coding-agent";
3
3
  import { fingerprintResolvedAuth, sanitizeDisplayText } from "./core.js";
4
- import {
5
- fallbackOAuthCredentialCandidates,
6
- type OAuthCredentialCandidateReader,
7
- } from "./oauth-credential-source.js";
8
- import {
9
- AUTH_FINGERPRINT_SALT,
10
- adapterForProvider,
11
- fetchProviderJson,
12
- resolveUsageAuth,
13
- } from "./query.js";
4
+ import { fallbackOAuthCredentialCandidates, type OAuthCredentialCandidateReader } from "./oauth-credential-source.js";
5
+ import { AUTH_FINGERPRINT_SALT, adapterForProvider, fetchProviderJson, resolveUsageAuth } from "./query.js";
14
6
  import type { ResolvedUsageAuth, UsageReport } from "./types.js";
15
7
 
16
8
  const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
@@ -23,351 +15,330 @@ type StoredCredentialReader = (providerId: string) => unknown;
23
15
  export type CodexResetOutcomeCode = "reset" | "nothing_to_reset" | "no_credit" | "already_redeemed";
24
16
 
25
17
  export interface CodexResetOption {
26
- creditId?: string;
27
- title: string;
28
- description: string;
29
- expiresAt?: number;
18
+ creditId?: string;
19
+ title: string;
20
+ description: string;
21
+ expiresAt?: number;
30
22
  }
31
23
 
32
24
  export interface CodexResetAvailability {
33
- availableCount: number;
34
- options: CodexResetOption[];
25
+ availableCount: number;
26
+ options: CodexResetOption[];
35
27
  }
36
28
 
37
29
  export interface CodexResetOutcome {
38
- code: CodexResetOutcomeCode;
39
- windowsReset: number;
30
+ code: CodexResetOutcomeCode;
31
+ windowsReset: number;
40
32
  }
41
33
 
42
34
  export function codexResetCount(report: UsageReport): number | undefined {
43
- const value = report.metrics.find((metric) => metric.id === "reset-credits")?.value;
44
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
35
+ const value = report.metrics.find((metric) => metric.id === "reset-credits")?.value;
36
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
45
37
  }
46
38
 
47
39
  export function codexResetActionDescription(report: UsageReport): string {
48
- const count = codexResetCount(report);
49
- if (count === undefined) return "Check reset availability.";
50
- if (count === 0) return "No usage limit resets available.";
51
- return `You have ${count} ${resetLabel(count)} available.`;
40
+ const count = codexResetCount(report);
41
+ if (count === undefined) return "Check reset availability.";
42
+ if (count === 0) return "No usage limit resets available.";
43
+ return `You have ${count} ${resetLabel(count)} available.`;
52
44
  }
53
45
 
54
46
  export function genericCodexResetOption(): CodexResetOption {
55
- return {
56
- title: "Full reset",
57
- description: "Reset your current usage limits.",
58
- };
47
+ return {
48
+ title: "Full reset",
49
+ description: "Reset your current usage limits.",
50
+ };
59
51
  }
60
52
 
61
53
  export function resetOptionExpiration(option: CodexResetOption): string {
62
- if (option.expiresAt === undefined) return "Does not expire.";
63
- const expiration = new Date(option.expiresAt * 1_000);
64
- if (Number.isNaN(expiration.getTime())) return "Expiration unavailable.";
65
- return `Expires ${expiration.toLocaleString()}.`;
54
+ if (option.expiresAt === undefined) return "Does not expire.";
55
+ const expiration = new Date(option.expiresAt * 1_000);
56
+ if (Number.isNaN(expiration.getTime())) return "Expiration unavailable.";
57
+ return `Expires ${expiration.toLocaleString()}.`;
66
58
  }
67
59
 
68
60
  export function resetConfirmationLines(option: CodexResetOption | undefined): string[] {
69
- if (!option) return ["The selected reset is unavailable."];
70
- return [
71
- option.title,
72
- resetOptionExpiration(option),
73
- option.description,
74
- "This consumes one earned reset for the current OpenAI Codex account.",
75
- ];
61
+ if (!option) return ["The selected reset is unavailable."];
62
+ return [
63
+ option.title,
64
+ resetOptionExpiration(option),
65
+ option.description,
66
+ "This consumes one earned reset for the current OpenAI Codex account.",
67
+ ];
76
68
  }
77
69
 
78
70
  export function formatCodexResetOutcome(
79
- outcome: CodexResetOutcome | undefined,
80
- remainingCount: number | undefined,
71
+ outcome: CodexResetOutcome | undefined,
72
+ remainingCount: number | undefined,
81
73
  ): string {
82
- const remaining =
83
- remainingCount === undefined
84
- ? ""
85
- : ` You have ${remainingCount} ${resetLabel(remainingCount)} left.`;
86
- if (!outcome) return "You don't have any usage limit resets available.";
87
- if (outcome.code === "reset") return `Usage reset.${remaining}`.trim();
88
- if (outcome.code === "already_redeemed") {
89
- return `Usage reset was already completed.${remaining}`.trim();
90
- }
91
- if (outcome.code === "nothing_to_reset") {
92
- return "Your usage does not need a reset right now.";
93
- }
94
- return "No usage limit resets are available.";
74
+ const remaining =
75
+ remainingCount === undefined ? "" : ` You have ${remainingCount} ${resetLabel(remainingCount)} left.`;
76
+ if (!outcome) return "You don't have any usage limit resets available.";
77
+ if (outcome.code === "reset") return `Usage reset.${remaining}`.trim();
78
+ if (outcome.code === "already_redeemed") {
79
+ return `Usage reset was already completed.${remaining}`.trim();
80
+ }
81
+ if (outcome.code === "nothing_to_reset") {
82
+ return "Your usage does not need a reset right now.";
83
+ }
84
+ return "No usage limit resets are available.";
95
85
  }
96
86
 
97
87
  export function resetLabel(count: number): string {
98
- return count === 1 ? "usage limit reset" : "usage limit resets";
88
+ return count === 1 ? "usage limit reset" : "usage limit resets";
99
89
  }
100
90
 
101
91
  export async function resolveCodexResetAuth(
102
- ctx: ExtensionContext,
103
- salt: Uint8Array = AUTH_FINGERPRINT_SALT,
104
- credentialReader: StoredCredentialReader = readStoredCredential,
105
- candidateReader?: OAuthCredentialCandidateReader,
92
+ ctx: ExtensionContext,
93
+ salt: Uint8Array = AUTH_FINGERPRINT_SALT,
94
+ credentialReader: StoredCredentialReader = readStoredCredential,
95
+ candidateReader?: OAuthCredentialCandidateReader,
106
96
  ): Promise<ResolvedUsageAuth> {
107
- const model = ctx.model;
108
- if (model?.provider !== "openai-codex") {
109
- throw new Error("Usage limit resets require the current model to use OpenAI Codex.");
110
- }
111
- const expectedModel = `${model.provider}/${model.id}`;
112
- const adapter = adapterForProvider("openai-codex");
113
- if (!adapter) throw new Error("OpenAI Codex usage support is unavailable.");
114
- const auth = await resolveUsageAuth(ctx, adapter, salt, credentialReader);
115
- if (`${ctx.model?.provider}/${ctx.model?.id}` !== expectedModel) {
116
- throw new Error("The current model changed while resolving Codex reset authentication.");
117
- }
118
- if (!auth) throw new Error("No runtime credential is configured for OpenAI Codex.");
119
-
120
- const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
121
- if (!resolvedAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
122
- const resolvedAccountId = codexAccountIdFromAccessToken(resolvedAccess);
123
- if (!resolvedAccountId) {
124
- throw new Error("The active OpenAI Codex access token did not contain a valid account ID.");
125
- }
126
- const offered = candidateReader
127
- ? candidateReader(ctx, "openai-codex")
128
- : fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
129
- if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
130
- const { accountId, storedAccess } = selectCodexResetCredential(
131
- offered.candidates,
132
- resolvedAccess,
133
- resolvedAccountId,
134
- offered.offeredCount === 0,
135
- );
136
- const authorization = `Bearer ${resolvedAccess}`;
137
- const headers = {
138
- Authorization: authorization,
139
- "chatgpt-account-id": accountId,
140
- };
141
- return {
142
- apiKey: resolvedAccess,
143
- headers,
144
- fingerprint: fingerprintResolvedAuth({ headers }, salt),
145
- secrets: [
146
- ...new Set([...auth.secrets, storedAccess, resolvedAccess, authorization, accountId]),
147
- ],
148
- model: auth.model,
149
- };
97
+ const model = ctx.model;
98
+ if (model?.provider !== "openai-codex") {
99
+ throw new Error("Usage limit resets require the current model to use OpenAI Codex.");
100
+ }
101
+ const expectedModel = `${model.provider}/${model.id}`;
102
+ const adapter = adapterForProvider("openai-codex");
103
+ if (!adapter) throw new Error("OpenAI Codex usage support is unavailable.");
104
+ const auth = await resolveUsageAuth(ctx, adapter, salt, credentialReader);
105
+ if (`${ctx.model?.provider}/${ctx.model?.id}` !== expectedModel) {
106
+ throw new Error("The current model changed while resolving Codex reset authentication.");
107
+ }
108
+ if (!auth) throw new Error("No runtime credential is configured for OpenAI Codex.");
109
+
110
+ const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
111
+ if (!resolvedAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
112
+ const resolvedAccountId = codexAccountIdFromAccessToken(resolvedAccess);
113
+ if (!resolvedAccountId) {
114
+ throw new Error("The active OpenAI Codex access token did not contain a valid account ID.");
115
+ }
116
+ const offered = candidateReader
117
+ ? candidateReader(ctx, "openai-codex")
118
+ : fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
119
+ if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
120
+ const { accountId, storedAccess } = selectCodexResetCredential(
121
+ offered.candidates,
122
+ resolvedAccess,
123
+ resolvedAccountId,
124
+ offered.offeredCount === 0,
125
+ );
126
+ const authorization = `Bearer ${resolvedAccess}`;
127
+ const headers = {
128
+ Authorization: authorization,
129
+ "chatgpt-account-id": accountId,
130
+ };
131
+ return {
132
+ apiKey: resolvedAccess,
133
+ headers,
134
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
135
+ secrets: [...new Set([...auth.secrets, storedAccess, resolvedAccess, authorization, accountId])],
136
+ model: auth.model,
137
+ };
150
138
  }
151
139
 
152
140
  export async function listCodexResetCredits(
153
- auth: ResolvedUsageAuth,
154
- signal: AbortSignal,
155
- timeoutMs: number,
141
+ auth: ResolvedUsageAuth,
142
+ signal: AbortSignal,
143
+ timeoutMs: number,
156
144
  ): Promise<CodexResetAvailability> {
157
- const payload = await fetchProviderJson(
158
- CODEX_RESET_CREDITS_URL,
159
- auth,
160
- signal,
161
- timeoutMs,
162
- "Codex usage-limit reset endpoint",
163
- );
164
- return normalizeCodexResetCreditsPayload(payload);
145
+ const payload = await fetchProviderJson(
146
+ CODEX_RESET_CREDITS_URL,
147
+ auth,
148
+ signal,
149
+ timeoutMs,
150
+ "Codex usage-limit reset endpoint",
151
+ );
152
+ return normalizeCodexResetCreditsPayload(payload);
165
153
  }
166
154
 
167
155
  export async function consumeCodexResetCredit(
168
- auth: ResolvedUsageAuth,
169
- option: CodexResetOption,
170
- redeemRequestId: string,
171
- signal: AbortSignal,
172
- timeoutMs: number,
156
+ auth: ResolvedUsageAuth,
157
+ option: CodexResetOption,
158
+ redeemRequestId: string,
159
+ signal: AbortSignal,
160
+ timeoutMs: number,
173
161
  ): Promise<CodexResetOutcome> {
174
- if (!redeemRequestId) throw new Error("Codex reset redemption request ID must not be empty.");
175
- const payload = await fetchProviderJson(
176
- CODEX_RESET_CONSUME_URL,
177
- auth,
178
- signal,
179
- timeoutMs,
180
- "Codex usage-limit reset consume endpoint",
181
- {
182
- method: "POST",
183
- body: {
184
- redeem_request_id: redeemRequestId,
185
- ...(option.creditId ? { credit_id: option.creditId } : {}),
186
- },
187
- },
188
- );
189
- const code = payload.code;
190
- if (!isCodexResetOutcomeCode(code)) {
191
- throw new Error("Codex reset consume endpoint returned an unknown outcome code.");
192
- }
193
- const windowsReset =
194
- payload.windows_reset === undefined ? 0 : nonnegativeInteger(payload.windows_reset);
195
- if (windowsReset === undefined) {
196
- throw new Error("Codex reset consume endpoint returned an invalid windows_reset value.");
197
- }
198
- return { code, windowsReset };
162
+ if (!redeemRequestId) throw new Error("Codex reset redemption request ID must not be empty.");
163
+ const payload = await fetchProviderJson(
164
+ CODEX_RESET_CONSUME_URL,
165
+ auth,
166
+ signal,
167
+ timeoutMs,
168
+ "Codex usage-limit reset consume endpoint",
169
+ {
170
+ method: "POST",
171
+ body: {
172
+ redeem_request_id: redeemRequestId,
173
+ ...(option.creditId ? { credit_id: option.creditId } : {}),
174
+ },
175
+ },
176
+ );
177
+ const code = payload.code;
178
+ if (!isCodexResetOutcomeCode(code)) {
179
+ throw new Error("Codex reset consume endpoint returned an unknown outcome code.");
180
+ }
181
+ const windowsReset = payload.windows_reset === undefined ? 0 : nonnegativeInteger(payload.windows_reset);
182
+ if (windowsReset === undefined) {
183
+ throw new Error("Codex reset consume endpoint returned an invalid windows_reset value.");
184
+ }
185
+ return { code, windowsReset };
199
186
  }
200
187
 
201
- export function normalizeCodexResetCreditsPayload(
202
- payload: Record<string, unknown>,
203
- ): CodexResetAvailability {
204
- const availableCount = nonnegativeInteger(payload.available_count);
205
- if (availableCount === undefined) {
206
- throw new Error("Codex reset credits response returned an invalid available_count.");
207
- }
208
- const rawCredits = payload.credits;
209
- if (rawCredits !== undefined && !Array.isArray(rawCredits)) {
210
- throw new Error("Codex reset credits response returned invalid credits.");
211
- }
212
-
213
- const options = (rawCredits ?? [])
214
- .map(asObject)
215
- .filter((credit): credit is Record<string, unknown> => Boolean(credit))
216
- .filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits")
217
- .map(normalizeResetOption)
218
- .sort(
219
- (left, right) =>
220
- (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER),
221
- )
222
- .slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
223
-
224
- if (availableCount > 0 && options.length === 0) {
225
- options.push(genericCodexResetOption());
226
- }
227
- return { availableCount, options };
188
+ export function normalizeCodexResetCreditsPayload(payload: Record<string, unknown>): CodexResetAvailability {
189
+ const availableCount = nonnegativeInteger(payload.available_count);
190
+ if (availableCount === undefined) {
191
+ throw new Error("Codex reset credits response returned an invalid available_count.");
192
+ }
193
+ const rawCredits = payload.credits;
194
+ if (rawCredits !== undefined && !Array.isArray(rawCredits)) {
195
+ throw new Error("Codex reset credits response returned invalid credits.");
196
+ }
197
+
198
+ const options = (rawCredits ?? [])
199
+ .map(asObject)
200
+ .filter((credit): credit is Record<string, unknown> => Boolean(credit))
201
+ .filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits")
202
+ .map(normalizeResetOption)
203
+ .sort((left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER))
204
+ .slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
205
+
206
+ if (availableCount > 0 && options.length === 0) {
207
+ options.push(genericCodexResetOption());
208
+ }
209
+ return { availableCount, options };
228
210
  }
229
211
 
230
212
  function selectCodexResetCredential(
231
- candidates: readonly unknown[],
232
- resolvedAccess: string,
233
- resolvedAccountId: string,
234
- standaloneFallback: boolean,
213
+ candidates: readonly unknown[],
214
+ resolvedAccess: string,
215
+ resolvedAccountId: string,
216
+ standaloneFallback: boolean,
235
217
  ): { accountId: string; storedAccess: string } {
236
- let sawOAuth = false;
237
- let sawMatchingAccess = false;
238
- let sawInvalidAccountId = false;
239
- const matches = new Map<string, { accountId: string; storedAccess: string }>();
240
- for (const candidate of candidates) {
241
- try {
242
- const credential = asObject(candidate);
243
- if (credential?.type !== "oauth") continue;
244
- sawOAuth = true;
245
- const storedAccess = asNonemptyString(credential.access);
246
- if (storedAccess !== resolvedAccess) continue;
247
- sawMatchingAccess = true;
248
- const accountId = validHeaderValue(credential.accountId);
249
- const refresh = asNonemptyString(credential.refresh);
250
- if (!accountId || accountId !== resolvedAccountId || !refresh) {
251
- sawInvalidAccountId = true;
252
- continue;
253
- }
254
- matches.set(refresh, { accountId, storedAccess });
255
- } catch {
256
- // Malformed candidates never authorize a reset request.
257
- }
258
- }
259
- if (sawInvalidAccountId) {
260
- throw new Error("The OpenAI Codex OAuth credential did not include a valid account ID.");
261
- }
262
- if (matches.size > 1) {
263
- throw new Error("Conflicting OAuth credentials match the active OpenAI Codex runtime account.");
264
- }
265
- const match = matches.values().next().value;
266
- if (match) return match;
267
- if (!sawOAuth) {
268
- throw new Error(
269
- standaloneFallback
270
- ? "Usage limit resets require the OpenAI Codex OAuth account configured through Pi /login."
271
- : "Usage limit resets require an OpenAI Codex OAuth account configured through Pi /login or a compatible credential source.",
272
- );
273
- }
274
- if (sawMatchingAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
275
- throw new Error(
276
- standaloneFallback
277
- ? "The active OpenAI Codex runtime account does not match Pi's stored OAuth account."
278
- : "The active OpenAI Codex runtime account does not match any available OAuth account.",
279
- );
218
+ let sawOAuth = false;
219
+ let sawMatchingAccess = false;
220
+ let sawInvalidAccountId = false;
221
+ const matches = new Map<string, { accountId: string; storedAccess: string }>();
222
+ for (const candidate of candidates) {
223
+ try {
224
+ const credential = asObject(candidate);
225
+ if (credential?.type !== "oauth") continue;
226
+ sawOAuth = true;
227
+ const storedAccess = asNonemptyString(credential.access);
228
+ if (storedAccess !== resolvedAccess) continue;
229
+ sawMatchingAccess = true;
230
+ const accountId = validHeaderValue(credential.accountId);
231
+ const refresh = asNonemptyString(credential.refresh);
232
+ if (!accountId || accountId !== resolvedAccountId || !refresh) {
233
+ sawInvalidAccountId = true;
234
+ continue;
235
+ }
236
+ matches.set(refresh, { accountId, storedAccess });
237
+ } catch {
238
+ // Malformed candidates never authorize a reset request.
239
+ }
240
+ }
241
+ if (sawInvalidAccountId) {
242
+ throw new Error("The OpenAI Codex OAuth credential did not include a valid account ID.");
243
+ }
244
+ if (matches.size > 1) {
245
+ throw new Error("Conflicting OAuth credentials match the active OpenAI Codex runtime account.");
246
+ }
247
+ const match = matches.values().next().value;
248
+ if (match) return match;
249
+ if (!sawOAuth) {
250
+ throw new Error(
251
+ standaloneFallback
252
+ ? "Usage limit resets require the OpenAI Codex OAuth account configured through Pi /login."
253
+ : "Usage limit resets require an OpenAI Codex OAuth account configured through Pi /login or a compatible credential source.",
254
+ );
255
+ }
256
+ if (sawMatchingAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
257
+ throw new Error(
258
+ standaloneFallback
259
+ ? "The active OpenAI Codex runtime account does not match Pi's stored OAuth account."
260
+ : "The active OpenAI Codex runtime account does not match any available OAuth account.",
261
+ );
280
262
  }
281
263
 
282
264
  function codexAccountIdFromAccessToken(access: string): string | undefined {
283
- try {
284
- const parts = access.split(".");
285
- if (parts.length !== 3 || !parts[1]) return undefined;
286
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as unknown;
287
- const claims = asObject(asObject(payload)?.["https://api.openai.com/auth"]);
288
- return validHeaderValue(claims?.chatgpt_account_id);
289
- } catch {
290
- return undefined;
291
- }
265
+ try {
266
+ const parts = access.split(".");
267
+ if (parts.length !== 3 || !parts[1]) return undefined;
268
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as unknown;
269
+ const claims = asObject(asObject(payload)?.["https://api.openai.com/auth"]);
270
+ return validHeaderValue(claims?.chatgpt_account_id);
271
+ } catch {
272
+ return undefined;
273
+ }
292
274
  }
293
275
 
294
276
  function normalizeResetOption(credit: Record<string, unknown>): CodexResetOption {
295
- const creditId = asOpaqueId(credit.id);
296
- if (!creditId) throw new Error("Codex reset credits response returned an invalid credit ID.");
297
- let expiresAt: number | undefined;
298
- if (credit.expires_at !== undefined && credit.expires_at !== null) {
299
- if (typeof credit.expires_at !== "string") {
300
- throw new Error("Codex reset credits response returned an invalid expiration time.");
301
- }
302
- const parsed = Date.parse(credit.expires_at);
303
- if (!Number.isFinite(parsed)) {
304
- throw new Error("Codex reset credits response returned an invalid expiration time.");
305
- }
306
- expiresAt = Math.floor(parsed / 1_000);
307
- }
308
- const title = displayString(credit.title) ?? "Full reset";
309
- const description = displayString(credit.description) ?? "Reset your current usage limits.";
310
- return {
311
- creditId,
312
- title,
313
- description,
314
- ...(expiresAt === undefined ? {} : { expiresAt }),
315
- };
277
+ const creditId = asOpaqueId(credit.id);
278
+ if (!creditId) throw new Error("Codex reset credits response returned an invalid credit ID.");
279
+ let expiresAt: number | undefined;
280
+ if (credit.expires_at !== undefined && credit.expires_at !== null) {
281
+ if (typeof credit.expires_at !== "string") {
282
+ throw new Error("Codex reset credits response returned an invalid expiration time.");
283
+ }
284
+ const parsed = Date.parse(credit.expires_at);
285
+ if (!Number.isFinite(parsed)) {
286
+ throw new Error("Codex reset credits response returned an invalid expiration time.");
287
+ }
288
+ expiresAt = Math.floor(parsed / 1_000);
289
+ }
290
+ const title = displayString(credit.title) ?? "Full reset";
291
+ const description = displayString(credit.description) ?? "Reset your current usage limits.";
292
+ return {
293
+ creditId,
294
+ title,
295
+ description,
296
+ ...(expiresAt === undefined ? {} : { expiresAt }),
297
+ };
316
298
  }
317
299
 
318
300
  function isCodexResetOutcomeCode(value: unknown): value is CodexResetOutcomeCode {
319
- return (
320
- value === "reset" ||
321
- value === "nothing_to_reset" ||
322
- value === "no_credit" ||
323
- value === "already_redeemed"
324
- );
301
+ return value === "reset" || value === "nothing_to_reset" || value === "no_credit" || value === "already_redeemed";
325
302
  }
326
303
 
327
304
  function asObject(value: unknown): Record<string, unknown> | undefined {
328
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
329
- return value as Record<string, unknown>;
305
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
306
+ return value as Record<string, unknown>;
330
307
  }
331
308
 
332
309
  function asNonemptyString(value: unknown): string | undefined {
333
- return typeof value === "string" && value.length > 0 ? value : undefined;
310
+ return typeof value === "string" && value.length > 0 ? value : undefined;
334
311
  }
335
312
 
336
313
  function asOpaqueId(value: unknown): string | undefined {
337
- if (typeof value !== "string" || value.length === 0 || value.length > MAX_CREDIT_ID_CHARS) {
338
- return undefined;
339
- }
340
- return value;
314
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_CREDIT_ID_CHARS) {
315
+ return undefined;
316
+ }
317
+ return value;
341
318
  }
342
319
 
343
320
  function displayString(value: unknown): string | undefined {
344
- if (typeof value !== "string") return undefined;
345
- return sanitizeDisplayText(value, 160) || undefined;
321
+ if (typeof value !== "string") return undefined;
322
+ return sanitizeDisplayText(value, 160) || undefined;
346
323
  }
347
324
 
348
325
  function validHeaderValue(value: unknown): string | undefined {
349
- if (typeof value !== "string" || !value || value.length > 512) return undefined;
350
- if (/[^\x20-\x7e]/u.test(value)) return undefined;
351
- return value;
326
+ if (typeof value !== "string" || !value || value.length > 512) return undefined;
327
+ if (/[^\x20-\x7e]/u.test(value)) return undefined;
328
+ return value;
352
329
  }
353
330
 
354
331
  function nonnegativeInteger(value: unknown): number | undefined {
355
- const parsed =
356
- typeof value === "number"
357
- ? value
358
- : typeof value === "string" && value.trim()
359
- ? Number(value)
360
- : Number.NaN;
361
- if (!Number.isSafeInteger(parsed) || parsed < 0) return undefined;
362
- return parsed;
332
+ const parsed =
333
+ typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
334
+ if (!Number.isSafeInteger(parsed) || parsed < 0) return undefined;
335
+ return parsed;
363
336
  }
364
337
 
365
338
  function bearerToken(authorization: string | undefined): string | undefined {
366
- return /^Bearer\s+(.+)$/iu.exec(authorization ?? "")?.[1];
339
+ return /^Bearer\s+(.+)$/iu.exec(authorization ?? "")?.[1];
367
340
  }
368
341
 
369
342
  function headerValue(headers: Record<string, string>, name: string): string | undefined {
370
- return Object.entries(headers).find(
371
- ([candidate]) => candidate.toLowerCase() === name.toLowerCase(),
372
- )?.[1];
343
+ return Object.entries(headers).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase())?.[1];
373
344
  }