@young1lin/dsh-gpt-sub 0.1.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 (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +268 -0
  4. package/client.js +908 -0
  5. package/cordis.patch.yml +44 -0
  6. package/lib/index.d.ts +52 -0
  7. package/lib/index.d.ts.map +1 -0
  8. package/lib/index.js +581 -0
  9. package/lib/index.js.map +1 -0
  10. package/lib/jwt.d.ts +15 -0
  11. package/lib/jwt.d.ts.map +1 -0
  12. package/lib/jwt.js +29 -0
  13. package/lib/jwt.js.map +1 -0
  14. package/lib/proxy-config.d.ts +91 -0
  15. package/lib/proxy-config.d.ts.map +1 -0
  16. package/lib/proxy-config.js +213 -0
  17. package/lib/proxy-config.js.map +1 -0
  18. package/lib/proxy-probe.d.ts +47 -0
  19. package/lib/proxy-probe.d.ts.map +1 -0
  20. package/lib/proxy-probe.js +51 -0
  21. package/lib/proxy-probe.js.map +1 -0
  22. package/lib/proxy-routing.d.ts +111 -0
  23. package/lib/proxy-routing.d.ts.map +1 -0
  24. package/lib/proxy-routing.js +171 -0
  25. package/lib/proxy-routing.js.map +1 -0
  26. package/lib/quota-route.d.ts +85 -0
  27. package/lib/quota-route.d.ts.map +1 -0
  28. package/lib/quota-route.js +105 -0
  29. package/lib/quota-route.js.map +1 -0
  30. package/lib/reset-credits.d.ts +64 -0
  31. package/lib/reset-credits.d.ts.map +1 -0
  32. package/lib/reset-credits.js +84 -0
  33. package/lib/reset-credits.js.map +1 -0
  34. package/lib/token-store.d.ts +107 -0
  35. package/lib/token-store.d.ts.map +1 -0
  36. package/lib/token-store.js +228 -0
  37. package/lib/token-store.js.map +1 -0
  38. package/lib/types.d.ts +18 -0
  39. package/lib/types.d.ts.map +1 -0
  40. package/lib/types.js +2 -0
  41. package/lib/types.js.map +1 -0
  42. package/lib/usage.d.ts +92 -0
  43. package/lib/usage.d.ts.map +1 -0
  44. package/lib/usage.js +106 -0
  45. package/lib/usage.js.map +1 -0
  46. package/package.json +82 -0
  47. package/src/index.ts +685 -0
  48. package/src/jwt.ts +26 -0
  49. package/src/proxy-config.ts +217 -0
  50. package/src/proxy-probe.ts +81 -0
  51. package/src/proxy-routing.ts +205 -0
  52. package/src/quota-route.ts +163 -0
  53. package/src/reset-credits.ts +128 -0
  54. package/src/token-store.ts +309 -0
  55. package/src/types.ts +17 -0
  56. package/src/usage.ts +154 -0
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Host-scoped proxy routing for the process-wide undici dispatcher.
3
+ *
4
+ * pi-ai issues its own requests through the global `fetch`, so this plugin
5
+ * cannot hand it a dispatcher the way it hands one to `TokenStore`. The only
6
+ * seam is undici's global dispatcher -- but replacing that wholesale would
7
+ * push every provider in the harness through the proxy, including endpoints
8
+ * that must not go near it.
9
+ *
10
+ * So route by hostname: the Codex hosts go through the proxy, everything else
11
+ * continues to whatever dispatcher was installed before. Reaching
12
+ * `chatgpt.com` from an unproxied address here answers 403 with a Cloudflare
13
+ * block page, which surfaces as an unreadable HTML body rather than an API
14
+ * error, so this is the difference between working and not.
15
+ *
16
+ * @module dsh-gpt-sub/proxy-routing
17
+ */
18
+ import { Dispatcher, ProxyAgent, getGlobalDispatcher, interceptors, setGlobalDispatcher } from 'undici';
19
+ /**
20
+ * Transport failures worth retrying: the link to this proxy drops connections
21
+ * at random, and each drop surfaces to DSH as a bare `fetch failed`.
22
+ *
23
+ * Measured direct to chatgpt.com through the same proxy, the failure is
24
+ * non-monotonic in payload size, so it is a random connection failure rather
25
+ * than a threshold -- which is exactly the shape a retry fixes.
26
+ */
27
+ const RETRYABLE_ERROR_CODES = [
28
+ 'ECONNRESET',
29
+ 'ECONNREFUSED',
30
+ 'ENOTFOUND',
31
+ 'ENETDOWN',
32
+ 'ENETUNREACH',
33
+ 'EHOSTDOWN',
34
+ 'EHOSTUNREACH',
35
+ 'EPIPE',
36
+ 'ETIMEDOUT',
37
+ 'UND_ERR_SOCKET',
38
+ 'UND_ERR_CONNECT_TIMEOUT',
39
+ ];
40
+ /**
41
+ * Hosts that must egress through the proxy: the Codex API and the OAuth token
42
+ * endpoint the refresh calls. Subdomains match too.
43
+ */
44
+ export const PROXIED_HOSTS = ['chatgpt.com', 'auth.openai.com', 'api.openai.com'];
45
+ /**
46
+ * Whether a hostname belongs to one of `hosts`, matching the host itself and
47
+ * any subdomain of it.
48
+ *
49
+ * @param hostname - the hostname to test.
50
+ * @param hosts - the suffixes to match against.
51
+ * @returns true when the hostname should be proxied.
52
+ */
53
+ export function shouldProxy(hostname, hosts = PROXIED_HOSTS) {
54
+ const lower = hostname.toLowerCase();
55
+ return hosts.some((host) => lower === host || lower.endsWith(`.${host}`));
56
+ }
57
+ /**
58
+ * A dispatcher that sends matching hosts to one delegate and everything else
59
+ * to another.
60
+ *
61
+ * Only the proxy delegate is owned: `close`/`destroy` never touch the
62
+ * fallback, because that dispatcher belongs to the host and outlives this
63
+ * plugin.
64
+ */
65
+ export class HostRoutingDispatcher extends Dispatcher {
66
+ #proxy;
67
+ #fallback;
68
+ #hosts;
69
+ constructor(proxy, fallback, hosts = PROXIED_HOSTS) {
70
+ super();
71
+ this.#proxy = proxy;
72
+ this.#fallback = fallback;
73
+ this.#hosts = hosts;
74
+ }
75
+ /**
76
+ * Route one request by its origin's hostname.
77
+ *
78
+ * An origin that cannot be parsed is sent to the fallback: defaulting to the
79
+ * proxy would silently divert unrelated traffic.
80
+ *
81
+ * @param options - undici dispatch options.
82
+ * @param handler - undici dispatch handler.
83
+ * @returns whatever the chosen delegate returns.
84
+ */
85
+ dispatch(options, handler) {
86
+ let hostname = '';
87
+ try {
88
+ const origin = options.origin;
89
+ hostname = new URL(typeof origin === 'string' ? origin : String(origin?.href ?? '')).hostname;
90
+ }
91
+ catch {
92
+ hostname = '';
93
+ }
94
+ const delegate = hostname !== '' && shouldProxy(hostname, this.#hosts) ? this.#proxy : this.#fallback;
95
+ return delegate.dispatch(options, handler);
96
+ }
97
+ /** Close only the proxy delegate; the fallback belongs to the host. */
98
+ async close() {
99
+ await this.#proxy.close();
100
+ }
101
+ /** Destroy only the proxy delegate; the fallback belongs to the host. */
102
+ async destroy() {
103
+ await this.#proxy.destroy();
104
+ }
105
+ }
106
+ /**
107
+ * Retry options that absorb a dropped connection without taking on any
108
+ * HTTP-level retry policy.
109
+ *
110
+ * `statusCodes: []` is deliberate: an upstream 429 or 500 carries a body DSH
111
+ * knows how to read and surface, and retrying here would swallow it. This
112
+ * dispatcher's job is narrow -- make the flaky link look reliable -- and every
113
+ * HTTP semantic stays with the harness.
114
+ *
115
+ * `POST` is listed even though undici omits it by default, because undici
116
+ * omits it for non-idempotency: a POST that may have been received must not be
117
+ * replayed. That does not apply to what is retried here. A connection error
118
+ * arrives before any response, and undici refuses to replay a request whose
119
+ * body was already consumed; a partially streamed response is caught by
120
+ * undici's own range check rather than silently concatenated. Model calls are
121
+ * POSTs, so without this the retry would never fire at all.
122
+ *
123
+ * @param maxRetries - how many extra attempts a single request may make.
124
+ * @returns options for undici's retry interceptor.
125
+ */
126
+ export function connectionRetryOptions(maxRetries) {
127
+ return {
128
+ maxRetries,
129
+ methods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'POST'],
130
+ statusCodes: [],
131
+ errorCodes: [...RETRYABLE_ERROR_CODES],
132
+ // 0.5s, 1s, 2s -- fast enough that a recovered connection still feels like
133
+ // one request, slow enough to let a flapping tunnel settle.
134
+ minTimeout: 500,
135
+ maxTimeout: 8_000,
136
+ timeoutFactor: 2,
137
+ };
138
+ }
139
+ /**
140
+ * Install host-scoped proxy routing as the global dispatcher.
141
+ *
142
+ * @param proxyUrl - the proxy to route the Codex hosts through.
143
+ * @param options - hosts to proxy and how hard to retry a dropped connection.
144
+ * @returns the proxy dispatcher and the function that restores the previous global.
145
+ */
146
+ export function installProxyRouting(proxyUrl, options = {}) {
147
+ const hosts = options.hosts ?? PROXIED_HOSTS;
148
+ const maxRetries = options.maxRetries ?? 3;
149
+ const proxy = new ProxyAgent(proxyUrl);
150
+ // `compose` returns a proxy over the same agent with only `dispatch`
151
+ // replaced, so `close()` on the result still closes the agent underneath --
152
+ // and only requests routed here are retried. Nothing else in the harness
153
+ // changes behaviour.
154
+ const dispatcher = maxRetries > 0 ? proxy.compose(interceptors.retry(connectionRetryOptions(maxRetries))) : proxy;
155
+ const previous = getGlobalDispatcher();
156
+ setGlobalDispatcher(new HostRoutingDispatcher(dispatcher, previous, hosts));
157
+ return {
158
+ dispatcher,
159
+ uninstall: async () => {
160
+ // Restore first, so nothing dispatches into a closing agent.
161
+ setGlobalDispatcher(previous);
162
+ try {
163
+ await proxy.close();
164
+ }
165
+ catch {
166
+ // Disposal must not hinge on the agent shutting down cleanly.
167
+ }
168
+ },
169
+ };
170
+ }
171
+ //# sourceMappingURL=proxy-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy-routing.js","sourceRoot":"","sources":["../src/proxy-routing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The host half of the quota panel: one cached JSON endpoint the browser polls.
3
+ *
4
+ * Every quota semantic lives here rather than in the client bundle -- which
5
+ * windows count, how often upstream may be asked, what a failure looks like --
6
+ * so the browser half stays a renderer.
7
+ *
8
+ * @module dsh-gpt-sub/quota-route
9
+ */
10
+ import type { Dispatcher } from 'undici';
11
+ import { type ReportedWindow } from './usage.ts';
12
+ /** One rate-limit window as the endpoint serves it to the panel. */
13
+ export interface QuotaWindowState {
14
+ /** Percent of the window consumed. */
15
+ usedPercent: number;
16
+ /** Length of the window, in hours. */
17
+ windowHours: number;
18
+ /** Epoch seconds at which the window resets. */
19
+ resetAt?: number;
20
+ }
21
+ /** What the endpoint serves. */
22
+ export interface QuotaState {
23
+ /** 'ready' once a reading has been taken, 'error' when none ever succeeded. */
24
+ phase: 'ready' | 'error';
25
+ /** Subscription plan, when upstream reported one. */
26
+ plan?: string;
27
+ /**
28
+ * The short rolling window -- 5 hours on every plan that reports one. A
29
+ * plan without it (pro currently) simply omits the field, and the panel
30
+ * says so instead of drawing an empty bar.
31
+ */
32
+ fiveHour?: QuotaWindowState;
33
+ /** The weekly window, alone or beside the 5-hour one. */
34
+ weekly?: QuotaWindowState;
35
+ /**
36
+ * On-demand usage resets the account can still spend, when the plan
37
+ * reports them -- each clears a capped window without waiting out its
38
+ * timer.
39
+ */
40
+ resetsRemaining?: number;
41
+ /** Every reported window, primary first; one panel row each. */
42
+ windows?: ReportedWindow[];
43
+ /** Epoch milliseconds this reading was taken. */
44
+ fetchedAt?: number;
45
+ /** True when the reading is older than the refresh interval and a retry failed. */
46
+ stale?: boolean;
47
+ /** Human-readable failure note; present on error, or beside a stale reading. */
48
+ message?: string;
49
+ }
50
+ /** Construction options. */
51
+ export interface QuotaSourceOptions {
52
+ /** Returns a live access token. */
53
+ readonly accessToken: () => Promise<string>;
54
+ /** Dispatcher for the upstream call. */
55
+ readonly dispatcher?: Dispatcher;
56
+ /** Minimum gap between upstream reads; a poll inside it is served from cache. */
57
+ readonly minIntervalMs?: number;
58
+ /** Injectable clock, defaulting to Date.now. */
59
+ readonly now?: () => number;
60
+ }
61
+ /**
62
+ * A throttled quota reader.
63
+ *
64
+ * The browser polls far more often than the account's usage changes, so an
65
+ * unthrottled endpoint would turn one open settings page into a steady stream
66
+ * of upstream requests.
67
+ */
68
+ export declare class QuotaSource {
69
+ #private;
70
+ constructor(options: QuotaSourceOptions);
71
+ /**
72
+ * Point later upstream reads at a different dispatcher, as a proxy switch does.
73
+ *
74
+ * @param dispatcher - the dispatcher future reads use; undefined for the global.
75
+ */
76
+ setDispatcher(dispatcher: Dispatcher | undefined): void;
77
+ /**
78
+ * Return the current reading, refreshing when the throttle allows.
79
+ *
80
+ * @param force - ignore the throttle, as the panel's refresh button does.
81
+ * @returns the reading to serve.
82
+ */
83
+ read(force?: boolean): Promise<QuotaState>;
84
+ }
85
+ //# sourceMappingURL=quota-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-route.d.ts","sourceRoot":"","sources":["../src/quota-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACxC,OAAO,EAA+D,KAAK,cAAc,EAAoB,MAAM,YAAY,CAAA;AAE/H,oEAAoE;AACpE,MAAM,WAAW,gBAAgB;IAC/B,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,gCAAgC;AAChC,MAAM,WAAW,UAAU;IACzB,+EAA+E;IAC/E,KAAK,EAAE,OAAO,GAAG,OAAO,CAAA;IACxB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B,yDAAyD;IACzD,MAAM,CAAC,EAAE,gBAAgB,CAAA;IACzB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,gEAAgE;IAChE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAA;IAC1B,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mFAAmF;IACnF,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAiBD,4BAA4B;AAC5B,MAAM,WAAW,kBAAkB;IACjC,mCAAmC;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3C,wCAAwC;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;IAChC,iFAAiF;IACjF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,qBAAa,WAAW;;gBAUV,OAAO,EAAE,kBAAkB;IAOvC;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI;IAIvD;;;;;OAKG;IACG,IAAI,CAAC,KAAK,UAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;CA2C/C"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The host half of the quota panel: one cached JSON endpoint the browser polls.
3
+ *
4
+ * Every quota semantic lives here rather than in the client bundle -- which
5
+ * windows count, how often upstream may be asked, what a failure looks like --
6
+ * so the browser half stays a renderer.
7
+ *
8
+ * @module dsh-gpt-sub/quota-route
9
+ */
10
+ import { fetchUsage, reportWindows, reportedWindows, resetsRemaining } from "./usage.js";
11
+ /** Windows shorter than a day are the 5-hour-style rolling limit. */
12
+ const DAY_SECONDS = 24 * 3600;
13
+ /**
14
+ * Fold one upstream window into the shape the panel renders.
15
+ *
16
+ * @param window - a window as the usage endpoint reported it.
17
+ * @returns the same figures in the panel's vocabulary.
18
+ */
19
+ const toWindowState = (window) => ({
20
+ usedPercent: window.used_percent,
21
+ windowHours: Math.round(window.limit_window_seconds / 3600),
22
+ ...(window.reset_at === undefined ? {} : { resetAt: window.reset_at }),
23
+ });
24
+ /**
25
+ * A throttled quota reader.
26
+ *
27
+ * The browser polls far more often than the account's usage changes, so an
28
+ * unthrottled endpoint would turn one open settings page into a steady stream
29
+ * of upstream requests.
30
+ */
31
+ export class QuotaSource {
32
+ #accessToken;
33
+ #dispatcher;
34
+ #minIntervalMs;
35
+ #now;
36
+ #state = { phase: 'error', message: 'not read yet' };
37
+ #lastAttempt = 0;
38
+ /** The in-flight read, shared by every caller that arrives during it. */
39
+ #inFlight;
40
+ constructor(options) {
41
+ this.#accessToken = options.accessToken;
42
+ this.#dispatcher = options.dispatcher;
43
+ this.#minIntervalMs = options.minIntervalMs ?? 60_000;
44
+ this.#now = options.now ?? (() => Date.now());
45
+ }
46
+ /**
47
+ * Point later upstream reads at a different dispatcher, as a proxy switch does.
48
+ *
49
+ * @param dispatcher - the dispatcher future reads use; undefined for the global.
50
+ */
51
+ setDispatcher(dispatcher) {
52
+ this.#dispatcher = dispatcher;
53
+ }
54
+ /**
55
+ * Return the current reading, refreshing when the throttle allows.
56
+ *
57
+ * @param force - ignore the throttle, as the panel's refresh button does.
58
+ * @returns the reading to serve.
59
+ */
60
+ async read(force = false) {
61
+ const elapsed = this.#now() - this.#lastAttempt;
62
+ if (!force && this.#state.phase === 'ready' && elapsed < this.#minIntervalMs)
63
+ return this.#state;
64
+ this.#inFlight ??= this.#refresh().finally(() => {
65
+ this.#inFlight = undefined;
66
+ });
67
+ return this.#inFlight;
68
+ }
69
+ /**
70
+ * Take one upstream reading and fold it into the cached state.
71
+ *
72
+ * A failure never discards a previous good reading: the panel shows the last
73
+ * known figure marked stale, which is more useful than an empty panel.
74
+ *
75
+ * @returns the new state.
76
+ */
77
+ async #refresh() {
78
+ this.#lastAttempt = this.#now();
79
+ try {
80
+ const usage = await fetchUsage(await this.#accessToken(), this.#dispatcher);
81
+ const reported = reportedWindows(usage);
82
+ const fiveHour = reported.find((window) => window.limit_window_seconds < DAY_SECONDS);
83
+ const weekly = reported.find((window) => window.limit_window_seconds >= DAY_SECONDS);
84
+ const resets = resetsRemaining(usage);
85
+ const windows = reportWindows(usage);
86
+ this.#state = {
87
+ phase: 'ready',
88
+ ...(usage.plan_type === undefined ? {} : { plan: usage.plan_type }),
89
+ ...(fiveHour === undefined ? {} : { fiveHour: toWindowState(fiveHour) }),
90
+ ...(weekly === undefined ? {} : { weekly: toWindowState(weekly) }),
91
+ ...(resets === undefined ? {} : { resetsRemaining: resets }),
92
+ ...(windows.length === 0 ? {} : { windows }),
93
+ };
94
+ }
95
+ catch (error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ this.#state =
98
+ this.#state.phase === 'ready'
99
+ ? { ...this.#state, stale: true, message }
100
+ : { phase: 'error', message };
101
+ }
102
+ return this.#state;
103
+ }
104
+ }
105
+ //# sourceMappingURL=quota-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota-route.js","sourceRoot":"","sources":["../src/quota-route.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Rate-limit reset credits: list the redeemable credits on the account and
3
+ * consume one, against the same wham host the usage endpoint lives on.
4
+ *
5
+ * Mirrors the codex CLI's backend-client contract
6
+ * (codex-rs/backend-client/src/client/rate_limit_resets.rs): GET
7
+ * .../rate-limit-reset-credits lists credits, POST .../consume redeems one
8
+ * with a caller-chosen idempotency key (`redeem_request_id`) and an optional
9
+ * `credit_id`. Same Bearer auth as every other wham call.
10
+ *
11
+ * @module dsh-gpt-sub/reset-credits
12
+ */
13
+ import { fetch as undiciFetch, type Dispatcher } from 'undici';
14
+ /** One redeemable credit as the list endpoint reports it. */
15
+ export interface ResetCredit {
16
+ id: string;
17
+ reset_type: string;
18
+ status: string;
19
+ granted_at: string;
20
+ expires_at?: string;
21
+ title?: string;
22
+ description?: string;
23
+ }
24
+ /** The list endpoint's reply. */
25
+ export interface ResetCreditsDetails {
26
+ credits: ResetCredit[];
27
+ available_count: number;
28
+ }
29
+ /** The consume endpoint's machine-readable outcomes. */
30
+ export type ConsumeCode = 'reset' | 'nothing_to_reset' | 'no_credit' | 'already_redeemed';
31
+ /** The consume endpoint's reply. */
32
+ export interface ConsumeReply {
33
+ code: ConsumeCode;
34
+ windows_reset?: number;
35
+ }
36
+ /**
37
+ * List the account's rate-limit reset credits.
38
+ *
39
+ * @param accessToken - a live Codex access token.
40
+ * @param dispatcher - undici dispatcher; omit to use the global one.
41
+ * @param fetchImpl - injectable fetch, for tests.
42
+ * @param signal - abort signal bounding the call.
43
+ * @returns the parsed credit list.
44
+ */
45
+ export declare function listResetCredits(accessToken: string, dispatcher?: Dispatcher, fetchImpl?: typeof undiciFetch, signal?: AbortSignal): Promise<ResetCreditsDetails>;
46
+ /**
47
+ * Consume one rate-limit reset credit, resetting the eligible window.
48
+ *
49
+ * @param accessToken - a live Codex access token.
50
+ * @param redeemRequestId - caller-chosen idempotency key; retries with the same
51
+ * key cannot double-redeem.
52
+ * @param creditId - consume this specific credit; omit for any available one.
53
+ * @param dispatcher - undici dispatcher; omit to use the global one.
54
+ * @param fetchImpl - injectable fetch, for tests.
55
+ * @param signal - abort signal bounding the call.
56
+ * @returns the outcome and how many windows were reset.
57
+ */
58
+ export declare function consumeResetCredit(accessToken: string, redeemRequestId: string, options?: {
59
+ creditId?: string;
60
+ dispatcher?: Dispatcher;
61
+ fetchImpl?: typeof undiciFetch;
62
+ signal?: AbortSignal;
63
+ }): Promise<ConsumeReply>;
64
+ //# sourceMappingURL=reset-credits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-credits.d.ts","sourceRoot":"","sources":["../src/reset-credits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,IAAI,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAA;AAE9D,6DAA6D;AAC7D,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,iCAAiC;AACjC,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,wDAAwD;AACxD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,kBAAkB,GAAG,WAAW,GAAG,kBAAkB,CAAA;AAEzF,oCAAoC;AACpC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AA2BD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,UAAU,EACvB,SAAS,GAAE,OAAO,WAAyB,EAC3C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,mBAAmB,CAAC,CAO9B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,WAAW,CAAA;IAC9B,MAAM,CAAC,EAAE,WAAW,CAAA;CAChB,GACL,OAAO,CAAC,YAAY,CAAC,CAiBvB"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Rate-limit reset credits: list the redeemable credits on the account and
3
+ * consume one, against the same wham host the usage endpoint lives on.
4
+ *
5
+ * Mirrors the codex CLI's backend-client contract
6
+ * (codex-rs/backend-client/src/client/rate_limit_resets.rs): GET
7
+ * .../rate-limit-reset-credits lists credits, POST .../consume redeems one
8
+ * with a caller-chosen idempotency key (`redeem_request_id`) and an optional
9
+ * `credit_id`. Same Bearer auth as every other wham call.
10
+ *
11
+ * @module dsh-gpt-sub/reset-credits
12
+ */
13
+ import { fetch as undiciFetch } from 'undici';
14
+ /** The list endpoint, on the same host as the model calls. */
15
+ const CREDITS_URL = 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits';
16
+ /** The consume endpoint, beside the list one. */
17
+ const CONSUME_URL = CREDITS_URL + '/consume';
18
+ /**
19
+ * Decode a wham JSON reply or throw with the status attached.
20
+ *
21
+ * @param response - the undici response.
22
+ * @param what - names the endpoint in error messages.
23
+ * @returns the parsed body.
24
+ */
25
+ async function decode(response, what) {
26
+ const body = await response.text();
27
+ if (!response.ok) {
28
+ throw new Error(`${what} answered ${String(response.status)}: ${body.slice(0, 200)}`);
29
+ }
30
+ try {
31
+ return JSON.parse(body);
32
+ }
33
+ catch {
34
+ throw new Error(`${what} did not answer with JSON`);
35
+ }
36
+ }
37
+ /**
38
+ * List the account's rate-limit reset credits.
39
+ *
40
+ * @param accessToken - a live Codex access token.
41
+ * @param dispatcher - undici dispatcher; omit to use the global one.
42
+ * @param fetchImpl - injectable fetch, for tests.
43
+ * @param signal - abort signal bounding the call.
44
+ * @returns the parsed credit list.
45
+ */
46
+ export async function listResetCredits(accessToken, dispatcher, fetchImpl = undiciFetch, signal) {
47
+ const response = await fetchImpl(CREDITS_URL, {
48
+ headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json' },
49
+ ...(dispatcher === undefined ? {} : { dispatcher }),
50
+ ...(signal === undefined ? {} : { signal }),
51
+ });
52
+ return decode(response, 'reset credits list');
53
+ }
54
+ /**
55
+ * Consume one rate-limit reset credit, resetting the eligible window.
56
+ *
57
+ * @param accessToken - a live Codex access token.
58
+ * @param redeemRequestId - caller-chosen idempotency key; retries with the same
59
+ * key cannot double-redeem.
60
+ * @param creditId - consume this specific credit; omit for any available one.
61
+ * @param dispatcher - undici dispatcher; omit to use the global one.
62
+ * @param fetchImpl - injectable fetch, for tests.
63
+ * @param signal - abort signal bounding the call.
64
+ * @returns the outcome and how many windows were reset.
65
+ */
66
+ export async function consumeResetCredit(accessToken, redeemRequestId, options = {}) {
67
+ const { creditId, dispatcher, fetchImpl = undiciFetch, signal } = options;
68
+ const response = await fetchImpl(CONSUME_URL, {
69
+ method: 'POST',
70
+ headers: {
71
+ authorization: `Bearer ${accessToken}`,
72
+ 'content-type': 'application/json',
73
+ accept: 'application/json',
74
+ },
75
+ body: JSON.stringify({
76
+ redeem_request_id: redeemRequestId,
77
+ ...(creditId === undefined ? {} : { credit_id: creditId }),
78
+ }),
79
+ ...(dispatcher === undefined ? {} : { dispatcher }),
80
+ ...(signal === undefined ? {} : { signal }),
81
+ });
82
+ return decode(response, 'reset credit consume');
83
+ }
84
+ //# sourceMappingURL=reset-credits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-credits.js","sourceRoot":"","sources":["../src/reset-credits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,IAAI,WAAW,EAAmB,MAAM,QAAQ,CAAA;AA4B9D,8DAA8D;AAC9D,MAAM,WAAW,GAAG,+DAA+D,CAAA;AAEnF,iDAAiD;AACjD,MAAM,WAAW,GAAG,WAAW,GAAG,UAAU,CAAA;AAE5C;;;;;;GAMG;AACH,KAAK,UAAU,MAAM,CAAI,QAAkB,EAAE,IAAY;IACvD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;IAClC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,aAAa,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;IACvF,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,2BAA2B,CAAC,CAAA;IACrD,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,WAAmB,EACnB,UAAuB,EACvB,YAAgC,WAAW,EAC3C,MAAoB;IAEpB,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,WAAW,EAAE;QAC5C,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,WAAW,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC/E,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC;QACnD,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;KAC5C,CAAC,CAAA;IACF,OAAO,MAAM,CAAsB,QAAQ,EAAE,oBAAoB,CAAC,CAAA;AACpE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,WAAmB,EACnB,eAAuB,EACvB,UAKI,EAAE;IAEN,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IACzE,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,WAAW,EAAE;QAC5C,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,WAAW,EAAE;YACtC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,kBAAkB;SAC3B;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,iBAAiB,EAAE,eAAe;YAClC,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;SAC3D,CAAC;QACF,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC;QACnD,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;KAC5C,CAAC,CAAA;IACF,OAAO,MAAM,CAAe,QAAQ,EAAE,sBAAsB,CAAC,CAAA;AAC/D,CAAC"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Codex OAuth credential access. Reads the Codex CLI's own auth.json so both
3
+ * tools share one login, and refreshes the access token before it expires.
4
+ *
5
+ * Expiry is judged solely from the access_token's exp claim. The id_token
6
+ * carries identity claims, lives one hour, and is routinely expired while the
7
+ * access token remains valid for days -- treating it as an expiry signal would
8
+ * trigger a refresh on nearly every request.
9
+ *
10
+ * @module dsh-gpt-sub/token-store
11
+ */
12
+ /** The subset of a token-endpoint response the refresh path reads. */
13
+ export interface RefreshResponse {
14
+ ok: boolean;
15
+ status: number;
16
+ text: () => Promise<string>;
17
+ json: () => Promise<unknown>;
18
+ }
19
+ /**
20
+ * The injectable token-endpoint call; undici's `fetch` satisfies this shape.
21
+ *
22
+ * The global `fetch` is deliberately not used: it has no `dispatcher` option,
23
+ * so a refresh through it ignores the proxy and cannot connect on a machine
24
+ * that reaches OpenAI only through one.
25
+ */
26
+ export type RefreshFetch = (url: string, init: {
27
+ method: string;
28
+ headers: Record<string, string>;
29
+ body: string;
30
+ dispatcher?: unknown;
31
+ }) => Promise<RefreshResponse>;
32
+ /** What {@link TokenStore.inspect} reports: file-only facts, no network. */
33
+ export interface TokenInspection {
34
+ /** Access-token expiry in epoch ms, when the JWT carries a decodable exp. */
35
+ accessTokenExpiresAt?: number;
36
+ }
37
+ /** The credential fields the shim needs for one upstream request. */
38
+ export interface CodexTokens {
39
+ accessToken: string;
40
+ refreshToken: string;
41
+ accountId: string;
42
+ }
43
+ /** Construction options; the injectable seams exist for tests. */
44
+ export interface TokenStoreOptions {
45
+ /** Path to the Codex CLI credential file. */
46
+ authFile: string;
47
+ /** Refresh once the access token has less than this many ms of life left. */
48
+ refreshMarginMs: number;
49
+ /** undici Dispatcher (the same ProxyAgent the plugin's routing uses, in production). */
50
+ dispatcher?: unknown;
51
+ /** Injectable token-endpoint call, defaulting to undici's fetch. */
52
+ fetchImpl?: RefreshFetch;
53
+ /** Injectable clock, defaulting to Date.now. */
54
+ now?: () => number;
55
+ }
56
+ export declare class TokenStore {
57
+ #private;
58
+ constructor(options: TokenStoreOptions);
59
+ /**
60
+ * Return usable credentials, refreshing first when the access token is
61
+ * inside the configured margin.
62
+ *
63
+ * @returns credentials valid at the moment of the call.
64
+ */
65
+ getTokens(): Promise<CodexTokens>;
66
+ /**
67
+ * Report the current access token's expiry without touching the network.
68
+ *
69
+ * Reads and parses the file only -- no refresh, no matter how close to the
70
+ * margin the token is -- so the status endpoint can show when the next
71
+ * automatic refresh will happen without triggering it.
72
+ *
73
+ * @returns file-derived facts about the stored credentials.
74
+ */
75
+ inspect(): Promise<TokenInspection>;
76
+ /**
77
+ * Point later reads at a different credential file, as a page-side switch
78
+ * does. The caller validates the new path before switching.
79
+ *
80
+ * @param authFile - the replacement credential file's path.
81
+ */
82
+ setAuthFile(authFile: string): void;
83
+ /**
84
+ * Point later refreshes at a different dispatcher, as a proxy switch does.
85
+ *
86
+ * @param dispatcher - the dispatcher future refresh calls use; undefined for the global.
87
+ */
88
+ setDispatcher(dispatcher: unknown): void;
89
+ /**
90
+ * Check that the credential file is readable and carries the fields the shim
91
+ * needs, without spending a refresh on it.
92
+ *
93
+ * Called at plugin start so a missing or unparseable auth.json fails there
94
+ * naming the path, as the spec's error table requires, instead of surfacing
95
+ * on the first request. Deliberately does not call getTokens(): that would
96
+ * put a network refresh on the startup path.
97
+ */
98
+ verify(): Promise<void>;
99
+ /**
100
+ * Refresh regardless of remaining lifetime. Used when the upstream rejects a
101
+ * token that local expiry math believed was still good.
102
+ *
103
+ * @returns freshly issued credentials.
104
+ */
105
+ forceRefresh(): Promise<CodexTokens>;
106
+ }
107
+ //# sourceMappingURL=token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../src/token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAYH,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3B,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,KAC1F,OAAO,CAAC,eAAe,CAAC,CAAA;AAE7B,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAA;IAChB,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAA;IACvB,wFAAwF;IACxF,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,oEAAoE;IACpE,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAcD,qBAAa,UAAU;;gBAST,OAAO,EAAE,iBAAiB;IAUtC;;;;;OAKG;IACG,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC;IAUvC;;;;;;;;OAQG;IACG,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC;IAMzC;;;;;OAKG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAInC;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,OAAO,GAAG,IAAI;IAIxC;;;;;;;;OAQG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAgC7B;;;;;OAKG;IACG,YAAY,IAAI,OAAO,CAAC,WAAW,CAAC;CA+G3C"}