@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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/client.js +908 -0
- package/cordis.patch.yml +44 -0
- package/lib/index.d.ts +52 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +581 -0
- package/lib/index.js.map +1 -0
- package/lib/jwt.d.ts +15 -0
- package/lib/jwt.d.ts.map +1 -0
- package/lib/jwt.js +29 -0
- package/lib/jwt.js.map +1 -0
- package/lib/proxy-config.d.ts +91 -0
- package/lib/proxy-config.d.ts.map +1 -0
- package/lib/proxy-config.js +213 -0
- package/lib/proxy-config.js.map +1 -0
- package/lib/proxy-probe.d.ts +47 -0
- package/lib/proxy-probe.d.ts.map +1 -0
- package/lib/proxy-probe.js +51 -0
- package/lib/proxy-probe.js.map +1 -0
- package/lib/proxy-routing.d.ts +111 -0
- package/lib/proxy-routing.d.ts.map +1 -0
- package/lib/proxy-routing.js +171 -0
- package/lib/proxy-routing.js.map +1 -0
- package/lib/quota-route.d.ts +85 -0
- package/lib/quota-route.d.ts.map +1 -0
- package/lib/quota-route.js +105 -0
- package/lib/quota-route.js.map +1 -0
- package/lib/reset-credits.d.ts +64 -0
- package/lib/reset-credits.d.ts.map +1 -0
- package/lib/reset-credits.js +84 -0
- package/lib/reset-credits.js.map +1 -0
- package/lib/token-store.d.ts +107 -0
- package/lib/token-store.d.ts.map +1 -0
- package/lib/token-store.js +228 -0
- package/lib/token-store.js.map +1 -0
- package/lib/types.d.ts +18 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/usage.d.ts +92 -0
- package/lib/usage.d.ts.map +1 -0
- package/lib/usage.js +106 -0
- package/lib/usage.js.map +1 -0
- package/package.json +82 -0
- package/src/index.ts +685 -0
- package/src/jwt.ts +26 -0
- package/src/proxy-config.ts +217 -0
- package/src/proxy-probe.ts +81 -0
- package/src/proxy-routing.ts +205 -0
- package/src/quota-route.ts +163 -0
- package/src/reset-credits.ts +128 -0
- package/src/token-store.ts +309 -0
- package/src/types.ts +17 -0
- package/src/usage.ts +154 -0
package/lib/jwt.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JWT claim reader. Only the `exp` claim is needed, and signature
|
|
3
|
+
* verification is deliberately absent: these tokens are read from the user's
|
|
4
|
+
* own credential file, never accepted from a remote party.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-gpt-sub/jwt
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Read a JWT's expiry.
|
|
10
|
+
*
|
|
11
|
+
* @param token - a candidate JWT; opaque strings are tolerated.
|
|
12
|
+
* @returns expiry in epoch milliseconds, or undefined when absent or undecodable.
|
|
13
|
+
*/
|
|
14
|
+
export function jwtExpiryMs(token) {
|
|
15
|
+
const payload = token.split('.')[1];
|
|
16
|
+
if (payload === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
try {
|
|
19
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
20
|
+
if (typeof claims !== 'object' || claims === null)
|
|
21
|
+
return undefined;
|
|
22
|
+
const exp = claims.exp;
|
|
23
|
+
return typeof exp === 'number' ? exp * 1000 : undefined;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=jwt.js.map
|
package/lib/jwt.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.js","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IACnC,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC3C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;QACtF,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,SAAS,CAAA;QACnE,MAAM,GAAG,GAAI,MAA4B,CAAC,GAAG,CAAA;QAC7C,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime proxy configuration: request-body reading, URL validation, and the
|
|
3
|
+
* persisted override the settings panel writes.
|
|
4
|
+
*
|
|
5
|
+
* The override file exists because cordis config layers are the loader's
|
|
6
|
+
* property -- a panel edit cannot rewrite them. The file holds the panel's
|
|
7
|
+
* last saved choice; when present, it wins over the config's proxyUrl so a
|
|
8
|
+
* page edit survives restarts.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-gpt-sub/proxy-config
|
|
11
|
+
*/
|
|
12
|
+
import type { IncomingMessage } from 'node:http';
|
|
13
|
+
/**
|
|
14
|
+
* Validate a caller-supplied proxy URL.
|
|
15
|
+
*
|
|
16
|
+
* @param value - the candidate value from a request body.
|
|
17
|
+
* @returns the trimmed URL, or the empty string for a direct connection.
|
|
18
|
+
* @throws Error when the value is not a string or not an http(s) URL.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateProxyUrl(value: unknown): string;
|
|
21
|
+
/**
|
|
22
|
+
* Mask any userinfo credentials in a proxy URL, for logging.
|
|
23
|
+
*
|
|
24
|
+
* The URL itself is worth logging; the password inside it is not. A URL
|
|
25
|
+
* without credentials is returned exactly as given.
|
|
26
|
+
*
|
|
27
|
+
* @param url - the proxy URL as configured.
|
|
28
|
+
* @returns the URL with its userinfo, if any, replaced by `***`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function redactProxyUrl(url: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Whether a request's Origin header names an origin other than the host the
|
|
33
|
+
* request was addressed to.
|
|
34
|
+
*
|
|
35
|
+
* The mutating routes must refuse such requests: a page on another origin can
|
|
36
|
+
* otherwise make the harness reroute its credentials through a proxy of the
|
|
37
|
+
* page's choosing. An absent Origin passes -- browsers always set one on a
|
|
38
|
+
* POST, so absence means a non-browser client such as curl.
|
|
39
|
+
*
|
|
40
|
+
* @param request - the incoming request.
|
|
41
|
+
* @returns true when Origin is present and names another host.
|
|
42
|
+
*/
|
|
43
|
+
export declare function crossOrigin(request: IncomingMessage): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Read one JSON object request body.
|
|
46
|
+
*
|
|
47
|
+
* A body that declares a content-type must declare JSON: browsers refuse to
|
|
48
|
+
* send `application/json` cross-origin without a preflight, so the check
|
|
49
|
+
* turns every form a forged cross-site POST can take into a refusal. An
|
|
50
|
+
* absent content-type passes for non-browser clients that send none.
|
|
51
|
+
*
|
|
52
|
+
* @param request - the incoming request whose body to consume.
|
|
53
|
+
* @param limitBytes - the maximum body size accepted.
|
|
54
|
+
* @returns the parsed object.
|
|
55
|
+
* @throws Error when the body is oversized, unparseable, not an object, or
|
|
56
|
+
* not JSON-content-typed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readJsonObject(request: IncomingMessage, limitBytes?: number): Promise<Record<string, unknown>>;
|
|
59
|
+
/** The state file's on-disk shape; each field stands alone. */
|
|
60
|
+
export interface StateOverride {
|
|
61
|
+
/** The saved proxy URL; empty string means direct. */
|
|
62
|
+
proxyUrl?: string;
|
|
63
|
+
/** The saved credential-file path, exactly as typed on the page. */
|
|
64
|
+
authFile?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load the persisted panel overrides.
|
|
68
|
+
*
|
|
69
|
+
* An absent file simply means "no override". A corrupt file is ignored too:
|
|
70
|
+
* the panel that wrote it may have died mid-write, and failing the whole
|
|
71
|
+
* plugin over a status file would trade working settings for a stale byte.
|
|
72
|
+
*
|
|
73
|
+
* Each known field validates on its own, so one invalid value drops without
|
|
74
|
+
* discarding the other choice the panel made.
|
|
75
|
+
*
|
|
76
|
+
* @param path - the state file's path.
|
|
77
|
+
* @returns the saved choices, or undefined when there is no readable file.
|
|
78
|
+
*/
|
|
79
|
+
export declare function loadStateOverride(path: string): Promise<StateOverride | undefined>;
|
|
80
|
+
/**
|
|
81
|
+
* Persist the panel's choices atomically, so a crash never leaves a half write.
|
|
82
|
+
*
|
|
83
|
+
* The object is written exactly as given; callers read-modify-write so saving
|
|
84
|
+
* one choice never erases the other.
|
|
85
|
+
*
|
|
86
|
+
* @param path - the state file's path.
|
|
87
|
+
* @param state - the choices to persist; absent fields mean "use the config".
|
|
88
|
+
* @returns nothing; failures propagate to the caller.
|
|
89
|
+
*/
|
|
90
|
+
export declare function saveStateOverride(path: string, state: StateOverride): Promise<void>;
|
|
91
|
+
//# sourceMappingURL=proxy-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-config.d.ts","sourceRoot":"","sources":["../src/proxy-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAahD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAcvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUlD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAY7D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,eAAe,EACxB,UAAU,SAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA0BlC;AAED,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CA+BxF;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAYzF"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime proxy configuration: request-body reading, URL validation, and the
|
|
3
|
+
* persisted override the settings panel writes.
|
|
4
|
+
*
|
|
5
|
+
* The override file exists because cordis config layers are the loader's
|
|
6
|
+
* property -- a panel edit cannot rewrite them. The file holds the panel's
|
|
7
|
+
* last saved choice; when present, it wins over the config's proxyUrl so a
|
|
8
|
+
* page edit survives restarts.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-gpt-sub/proxy-config
|
|
11
|
+
*/
|
|
12
|
+
import { readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
13
|
+
/** Bodies larger than this are refused rather than buffered. */
|
|
14
|
+
const BODY_LIMIT_BYTES = 65_536;
|
|
15
|
+
/** Read one header, tolerating the array form node uses for repeated headers. */
|
|
16
|
+
function header(request, name) {
|
|
17
|
+
const value = request.headers?.[name];
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value[0];
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate a caller-supplied proxy URL.
|
|
24
|
+
*
|
|
25
|
+
* @param value - the candidate value from a request body.
|
|
26
|
+
* @returns the trimmed URL, or the empty string for a direct connection.
|
|
27
|
+
* @throws Error when the value is not a string or not an http(s) URL.
|
|
28
|
+
*/
|
|
29
|
+
export function validateProxyUrl(value) {
|
|
30
|
+
if (typeof value !== 'string')
|
|
31
|
+
throw new Error('proxyUrl must be a string');
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
if (trimmed === '')
|
|
34
|
+
return '';
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = new URL(trimmed);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error(`proxyUrl is not a valid URL: ${trimmed}`);
|
|
41
|
+
}
|
|
42
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
43
|
+
throw new Error(`proxyUrl must be http or https, got ${parsed.protocol}`);
|
|
44
|
+
}
|
|
45
|
+
return trimmed;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Mask any userinfo credentials in a proxy URL, for logging.
|
|
49
|
+
*
|
|
50
|
+
* The URL itself is worth logging; the password inside it is not. A URL
|
|
51
|
+
* without credentials is returned exactly as given.
|
|
52
|
+
*
|
|
53
|
+
* @param url - the proxy URL as configured.
|
|
54
|
+
* @returns the URL with its userinfo, if any, replaced by `***`.
|
|
55
|
+
*/
|
|
56
|
+
export function redactProxyUrl(url) {
|
|
57
|
+
if (url === '')
|
|
58
|
+
return '';
|
|
59
|
+
const scheme = /^https?:\/\//.exec(url);
|
|
60
|
+
if (scheme === null)
|
|
61
|
+
return url;
|
|
62
|
+
const rest = url.slice(scheme[0].length);
|
|
63
|
+
const credentialsEnd = rest.indexOf('@');
|
|
64
|
+
if (credentialsEnd === -1)
|
|
65
|
+
return url;
|
|
66
|
+
// Replace the userinfo by string surgery rather than URL re-serialisation,
|
|
67
|
+
// so everything after the `@` stays byte-identical to what was configured.
|
|
68
|
+
return `${scheme[0]}***@${rest.slice(credentialsEnd + 1)}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a request's Origin header names an origin other than the host the
|
|
72
|
+
* request was addressed to.
|
|
73
|
+
*
|
|
74
|
+
* The mutating routes must refuse such requests: a page on another origin can
|
|
75
|
+
* otherwise make the harness reroute its credentials through a proxy of the
|
|
76
|
+
* page's choosing. An absent Origin passes -- browsers always set one on a
|
|
77
|
+
* POST, so absence means a non-browser client such as curl.
|
|
78
|
+
*
|
|
79
|
+
* @param request - the incoming request.
|
|
80
|
+
* @returns true when Origin is present and names another host.
|
|
81
|
+
*/
|
|
82
|
+
export function crossOrigin(request) {
|
|
83
|
+
const origin = header(request, 'origin');
|
|
84
|
+
if (origin === undefined || origin === '')
|
|
85
|
+
return false;
|
|
86
|
+
const host = header(request, 'host');
|
|
87
|
+
if (host === undefined || host === '')
|
|
88
|
+
return true;
|
|
89
|
+
try {
|
|
90
|
+
// Comparing through URL normalises default ports away on both sides.
|
|
91
|
+
return new URL(origin).host !== new URL(`http://${host}`).host;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// An unparsable Origin (including the literal "null") vouches for nothing.
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read one JSON object request body.
|
|
100
|
+
*
|
|
101
|
+
* A body that declares a content-type must declare JSON: browsers refuse to
|
|
102
|
+
* send `application/json` cross-origin without a preflight, so the check
|
|
103
|
+
* turns every form a forged cross-site POST can take into a refusal. An
|
|
104
|
+
* absent content-type passes for non-browser clients that send none.
|
|
105
|
+
*
|
|
106
|
+
* @param request - the incoming request whose body to consume.
|
|
107
|
+
* @param limitBytes - the maximum body size accepted.
|
|
108
|
+
* @returns the parsed object.
|
|
109
|
+
* @throws Error when the body is oversized, unparseable, not an object, or
|
|
110
|
+
* not JSON-content-typed.
|
|
111
|
+
*/
|
|
112
|
+
export async function readJsonObject(request, limitBytes = BODY_LIMIT_BYTES) {
|
|
113
|
+
const contentType = header(request, 'content-type');
|
|
114
|
+
if (contentType !== undefined) {
|
|
115
|
+
const mediaType = contentType.split(';')[0]?.trim().toLowerCase() ?? '';
|
|
116
|
+
if (mediaType !== 'application/json') {
|
|
117
|
+
throw new Error(`request body content-type must be application/json, got "${mediaType}"`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const chunks = [];
|
|
121
|
+
let size = 0;
|
|
122
|
+
for await (const chunk of request) {
|
|
123
|
+
const piece = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
124
|
+
size += piece.length;
|
|
125
|
+
if (size > limitBytes)
|
|
126
|
+
throw new Error('request body too large');
|
|
127
|
+
chunks.push(piece);
|
|
128
|
+
}
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new Error('request body is not valid JSON');
|
|
135
|
+
}
|
|
136
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
137
|
+
throw new Error('request body must be a JSON object');
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Load the persisted panel overrides.
|
|
143
|
+
*
|
|
144
|
+
* An absent file simply means "no override". A corrupt file is ignored too:
|
|
145
|
+
* the panel that wrote it may have died mid-write, and failing the whole
|
|
146
|
+
* plugin over a status file would trade working settings for a stale byte.
|
|
147
|
+
*
|
|
148
|
+
* Each known field validates on its own, so one invalid value drops without
|
|
149
|
+
* discarding the other choice the panel made.
|
|
150
|
+
*
|
|
151
|
+
* @param path - the state file's path.
|
|
152
|
+
* @returns the saved choices, or undefined when there is no readable file.
|
|
153
|
+
*/
|
|
154
|
+
export async function loadStateOverride(path) {
|
|
155
|
+
let raw;
|
|
156
|
+
try {
|
|
157
|
+
raw = await readFile(path, 'utf8');
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(raw);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
170
|
+
return undefined;
|
|
171
|
+
const document = parsed;
|
|
172
|
+
const state = {};
|
|
173
|
+
const proxyUrl = document['proxyUrl'];
|
|
174
|
+
if (typeof proxyUrl === 'string') {
|
|
175
|
+
try {
|
|
176
|
+
state.proxyUrl = validateProxyUrl(proxyUrl);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// A stale invalid proxy falls back to the config rather than
|
|
180
|
+
// invalidating the whole file.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const authFile = document['authFile'];
|
|
184
|
+
if (typeof authFile === 'string' && authFile.trim() !== '') {
|
|
185
|
+
state.authFile = authFile.trim();
|
|
186
|
+
}
|
|
187
|
+
return state;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Persist the panel's choices atomically, so a crash never leaves a half write.
|
|
191
|
+
*
|
|
192
|
+
* The object is written exactly as given; callers read-modify-write so saving
|
|
193
|
+
* one choice never erases the other.
|
|
194
|
+
*
|
|
195
|
+
* @param path - the state file's path.
|
|
196
|
+
* @param state - the choices to persist; absent fields mean "use the config".
|
|
197
|
+
* @returns nothing; failures propagate to the caller.
|
|
198
|
+
*/
|
|
199
|
+
export async function saveStateOverride(path, state) {
|
|
200
|
+
const temporary = `${path}.tmp-${String(process.pid)}`;
|
|
201
|
+
// The file can hold a proxy URL with embedded credentials and a private
|
|
202
|
+
// filesystem layout, so create it private to the user, the same way
|
|
203
|
+
// token-store writes auth.json.
|
|
204
|
+
await writeFile(temporary, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
205
|
+
try {
|
|
206
|
+
await rename(temporary, path);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
await rm(temporary, { force: true });
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=proxy-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-config.js","sourceRoot":"","sources":["../src/proxy-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connectivity probe for a candidate proxy: one real usage read through a
|
|
3
|
+
* throwaway ProxyAgent, timed, answering ok/latency or the failure text.
|
|
4
|
+
*
|
|
5
|
+
* This is the host half of the panel's "test connection" button. It exercises
|
|
6
|
+
* the exact path production traffic takes -- same host, same endpoint, same
|
|
7
|
+
* dispatcher kind -- so a green result means model calls will connect, not
|
|
8
|
+
* merely that the proxy's TCP port answers.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-gpt-sub/proxy-probe
|
|
11
|
+
*/
|
|
12
|
+
import { fetch as undiciFetch } from 'undici';
|
|
13
|
+
/** What the probe answers. */
|
|
14
|
+
export interface ProxyProbeResult {
|
|
15
|
+
/** True when the usage endpoint answered through the candidate. */
|
|
16
|
+
ok: boolean;
|
|
17
|
+
/** Round-trip milliseconds; present on success. */
|
|
18
|
+
latencyMs?: number;
|
|
19
|
+
/** Subscription plan reported upstream; present on success when known. */
|
|
20
|
+
plan?: string;
|
|
21
|
+
/** Failure text; present when ok is false. */
|
|
22
|
+
message?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Construction options; the injectable seams exist for tests. */
|
|
25
|
+
export interface ProxyProbeOptions {
|
|
26
|
+
/** Candidate proxy URL; empty string probes a direct connection. */
|
|
27
|
+
readonly proxyUrl: string;
|
|
28
|
+
/** A live access token for the usage call. */
|
|
29
|
+
readonly accessToken: string;
|
|
30
|
+
/** Per-probe timeout; defaults to {@link DEFAULT_TIMEOUT_MS}. */
|
|
31
|
+
readonly timeoutMs?: number;
|
|
32
|
+
/** Injectable fetch, defaulting to undici's. */
|
|
33
|
+
readonly fetchImpl?: typeof undiciFetch;
|
|
34
|
+
/** Injectable clock, defaulting to Date.now. */
|
|
35
|
+
readonly now?: () => number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Probe one candidate proxy with a single usage read.
|
|
39
|
+
*
|
|
40
|
+
* The probe's ProxyAgent is private: it is created here and closed here, so a
|
|
41
|
+
* probe can never leak dispatcher state into the live routing.
|
|
42
|
+
*
|
|
43
|
+
* @param options - the candidate and the credentials to read with.
|
|
44
|
+
* @returns the probe outcome, never a rejection.
|
|
45
|
+
*/
|
|
46
|
+
export declare function probeProxy(options: ProxyProbeOptions): Promise<ProxyProbeResult>;
|
|
47
|
+
//# sourceMappingURL=proxy-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-probe.d.ts","sourceRoot":"","sources":["../src/proxy-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAc,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAA;AAMzD,8BAA8B;AAC9B,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,EAAE,EAAE,OAAO,CAAA;IACX,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,gDAAgD;IAChD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,WAAW,CAAA;IACvC,gDAAgD;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2BtF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connectivity probe for a candidate proxy: one real usage read through a
|
|
3
|
+
* throwaway ProxyAgent, timed, answering ok/latency or the failure text.
|
|
4
|
+
*
|
|
5
|
+
* This is the host half of the panel's "test connection" button. It exercises
|
|
6
|
+
* the exact path production traffic takes -- same host, same endpoint, same
|
|
7
|
+
* dispatcher kind -- so a green result means model calls will connect, not
|
|
8
|
+
* merely that the proxy's TCP port answers.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-gpt-sub/proxy-probe
|
|
11
|
+
*/
|
|
12
|
+
import { ProxyAgent } from 'undici';
|
|
13
|
+
import { fetchUsage } from "./usage.js";
|
|
14
|
+
/** Default ceiling on one probe; a proxy that cannot answer this is broken. */
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
16
|
+
/**
|
|
17
|
+
* Probe one candidate proxy with a single usage read.
|
|
18
|
+
*
|
|
19
|
+
* The probe's ProxyAgent is private: it is created here and closed here, so a
|
|
20
|
+
* probe can never leak dispatcher state into the live routing.
|
|
21
|
+
*
|
|
22
|
+
* @param options - the candidate and the credentials to read with.
|
|
23
|
+
* @returns the probe outcome, never a rejection.
|
|
24
|
+
*/
|
|
25
|
+
export async function probeProxy(options) {
|
|
26
|
+
const { proxyUrl, accessToken } = options;
|
|
27
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
28
|
+
const now = options.now ?? (() => Date.now());
|
|
29
|
+
const agent = proxyUrl === '' ? undefined : new ProxyAgent(proxyUrl);
|
|
30
|
+
const started = now();
|
|
31
|
+
try {
|
|
32
|
+
const usage = await fetchUsage(accessToken, agent, options.fetchImpl, AbortSignal.timeout(timeoutMs));
|
|
33
|
+
const latencyMs = now() - started;
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
latencyMs,
|
|
37
|
+
...(usage.plan_type === undefined ? {} : { plan: usage.plan_type }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: error instanceof Error ? error.message : String(error),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
if (agent !== undefined)
|
|
48
|
+
await agent.close().catch(() => undefined);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=proxy-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-probe.js","sourceRoot":"","sources":["../src/proxy-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAwB,MAAM,QAAQ,CAAA;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAEvC,+EAA+E;AAC/E,MAAM,kBAAkB,GAAG,MAAM,CAAA;AA4BjC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAA0B;IACzD,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,OAAO,CAAA;IACzC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,CAAA;IACzD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IAC7C,MAAM,KAAK,GAAG,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAA;IACpE,MAAM,OAAO,GAAG,GAAG,EAAE,CAAA;IACrB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,UAAU,CAC5B,WAAW,EACX,KAAK,EACL,OAAO,CAAC,SAAS,EACjB,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CAC/B,CAAA;QACD,MAAM,SAAS,GAAG,GAAG,EAAE,GAAG,OAAO,CAAA;QACjC,OAAO;YACL,EAAE,EAAE,IAAI;YACR,SAAS;YACT,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;SACpE,CAAA;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAChE,CAAA;IACH,CAAC;YAAS,CAAC;QACT,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACrE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
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 } from 'undici';
|
|
19
|
+
/**
|
|
20
|
+
* Hosts that must egress through the proxy: the Codex API and the OAuth token
|
|
21
|
+
* endpoint the refresh calls. Subdomains match too.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PROXIED_HOSTS: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether a hostname belongs to one of `hosts`, matching the host itself and
|
|
26
|
+
* any subdomain of it.
|
|
27
|
+
*
|
|
28
|
+
* @param hostname - the hostname to test.
|
|
29
|
+
* @param hosts - the suffixes to match against.
|
|
30
|
+
* @returns true when the hostname should be proxied.
|
|
31
|
+
*/
|
|
32
|
+
export declare function shouldProxy(hostname: string, hosts?: readonly string[]): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* A dispatcher that sends matching hosts to one delegate and everything else
|
|
35
|
+
* to another.
|
|
36
|
+
*
|
|
37
|
+
* Only the proxy delegate is owned: `close`/`destroy` never touch the
|
|
38
|
+
* fallback, because that dispatcher belongs to the host and outlives this
|
|
39
|
+
* plugin.
|
|
40
|
+
*/
|
|
41
|
+
export declare class HostRoutingDispatcher extends Dispatcher {
|
|
42
|
+
#private;
|
|
43
|
+
constructor(proxy: Dispatcher, fallback: Dispatcher, hosts?: readonly string[]);
|
|
44
|
+
/**
|
|
45
|
+
* Route one request by its origin's hostname.
|
|
46
|
+
*
|
|
47
|
+
* An origin that cannot be parsed is sent to the fallback: defaulting to the
|
|
48
|
+
* proxy would silently divert unrelated traffic.
|
|
49
|
+
*
|
|
50
|
+
* @param options - undici dispatch options.
|
|
51
|
+
* @param handler - undici dispatch handler.
|
|
52
|
+
* @returns whatever the chosen delegate returns.
|
|
53
|
+
*/
|
|
54
|
+
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean;
|
|
55
|
+
/** Close only the proxy delegate; the fallback belongs to the host. */
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
/** Destroy only the proxy delegate; the fallback belongs to the host. */
|
|
58
|
+
destroy(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Retry options that absorb a dropped connection without taking on any
|
|
62
|
+
* HTTP-level retry policy.
|
|
63
|
+
*
|
|
64
|
+
* `statusCodes: []` is deliberate: an upstream 429 or 500 carries a body DSH
|
|
65
|
+
* knows how to read and surface, and retrying here would swallow it. This
|
|
66
|
+
* dispatcher's job is narrow -- make the flaky link look reliable -- and every
|
|
67
|
+
* HTTP semantic stays with the harness.
|
|
68
|
+
*
|
|
69
|
+
* `POST` is listed even though undici omits it by default, because undici
|
|
70
|
+
* omits it for non-idempotency: a POST that may have been received must not be
|
|
71
|
+
* replayed. That does not apply to what is retried here. A connection error
|
|
72
|
+
* arrives before any response, and undici refuses to replay a request whose
|
|
73
|
+
* body was already consumed; a partially streamed response is caught by
|
|
74
|
+
* undici's own range check rather than silently concatenated. Model calls are
|
|
75
|
+
* POSTs, so without this the retry would never fire at all.
|
|
76
|
+
*
|
|
77
|
+
* @param maxRetries - how many extra attempts a single request may make.
|
|
78
|
+
* @returns options for undici's retry interceptor.
|
|
79
|
+
*/
|
|
80
|
+
export declare function connectionRetryOptions(maxRetries: number): {
|
|
81
|
+
maxRetries: number;
|
|
82
|
+
methods: string[];
|
|
83
|
+
statusCodes: number[];
|
|
84
|
+
errorCodes: string[];
|
|
85
|
+
minTimeout: number;
|
|
86
|
+
maxTimeout: number;
|
|
87
|
+
timeoutFactor: number;
|
|
88
|
+
};
|
|
89
|
+
/** What {@link installProxyRouting} returns, so the caller can undo it. */
|
|
90
|
+
export interface ProxyRouting {
|
|
91
|
+
/** The proxy agent, for handing to components that take an explicit dispatcher. */
|
|
92
|
+
readonly dispatcher: Dispatcher;
|
|
93
|
+
/** Restore the previous global dispatcher and close the proxy agent. */
|
|
94
|
+
readonly uninstall: () => Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
/** Options for {@link installProxyRouting}. */
|
|
97
|
+
export interface ProxyRoutingOptions {
|
|
98
|
+
/** Hostnames to proxy; defaults to {@link PROXIED_HOSTS}. */
|
|
99
|
+
readonly hosts?: readonly string[];
|
|
100
|
+
/** Extra attempts per request after a connection failure. 0 disables retry. */
|
|
101
|
+
readonly maxRetries?: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Install host-scoped proxy routing as the global dispatcher.
|
|
105
|
+
*
|
|
106
|
+
* @param proxyUrl - the proxy to route the Codex hosts through.
|
|
107
|
+
* @param options - hosts to proxy and how hard to retry a dropped connection.
|
|
108
|
+
* @returns the proxy dispatcher and the function that restores the previous global.
|
|
109
|
+
*/
|
|
110
|
+
export declare function installProxyRouting(proxyUrl: string, options?: ProxyRoutingOptions): ProxyRouting;
|
|
111
|
+
//# sourceMappingURL=proxy-routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-routing.d.ts","sourceRoot":"","sources":["../src/proxy-routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAsE,MAAM,QAAQ,CAAA;AAwBvG;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAAyD,CAAA;AAEpG;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,GAAE,SAAS,MAAM,EAAkB,GAAG,OAAO,CAG/F;AAED;;;;;;;GAOG;AACH,qBAAa,qBAAsB,SAAQ,UAAU;;gBAKvC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,GAAE,SAAS,MAAM,EAAkB;IAO7F;;;;;;;;;OASG;IACM,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,eAAe,EAAE,OAAO,EAAE,UAAU,CAAC,eAAe,GAAG,OAAO;IAYpG,uEAAuE;IACxD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC,yEAAyE;IAC1D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAGxC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG;IAC1D,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;CACtB,CAYA;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACxC;AAED,+CAA+C;AAC/C,MAAM,WAAW,mBAAmB;IAClC,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,YAAY,CAyBrG"}
|