@jeffusion/bungee-core 5.10.0 → 5.12.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/dist/main.js +31436 -17327
- package/dist/plugins/ai-transformer/index.js.map +2 -2
- package/dist/plugins/anthropic-request-sanitizer/index.js.map +2 -2
- package/dist/plugins/anthropic-tool-name-transformer/index.js.map +2 -2
- package/dist/plugins/chatgpt-oauth/control.js +850 -47
- package/dist/plugins/chatgpt-oauth/control.js.map +16 -7
- package/dist/plugins/chatgpt-oauth/index.js +691 -16
- package/dist/plugins/chatgpt-oauth/index.js.map +15 -7
- package/dist/plugins/chatgpt-oauth/manifest.json +117 -7
- package/dist/plugins/chatgpt-oauth/ui/AccountsPage.svelte +25 -15
- package/dist/plugins/chatgpt-oauth/ui/ChatgptQuotaWidget.svelte +7 -4
- package/dist/plugins/chatgpt-oauth/ui/account-model.js +16 -7
- package/dist/plugins/deepseek-reasoning-fix/index.js.map +2 -2
- package/dist/plugins/key-access/index.js.map +2 -2
- package/dist/plugins/key-rate-limit/index.js.map +2 -2
- package/dist/plugins/local-accounts/control.js.map +2 -2
- package/dist/plugins/local-accounts/index.js.map +2 -2
- package/dist/plugins/model-mapping/control.js +72 -17741
- package/dist/plugins/model-mapping/control.js.map +6 -58
- package/dist/plugins/model-mapping/index.js +32 -17433
- package/dist/plugins/model-mapping/index.js.map +6 -58
- package/dist/plugins/model-mapping/manifest.json +30 -14
- package/dist/plugins/models-dev/control.js +1042 -0
- package/dist/plugins/models-dev/control.js.map +16 -0
- package/dist/plugins/models-dev/index.js +15418 -0
- package/dist/plugins/models-dev/index.js.map +134 -0
- package/dist/plugins/models-dev/manifest.json +199 -0
- package/dist/plugins/models-dev/ui/ModelsDevSettings.svelte +120 -0
- package/dist/plugins/openai-messages-to-chat/index.js.map +2 -2
- package/dist/plugins/signature-repair/index.js.map +2 -2
- package/dist/plugins/token-budget/control.js +524 -9
- package/dist/plugins/token-budget/control.js.map +12 -4
- package/dist/plugins/token-budget/index.js +519 -12
- package/dist/plugins/token-budget/index.js.map +13 -6
- package/dist/plugins/token-budget/manifest.json +16 -2
- package/dist/plugins/token-metering/index.js +93 -2
- package/dist/plugins/token-metering/index.js.map +12 -6
- package/dist/plugins/token-stats/control.js +174 -312
- package/dist/plugins/token-stats/control.js.map +7 -7
- package/dist/plugins/token-stats/index.js +283 -486
- package/dist/plugins/token-stats/index.js.map +16 -12
- package/dist/plugins/token-stats/manifest.json +99 -12
- package/dist/plugins/token-stats/ui/ClientModelPicker.svelte +76 -0
- package/dist/plugins/token-stats/ui/PriceModelPicker.svelte +87 -0
- package/dist/plugins/token-stats/ui/TokenStatsSettings.svelte +136 -80
- package/dist/plugins/token-stats/ui/client-model-search.test.ts +58 -0
- package/dist/plugins/token-stats/ui/client-model-search.ts +16 -0
- package/dist/plugins/token-stats/ui/model-search.ts +45 -0
- package/dist/plugins/token-stats/ui/pricing-catalog.test.ts +114 -0
- package/dist/plugins/token-stats/ui/pricing-catalog.ts +17 -0
- package/package.json +2 -2
|
@@ -1,6 +1,512 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// packages/core/src/plugin-services/host-rpc.ts
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
+
|
|
5
|
+
// packages/core/src/plugin-services/wire-contract.ts
|
|
6
|
+
import { isProxy } from "util/types";
|
|
7
|
+
var RPC_JSON_MAX_BYTES = 64 * 1024;
|
|
8
|
+
var RPC_CONTRACT_MAX_BYTES = 256 * 1024;
|
|
9
|
+
var JSON_MAX_DEPTH = 64;
|
|
10
|
+
var SCHEMA_MAX_DEPTH = 64;
|
|
11
|
+
var RPC_MAX_TIMEOUT_MS = 300000;
|
|
12
|
+
var RPC_MAX_METHODS = 128;
|
|
13
|
+
var RPC_SERVICE_ID = /^[a-z][a-z0-9.-]{0,127}$/;
|
|
14
|
+
var RPC_METHOD_NAME = /^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/;
|
|
15
|
+
var RPC_CALL_PURPOSES = new Set(["bootstrap", "background", "management", "request", "attempt"]);
|
|
16
|
+
var RPC_COMMAND_DEDUPLICATION = new Set(["local-transaction", "external-contract", "none"]);
|
|
17
|
+
var RPC_COMMAND_POLICY_KEYS = new Set(["deduplication", "resultRetentionMs", "quotaBytes", "maxResultBytes"]);
|
|
18
|
+
var CONTRACT_KEYS = ["id", "version", "methods"];
|
|
19
|
+
var METHOD_KEYS = [
|
|
20
|
+
"kind",
|
|
21
|
+
"input",
|
|
22
|
+
"output",
|
|
23
|
+
"purposes",
|
|
24
|
+
"timeoutMs",
|
|
25
|
+
"maxInputBytes",
|
|
26
|
+
"maxOutputBytes",
|
|
27
|
+
"command"
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
class RpcProtocolError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
name = "RpcProtocolError";
|
|
33
|
+
constructor(code, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function failData(category) {
|
|
39
|
+
throw new RpcProtocolError("invalid_data", `invalid_data in ${category}`);
|
|
40
|
+
}
|
|
41
|
+
function failSize() {
|
|
42
|
+
throw new RpcProtocolError("size_limit", "size_limit: JSON value exceeds the byte limit");
|
|
43
|
+
}
|
|
44
|
+
function failContract(reason) {
|
|
45
|
+
throw new RpcProtocolError("invalid_contract", `invalid_contract: ${reason}`);
|
|
46
|
+
}
|
|
47
|
+
function isPlainObject(value) {
|
|
48
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
49
|
+
return false;
|
|
50
|
+
if (isProxy(value))
|
|
51
|
+
return false;
|
|
52
|
+
const prototype = Object.getPrototypeOf(value);
|
|
53
|
+
return prototype === Object.prototype || prototype === null;
|
|
54
|
+
}
|
|
55
|
+
function isPositiveSafeInteger(value) {
|
|
56
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
57
|
+
}
|
|
58
|
+
function isNonNegativeSafeInteger(value) {
|
|
59
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
60
|
+
}
|
|
61
|
+
function hasLoneSurrogate(value) {
|
|
62
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
63
|
+
const code = value.charCodeAt(index);
|
|
64
|
+
if (code >= 55296 && code <= 56319) {
|
|
65
|
+
if (index + 1 >= value.length)
|
|
66
|
+
return true;
|
|
67
|
+
const next = value.charCodeAt(index + 1);
|
|
68
|
+
if (next < 56320 || next > 57343)
|
|
69
|
+
return true;
|
|
70
|
+
index += 1;
|
|
71
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function isArrayIndex(key, length) {
|
|
78
|
+
if (!/^(?:0|[1-9]\d*)$/.test(key))
|
|
79
|
+
return false;
|
|
80
|
+
const index = Number(key);
|
|
81
|
+
return Number.isSafeInteger(index) && index >= 0 && index < length;
|
|
82
|
+
}
|
|
83
|
+
function assertKnownKeys(input, allowed, category) {
|
|
84
|
+
for (const key of Object.keys(input)) {
|
|
85
|
+
if (!allowed.includes(key))
|
|
86
|
+
failContract(`unknown ${category} field`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function spend(budget, bytes) {
|
|
90
|
+
budget.left -= bytes;
|
|
91
|
+
if (budget.left < 0)
|
|
92
|
+
failSize();
|
|
93
|
+
}
|
|
94
|
+
function snapshot(value, depth, ancestors, budget) {
|
|
95
|
+
if (value === null) {
|
|
96
|
+
spend(budget, 4);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === "boolean") {
|
|
100
|
+
spend(budget, value ? 4 : 5);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
if (hasLoneSurrogate(value))
|
|
105
|
+
failData("string");
|
|
106
|
+
spend(budget, Buffer.byteLength(value, "utf8") + 2);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
if (typeof value === "number") {
|
|
110
|
+
if (!Number.isFinite(value))
|
|
111
|
+
failData("number");
|
|
112
|
+
spend(budget, 1);
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value !== "object")
|
|
116
|
+
failData("json");
|
|
117
|
+
const object = value;
|
|
118
|
+
if (isProxy(object))
|
|
119
|
+
failData("json");
|
|
120
|
+
if (ancestors.has(object))
|
|
121
|
+
failData("json");
|
|
122
|
+
const nextDepth = depth + 1;
|
|
123
|
+
if (nextDepth > JSON_MAX_DEPTH)
|
|
124
|
+
failData("json");
|
|
125
|
+
if (Array.isArray(object)) {
|
|
126
|
+
if (Object.getPrototypeOf(object) !== Array.prototype)
|
|
127
|
+
failData("array");
|
|
128
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(object, "length");
|
|
129
|
+
if (!lengthDescriptor || !("value" in lengthDescriptor) || typeof lengthDescriptor.value !== "number") {
|
|
130
|
+
failData("array");
|
|
131
|
+
}
|
|
132
|
+
const length = lengthDescriptor.value;
|
|
133
|
+
if (!Number.isSafeInteger(length) || length < 0)
|
|
134
|
+
failData("array");
|
|
135
|
+
if (length > budget.left)
|
|
136
|
+
failSize();
|
|
137
|
+
spend(budget, 2);
|
|
138
|
+
if (length > 1)
|
|
139
|
+
spend(budget, length - 1);
|
|
140
|
+
ancestors.add(object);
|
|
141
|
+
const items = new Array(length);
|
|
142
|
+
let count = 0;
|
|
143
|
+
for (const key of Reflect.ownKeys(object)) {
|
|
144
|
+
if (key === "length")
|
|
145
|
+
continue;
|
|
146
|
+
if (typeof key !== "string" || !isArrayIndex(key, length))
|
|
147
|
+
failData("array");
|
|
148
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
149
|
+
if (!descriptor || !descriptor.enumerable || !("value" in descriptor))
|
|
150
|
+
failData("array");
|
|
151
|
+
items[Number(key)] = snapshot(descriptor.value, nextDepth, ancestors, budget);
|
|
152
|
+
count += 1;
|
|
153
|
+
}
|
|
154
|
+
if (count !== length)
|
|
155
|
+
failData("array");
|
|
156
|
+
ancestors.delete(object);
|
|
157
|
+
return items;
|
|
158
|
+
}
|
|
159
|
+
const prototype = Object.getPrototypeOf(object);
|
|
160
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
161
|
+
failData("object");
|
|
162
|
+
const keys = Reflect.ownKeys(object);
|
|
163
|
+
if (keys.length > budget.left)
|
|
164
|
+
failSize();
|
|
165
|
+
spend(budget, 2);
|
|
166
|
+
if (keys.length > 1)
|
|
167
|
+
spend(budget, keys.length - 1);
|
|
168
|
+
ancestors.add(object);
|
|
169
|
+
const result = {};
|
|
170
|
+
for (const key of keys) {
|
|
171
|
+
if (typeof key !== "string")
|
|
172
|
+
failData("object");
|
|
173
|
+
if (hasLoneSurrogate(key))
|
|
174
|
+
failData("object");
|
|
175
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
176
|
+
if (!descriptor || !descriptor.enumerable || !("value" in descriptor))
|
|
177
|
+
failData("object");
|
|
178
|
+
spend(budget, Buffer.byteLength(key, "utf8") + 3);
|
|
179
|
+
Object.defineProperty(result, key, {
|
|
180
|
+
value: snapshot(descriptor.value, nextDepth, ancestors, budget),
|
|
181
|
+
enumerable: true,
|
|
182
|
+
writable: true,
|
|
183
|
+
configurable: true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
ancestors.delete(object);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
function snapshotPureInput(input) {
|
|
190
|
+
try {
|
|
191
|
+
const pure = snapshot(input, 0, new Set, { left: RPC_CONTRACT_MAX_BYTES });
|
|
192
|
+
if (Buffer.byteLength(JSON.stringify(pure), "utf8") > RPC_CONTRACT_MAX_BYTES)
|
|
193
|
+
failContract("input must be bounded pure JSON data");
|
|
194
|
+
return pure;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error instanceof RpcProtocolError && (error.code === "invalid_data" || error.code === "size_limit")) {
|
|
197
|
+
failContract("input must be bounded pure JSON data");
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function checkOptionalNonNegative(value, field) {
|
|
203
|
+
if (value === undefined)
|
|
204
|
+
return;
|
|
205
|
+
if (!isNonNegativeSafeInteger(value))
|
|
206
|
+
failContract(`${field} must be a non-negative safe integer`);
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
function checkOptionalFinite(value, field) {
|
|
210
|
+
if (value === undefined)
|
|
211
|
+
return;
|
|
212
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
213
|
+
failContract(`${field} must be a finite number`);
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
function checkSchema(input, depth, seen) {
|
|
217
|
+
if (depth > SCHEMA_MAX_DEPTH)
|
|
218
|
+
failContract("schema nesting exceeds the limit");
|
|
219
|
+
if (!isPlainObject(input))
|
|
220
|
+
failContract("schema must be a plain object");
|
|
221
|
+
if (seen.has(input))
|
|
222
|
+
failContract("schema must not contain cycles");
|
|
223
|
+
seen.add(input);
|
|
224
|
+
const type = input.type;
|
|
225
|
+
if (type === "string") {
|
|
226
|
+
assertKnownKeys(input, ["type", "minLength", "maxLength"], "schema");
|
|
227
|
+
const min = checkOptionalNonNegative(input.minLength, "minLength");
|
|
228
|
+
const max = checkOptionalNonNegative(input.maxLength, "maxLength");
|
|
229
|
+
if (min !== undefined && max !== undefined && min > max)
|
|
230
|
+
failContract("string minLength must not exceed maxLength");
|
|
231
|
+
} else if (type === "number") {
|
|
232
|
+
assertKnownKeys(input, ["type", "integer", "minimum", "maximum"], "schema");
|
|
233
|
+
if (input.integer !== undefined && typeof input.integer !== "boolean")
|
|
234
|
+
failContract("number integer flag must be boolean");
|
|
235
|
+
const min = checkOptionalFinite(input.minimum, "minimum");
|
|
236
|
+
const max = checkOptionalFinite(input.maximum, "maximum");
|
|
237
|
+
if (min !== undefined && max !== undefined && min > max)
|
|
238
|
+
failContract("number minimum must not exceed maximum");
|
|
239
|
+
} else if (type === "boolean") {
|
|
240
|
+
assertKnownKeys(input, ["type"], "schema");
|
|
241
|
+
} else if (type === "null") {
|
|
242
|
+
assertKnownKeys(input, ["type"], "schema");
|
|
243
|
+
} else if (type === "literal") {
|
|
244
|
+
assertKnownKeys(input, ["type", "value"], "schema");
|
|
245
|
+
if (!Object.prototype.hasOwnProperty.call(input, "value"))
|
|
246
|
+
failContract("literal schema requires a value");
|
|
247
|
+
try {
|
|
248
|
+
snapshot(input.value, 0, new Set, { left: Infinity });
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof RpcProtocolError && error.code === "invalid_data") {
|
|
251
|
+
failContract("literal value must be pure JSON");
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
} else if (type === "array") {
|
|
256
|
+
assertKnownKeys(input, ["type", "items", "maxItems"], "schema");
|
|
257
|
+
if (!Object.prototype.hasOwnProperty.call(input, "items"))
|
|
258
|
+
failContract("array schema requires items");
|
|
259
|
+
checkSchema(input.items, depth + 1, seen);
|
|
260
|
+
checkOptionalNonNegative(input.maxItems, "maxItems");
|
|
261
|
+
} else if (type === "object") {
|
|
262
|
+
assertKnownKeys(input, ["type", "properties", "optional"], "schema");
|
|
263
|
+
const properties = input.properties;
|
|
264
|
+
if (!isPlainObject(properties))
|
|
265
|
+
failContract("object schema requires a properties record");
|
|
266
|
+
for (const key of Object.keys(properties))
|
|
267
|
+
checkSchema(properties[key], depth + 1, seen);
|
|
268
|
+
const optional = input.optional;
|
|
269
|
+
if (optional !== undefined) {
|
|
270
|
+
if (!Array.isArray(optional))
|
|
271
|
+
failContract("object optional must be an array of field names");
|
|
272
|
+
const names = new Set;
|
|
273
|
+
for (const name of optional) {
|
|
274
|
+
if (typeof name !== "string")
|
|
275
|
+
failContract("object optional entries must be strings");
|
|
276
|
+
if (names.has(name))
|
|
277
|
+
failContract("object optional entries must be unique");
|
|
278
|
+
names.add(name);
|
|
279
|
+
if (!Object.prototype.hasOwnProperty.call(properties, name)) {
|
|
280
|
+
failContract("object optional names must exist in properties");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} else if (type === "record") {
|
|
285
|
+
assertKnownKeys(input, ["type", "values", "maxEntries"], "schema");
|
|
286
|
+
if (!Object.prototype.hasOwnProperty.call(input, "values"))
|
|
287
|
+
failContract("record schema requires values");
|
|
288
|
+
checkSchema(input.values, depth + 1, seen);
|
|
289
|
+
checkOptionalNonNegative(input.maxEntries, "maxEntries");
|
|
290
|
+
} else if (type === "union") {
|
|
291
|
+
assertKnownKeys(input, ["type", "variants"], "schema");
|
|
292
|
+
const variants = input.variants;
|
|
293
|
+
if (!Array.isArray(variants) || variants.length === 0) {
|
|
294
|
+
failContract("union schema requires a non-empty variants array");
|
|
295
|
+
}
|
|
296
|
+
for (const variant of variants)
|
|
297
|
+
checkSchema(variant, depth + 1, seen);
|
|
298
|
+
} else if (type === "json") {
|
|
299
|
+
assertKnownKeys(input, ["type", "maxBytes"], "schema");
|
|
300
|
+
if (input.maxBytes !== undefined && !isPositiveSafeInteger(input.maxBytes)) {
|
|
301
|
+
failContract("json schema maxBytes must be a positive safe integer");
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
failContract("unknown schema type");
|
|
305
|
+
}
|
|
306
|
+
seen.delete(input);
|
|
307
|
+
}
|
|
308
|
+
function checkByteLimit(value, field) {
|
|
309
|
+
if (value === undefined)
|
|
310
|
+
return;
|
|
311
|
+
if (!isPositiveSafeInteger(value) || value > RPC_JSON_MAX_BYTES) {
|
|
312
|
+
failContract(`${field} must be a positive integer no larger than the RPC envelope`);
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
function checkCommandPolicy(policy, maxOutputBytes) {
|
|
317
|
+
const keys = Object.keys(policy);
|
|
318
|
+
if (keys.length !== RPC_COMMAND_POLICY_KEYS.size || keys.some((key) => !RPC_COMMAND_POLICY_KEYS.has(key))) {
|
|
319
|
+
failContract("command policy fields are fixed");
|
|
320
|
+
}
|
|
321
|
+
const deduplication = policy.deduplication;
|
|
322
|
+
if (typeof deduplication !== "string" || !RPC_COMMAND_DEDUPLICATION.has(deduplication)) {
|
|
323
|
+
failContract("invalid command deduplication");
|
|
324
|
+
}
|
|
325
|
+
const retention = policy.resultRetentionMs;
|
|
326
|
+
if (retention !== null && !isPositiveSafeInteger(retention)) {
|
|
327
|
+
failContract("resultRetentionMs must be a positive integer or null");
|
|
328
|
+
}
|
|
329
|
+
const maxResultBytes = policy.maxResultBytes;
|
|
330
|
+
if (!isPositiveSafeInteger(maxResultBytes))
|
|
331
|
+
failContract("maxResultBytes must be a positive safe integer");
|
|
332
|
+
if (maxResultBytes > maxOutputBytes)
|
|
333
|
+
failContract("maxResultBytes must not exceed maxOutputBytes");
|
|
334
|
+
const quotaBytes = policy.quotaBytes;
|
|
335
|
+
if (!isPositiveSafeInteger(quotaBytes))
|
|
336
|
+
failContract("quotaBytes must be a positive safe integer");
|
|
337
|
+
if (quotaBytes < maxResultBytes)
|
|
338
|
+
failContract("quotaBytes must be able to hold maxResultBytes");
|
|
339
|
+
}
|
|
340
|
+
function checkMethod(definition) {
|
|
341
|
+
assertKnownKeys(definition, METHOD_KEYS, "method");
|
|
342
|
+
const kind = definition.kind;
|
|
343
|
+
if (kind !== "query" && kind !== "command")
|
|
344
|
+
failContract("method kind must be query or command");
|
|
345
|
+
checkSchema(definition.input, 0, new Set);
|
|
346
|
+
checkSchema(definition.output, 0, new Set);
|
|
347
|
+
const purposes = definition.purposes;
|
|
348
|
+
if (!Array.isArray(purposes) || purposes.length === 0)
|
|
349
|
+
failContract("purposes must be a non-empty array");
|
|
350
|
+
const seenPurposes = new Set;
|
|
351
|
+
for (const purpose of purposes) {
|
|
352
|
+
if (typeof purpose !== "string" || !RPC_CALL_PURPOSES.has(purpose))
|
|
353
|
+
failContract("invalid purpose");
|
|
354
|
+
if (seenPurposes.has(purpose))
|
|
355
|
+
failContract("purposes must be unique");
|
|
356
|
+
seenPurposes.add(purpose);
|
|
357
|
+
}
|
|
358
|
+
if (definition.timeoutMs !== undefined) {
|
|
359
|
+
if (!isPositiveSafeInteger(definition.timeoutMs) || definition.timeoutMs > RPC_MAX_TIMEOUT_MS) {
|
|
360
|
+
failContract("timeoutMs must be a positive integer no larger than the timeout bound");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
checkByteLimit(definition.maxInputBytes, "maxInputBytes");
|
|
364
|
+
const maxOutputBytes = checkByteLimit(definition.maxOutputBytes, "maxOutputBytes");
|
|
365
|
+
const command = definition.command;
|
|
366
|
+
if (kind === "command") {
|
|
367
|
+
if (!isPlainObject(command))
|
|
368
|
+
failContract("command methods require an explicit command policy");
|
|
369
|
+
checkCommandPolicy(command, maxOutputBytes ?? RPC_JSON_MAX_BYTES);
|
|
370
|
+
} else if (command !== undefined) {
|
|
371
|
+
failContract("query methods must not declare a command policy");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function checkContract(contract) {
|
|
375
|
+
if (!isPlainObject(contract))
|
|
376
|
+
failContract("contract must be a plain object");
|
|
377
|
+
assertKnownKeys(contract, CONTRACT_KEYS, "contract");
|
|
378
|
+
if (typeof contract.id !== "string" || !RPC_SERVICE_ID.test(contract.id))
|
|
379
|
+
failContract("invalid service id");
|
|
380
|
+
if (!isPositiveSafeInteger(contract.version))
|
|
381
|
+
failContract("service version must be a positive safe integer");
|
|
382
|
+
const methods = contract.methods;
|
|
383
|
+
if (!isPlainObject(methods))
|
|
384
|
+
failContract("methods must be a plain object");
|
|
385
|
+
const names = Object.keys(methods);
|
|
386
|
+
if (names.length === 0)
|
|
387
|
+
failContract("service must declare at least one method");
|
|
388
|
+
if (names.length > RPC_MAX_METHODS)
|
|
389
|
+
failContract("service declares too many methods");
|
|
390
|
+
for (const name of names) {
|
|
391
|
+
if (!RPC_METHOD_NAME.test(name))
|
|
392
|
+
failContract("invalid method name");
|
|
393
|
+
const definition = methods[name];
|
|
394
|
+
if (!isPlainObject(definition))
|
|
395
|
+
failContract("method definition must be a plain object");
|
|
396
|
+
checkMethod(definition);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function deepFreeze(value) {
|
|
400
|
+
if (Array.isArray(value)) {
|
|
401
|
+
for (const item of value)
|
|
402
|
+
deepFreeze(item);
|
|
403
|
+
return Object.freeze(value);
|
|
404
|
+
}
|
|
405
|
+
if (isPlainObject(value)) {
|
|
406
|
+
for (const key of Object.keys(value))
|
|
407
|
+
deepFreeze(value[key]);
|
|
408
|
+
return Object.freeze(value);
|
|
409
|
+
}
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
function defineRpcService(contract) {
|
|
413
|
+
const pure = snapshotPureInput(contract);
|
|
414
|
+
checkContract(pure);
|
|
415
|
+
return deepFreeze(pure);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// packages/core/src/plugin-services/rpc-runtime.ts
|
|
419
|
+
var PROCESSES = new Set(["control", "worker", "ingress"]);
|
|
420
|
+
var SCOPES = new Set(["global", "binding"]);
|
|
421
|
+
var PURPOSES = new Set(["bootstrap", "background", "management", "request", "attempt"]);
|
|
422
|
+
var ENDPOINTS = new WeakMap;
|
|
423
|
+
var REVOKED = new WeakMap;
|
|
424
|
+
|
|
425
|
+
// packages/core/src/plugin-services/host-rpc.ts
|
|
426
|
+
var SERVICE_ERROR_MESSAGES = {
|
|
427
|
+
unauthorized: "RPC call is not authorized",
|
|
428
|
+
not_ready: "RPC endpoint is not ready",
|
|
429
|
+
retired: "RPC endpoint is retiring",
|
|
430
|
+
revoked: "RPC endpoint generation was revoked",
|
|
431
|
+
unsupported_method: "RPC method is not published",
|
|
432
|
+
wrong_purpose: "RPC method is not published for this purpose",
|
|
433
|
+
invalid_input: "RPC input is not valid",
|
|
434
|
+
invalid_output: "RPC output is not valid",
|
|
435
|
+
invalid_operation_id: "RPC command operation id is invalid",
|
|
436
|
+
overloaded: "RPC runtime is at capacity",
|
|
437
|
+
cancelled: "RPC call was cancelled",
|
|
438
|
+
timeout: "RPC call deadline exceeded",
|
|
439
|
+
unknown: "RPC command outcome is unknown",
|
|
440
|
+
capability_unavailable: "RPC command execution is unavailable",
|
|
441
|
+
closed: "RPC runtime is closed",
|
|
442
|
+
failed: "RPC call failed",
|
|
443
|
+
conflict: "RPC command identity conflicts with an existing record",
|
|
444
|
+
expired: "RPC command result has expired",
|
|
445
|
+
pending: "RPC command is still pending",
|
|
446
|
+
rejected: "RPC command was rejected",
|
|
447
|
+
missing: "RPC command has no stored result",
|
|
448
|
+
storage_failure: "RPC command storage failed",
|
|
449
|
+
deadlock: "RPC call chain would deadlock",
|
|
450
|
+
call_depth_exceeded: "RPC call depth exceeded",
|
|
451
|
+
undeclared: "RPC service is not declared by the manifest",
|
|
452
|
+
unavailable: "RPC service is unavailable",
|
|
453
|
+
ambiguous_placement: "RPC service placement is ambiguous",
|
|
454
|
+
invalid_registration: "RPC registration is invalid"
|
|
455
|
+
};
|
|
456
|
+
var SERVICE_ERROR_CODES = new Set(Object.keys(SERVICE_ERROR_MESSAGES));
|
|
457
|
+
var EMPTY_OPTIONS = Object.freeze({});
|
|
458
|
+
var FRAME_STORE = new AsyncLocalStorage;
|
|
459
|
+
var CALLEE_INFO = new WeakMap;
|
|
460
|
+
// packages/core/src/plugin-services/peer-channel-protocol.ts
|
|
461
|
+
var CHANNEL_REQUEST_MAX_BYTES = 4 * 1024;
|
|
462
|
+
var CHANNEL_RESULT_MAX_BYTES = 60 * 1024;
|
|
463
|
+
var CHANNEL_MAX_CHUNK_BYTES = 60 * 1024;
|
|
464
|
+
var PLUGIN_CHANNEL_LANES = Object.freeze(["event", "snapshot", "stream"]);
|
|
465
|
+
var ERROR_CODES = new Set([
|
|
466
|
+
"unavailable",
|
|
467
|
+
"ambiguous",
|
|
468
|
+
"unauthorized",
|
|
469
|
+
"invalid",
|
|
470
|
+
"overloaded",
|
|
471
|
+
"expired",
|
|
472
|
+
"conflict",
|
|
473
|
+
"gap",
|
|
474
|
+
"truncated",
|
|
475
|
+
"rejected",
|
|
476
|
+
"cancelled",
|
|
477
|
+
"closed",
|
|
478
|
+
"unknown",
|
|
479
|
+
"failed"
|
|
480
|
+
]);
|
|
481
|
+
var RPC_CALL_PURPOSES2 = new Set(["bootstrap", "background", "management", "request", "attempt"]);
|
|
482
|
+
|
|
483
|
+
// packages/core/src/plugin-services/peer-channel-hub.ts
|
|
484
|
+
var CHANNEL_MAX_TRANSFER_BUFFER_BYTES = 4 * CHANNEL_MAX_CHUNK_BYTES;
|
|
485
|
+
var CHANNEL_MAX_TOTAL_BUFFER_BYTES = 16 * CHANNEL_MAX_CHUNK_BYTES;
|
|
486
|
+
var CHANNEL_STREAM_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
487
|
+
var CHANNEL_STREAM_MAX_CREDIT_BYTES = 8 * CHANNEL_MAX_CHUNK_BYTES;
|
|
488
|
+
var CHANNEL_STREAM_DEFAULT_CREDIT_BYTES = 4 * CHANNEL_MAX_CHUNK_BYTES;
|
|
489
|
+
var CHANNEL_SNAPSHOT_MAX_BYTES = 64 * 1024 * 1024;
|
|
490
|
+
var CHANNEL_EVENT_MAX_PAYLOAD_BYTES = 60 * 1024;
|
|
491
|
+
var EMPTY = new Uint8Array(0);
|
|
492
|
+
var HANDLE_REGISTRATIONS = new WeakMap;
|
|
493
|
+
|
|
494
|
+
// packages/core/src/plugin-services/snapshot-store.ts
|
|
495
|
+
var DEFAULT_CHUNK_BYTES = 60 * 1024;
|
|
496
|
+
var SNAPSHOT_FAMILY_REFS = new Map;
|
|
497
|
+
|
|
498
|
+
// packages/core/src/plugin-services.ts
|
|
499
|
+
var CHANNEL_KINDS = new Set(["events", "snapshot", "stream"]);
|
|
500
|
+
|
|
501
|
+
// plugins/chatgpt-oauth/server/rpc.ts
|
|
502
|
+
var command = { deduplication: "none", resultRetentionMs: null, quotaBytes: 65536, maxResultBytes: 65536 };
|
|
503
|
+
var businessRpc = defineRpcService({ id: "chatgpt-oauth.credentials.v1", version: 1, methods: {
|
|
504
|
+
getCredential: { kind: "command", input: { type: "object", properties: {} }, output: { type: "object", properties: { version: { type: "number", integer: true, minimum: 1 }, expiresAt: { type: "number" }, headers: { type: "record", values: { type: "string" }, maxEntries: 16 } } }, purposes: ["attempt"], command, timeoutMs: 15000 },
|
|
505
|
+
rejectAccess: { kind: "command", input: { type: "object", properties: { version: { type: "number", integer: true, minimum: 1 } } }, output: { type: "object", properties: { rejected: { type: "boolean" } } }, purposes: ["attempt"], command, timeoutMs: 15000 }
|
|
506
|
+
} });
|
|
507
|
+
|
|
2
508
|
// plugins/chatgpt-oauth/server/control.ts
|
|
3
|
-
import { randomUUID as
|
|
509
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4
510
|
|
|
5
511
|
// plugins/chatgpt-oauth/server/oauth.ts
|
|
6
512
|
import { createHash, randomBytes } from "crypto";
|
|
@@ -519,8 +1025,181 @@ function extractCodexIdentity(idToken) {
|
|
|
519
1025
|
};
|
|
520
1026
|
}
|
|
521
1027
|
|
|
1028
|
+
// plugins/chatgpt-oauth/server/siwc.ts
|
|
1029
|
+
import { createPublicKey, randomBytes as randomBytes2, randomUUID, verify } from "crypto";
|
|
1030
|
+
var SIWC_AUTH_URL = "https://auth.openai.com/api/accounts/authorize";
|
|
1031
|
+
var SIWC_TOKEN_URL = "https://auth.openai.com/api/accounts/oauth/token";
|
|
1032
|
+
var SIWC_JWKS_URL = "https://auth.openai.com/.well-known/jwks.json";
|
|
1033
|
+
var SIWC_ISSUER = "https://auth.openai.com";
|
|
1034
|
+
var SIWC_REDIRECT_URI = "http://127.0.0.1:1455/auth/callback";
|
|
1035
|
+
var SIWC_RESOURCE = "https://api.openai.com/v1";
|
|
1036
|
+
var SIWC_SCOPES = ["openid", "profile", "email", "offline_access", "resource.invoke", "chatgpt.tokens.use.direct"];
|
|
1037
|
+
var SIWC_REGISTRATION_KEY = "siwc.registration.v1";
|
|
1038
|
+
var CLIENT_ID = /^oaiapp_[A-Za-z0-9_-]{1,200}$/;
|
|
1039
|
+
var HOST_ID = /^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
1040
|
+
function invalid(kind = "invalid_response") {
|
|
1041
|
+
throw new CodexOAuthError(kind, `SIWC ${kind === "invalid_callback" ? "callback" : "response"} is invalid`, { retryable: false });
|
|
1042
|
+
}
|
|
1043
|
+
async function getSiwcHostId(store, canWrite = () => true) {
|
|
1044
|
+
for (let attempt = 0;attempt < 12; attempt++) {
|
|
1045
|
+
if (!canWrite())
|
|
1046
|
+
throw new CodexOAuthError("cancelled", "SIWC operation was cancelled", { retryable: false });
|
|
1047
|
+
const current = await store.get(SIWC_REGISTRATION_KEY);
|
|
1048
|
+
if (current) {
|
|
1049
|
+
try {
|
|
1050
|
+
const record = JSON.parse(current.value);
|
|
1051
|
+
if (record?.schema === 1 && typeof record.hostId === "string" && HOST_ID.test(record.hostId))
|
|
1052
|
+
return record.hostId;
|
|
1053
|
+
} catch {}
|
|
1054
|
+
return invalid();
|
|
1055
|
+
}
|
|
1056
|
+
const hostId = `urn:uuid:${randomUUID()}`;
|
|
1057
|
+
if (!canWrite())
|
|
1058
|
+
throw new CodexOAuthError("cancelled", "SIWC operation was cancelled", { retryable: false });
|
|
1059
|
+
try {
|
|
1060
|
+
await store.compareAndSet(SIWC_REGISTRATION_KEY, null, JSON.stringify({ schema: 1, hostId }));
|
|
1061
|
+
return hostId;
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (error.code !== "version_conflict")
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return invalid();
|
|
1068
|
+
}
|
|
1069
|
+
function validSiwcMetadata(value) {
|
|
1070
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1071
|
+
return false;
|
|
1072
|
+
const item = value;
|
|
1073
|
+
return Object.keys(item).every((key) => ["clientId", "subject", "scopes"].includes(key)) && typeof item.clientId === "string" && CLIENT_ID.test(item.clientId) && typeof item.subject === "string" && item.subject.length > 0 && item.subject.length <= 512 && item.subject.trim() === item.subject && !/[\u0000-\u001f\u007f]/.test(item.subject) && Array.isArray(item.scopes) && item.scopes.length <= 128 && item.scopes.every((scope) => typeof scope === "string" && /^[\x21\x23-\x5b\x5d-\x7e]{1,128}$/.test(scope)) && item.scopes.includes("resource.invoke") && item.scopes.includes("chatgpt.tokens.use.direct");
|
|
1074
|
+
}
|
|
1075
|
+
function createSiwcNonce(random = randomBytes2) {
|
|
1076
|
+
return Buffer.from(random(32)).toString("base64url");
|
|
1077
|
+
}
|
|
1078
|
+
function buildSiwcAuthorizationUrl(pkce, nonce, hostId, clientId) {
|
|
1079
|
+
if (!HOST_ID.test(hostId) || clientId !== undefined && !CLIENT_ID.test(clientId) || !nonce || !pkce.state || !pkce.codeChallenge)
|
|
1080
|
+
return invalid();
|
|
1081
|
+
const url = new URL(SIWC_AUTH_URL);
|
|
1082
|
+
url.search = new URLSearchParams({
|
|
1083
|
+
client_id: clientId ?? "dynamic_agent_client",
|
|
1084
|
+
agent_name_hint: "Bungee",
|
|
1085
|
+
ext_agent_host_id: hostId,
|
|
1086
|
+
response_type: "code",
|
|
1087
|
+
redirect_uri: SIWC_REDIRECT_URI,
|
|
1088
|
+
resource: SIWC_RESOURCE,
|
|
1089
|
+
scope: SIWC_SCOPES.join(" "),
|
|
1090
|
+
state: pkce.state,
|
|
1091
|
+
nonce,
|
|
1092
|
+
code_challenge: pkce.codeChallenge,
|
|
1093
|
+
code_challenge_method: "S256"
|
|
1094
|
+
}).toString();
|
|
1095
|
+
return url.toString();
|
|
1096
|
+
}
|
|
1097
|
+
function parseSiwcCallbackUrl(raw, options) {
|
|
1098
|
+
let url;
|
|
1099
|
+
try {
|
|
1100
|
+
url = new URL(raw);
|
|
1101
|
+
} catch {
|
|
1102
|
+
return invalid("invalid_callback");
|
|
1103
|
+
}
|
|
1104
|
+
const expected = new URL(SIWC_REDIRECT_URI);
|
|
1105
|
+
const now = options.now ?? Date.now();
|
|
1106
|
+
if (raw.length > 4096 || raw.trim() !== raw || raw.split("?")[0] !== SIWC_REDIRECT_URI || url.origin !== expected.origin || url.pathname !== expected.pathname || url.username || url.password || url.hash || !Number.isFinite(now) || !Number.isFinite(options.issuedAt) || options.issuedAt < 0 || now < options.issuedAt || now - options.issuedAt > CODEX_CALLBACK_TTL_MS || [...url.searchParams.keys()].some((key) => url.searchParams.getAll(key).length !== 1) || url.searchParams.has("userinfo") || !options.expectedState || url.searchParams.get("state") !== options.expectedState || url.searchParams.has("error") || url.searchParams.has("error_description"))
|
|
1107
|
+
return invalid("invalid_callback");
|
|
1108
|
+
const code = url.searchParams.get("code");
|
|
1109
|
+
const clientId = url.searchParams.get("client_id");
|
|
1110
|
+
if (!code || code.trim() !== code || /[\u0000-\u001f\u007f]/.test(code) || !clientId || !CLIENT_ID.test(clientId) || options.expectedClientId !== undefined && clientId !== options.expectedClientId)
|
|
1111
|
+
return invalid("invalid_callback");
|
|
1112
|
+
return { code, clientId };
|
|
1113
|
+
}
|
|
1114
|
+
function jwtObject(part) {
|
|
1115
|
+
try {
|
|
1116
|
+
if (!/^[A-Za-z0-9_-]+$/.test(part))
|
|
1117
|
+
return invalid();
|
|
1118
|
+
const value = JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
1119
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
1120
|
+
return value;
|
|
1121
|
+
} catch {}
|
|
1122
|
+
return invalid();
|
|
1123
|
+
}
|
|
1124
|
+
async function verifySiwcIdToken(idToken, expected, options = {}) {
|
|
1125
|
+
if (!CLIENT_ID.test(expected.clientId) || idToken.length > 32768)
|
|
1126
|
+
return invalid();
|
|
1127
|
+
const parts = idToken.split(".");
|
|
1128
|
+
if (parts.length !== 3 || !/^[A-Za-z0-9_-]+$/.test(parts[2]))
|
|
1129
|
+
return invalid();
|
|
1130
|
+
const header = jwtObject(parts[0]);
|
|
1131
|
+
const payload = jwtObject(parts[1]);
|
|
1132
|
+
if (header.alg !== "RS256" || typeof header.kid !== "string" || !header.kid || header.crit !== undefined)
|
|
1133
|
+
return invalid();
|
|
1134
|
+
const jwks = await requestJson(SIWC_JWKS_URL, { method: "GET", headers: { accept: "application/json" } }, options);
|
|
1135
|
+
if (!Array.isArray(jwks.keys) || jwks.keys.length > 128)
|
|
1136
|
+
return invalid();
|
|
1137
|
+
const keys = jwks.keys.filter((key) => key && typeof key === "object" && key.kid === header.kid && key.kty === "RSA" && (key.alg === undefined || key.alg === "RS256") && (key.use === undefined || key.use === "sig") && (key.key_ops === undefined || Array.isArray(key.key_ops) && key.key_ops.includes("verify")) && key.d === undefined);
|
|
1138
|
+
if (keys.length !== 1)
|
|
1139
|
+
return invalid();
|
|
1140
|
+
try {
|
|
1141
|
+
const publicKey = createPublicKey({ key: keys[0], format: "jwk" });
|
|
1142
|
+
if (!verify("RSA-SHA256", Buffer.from(`${parts[0]}.${parts[1]}`), publicKey, Buffer.from(parts[2], "base64url")))
|
|
1143
|
+
return invalid();
|
|
1144
|
+
} catch {
|
|
1145
|
+
return invalid();
|
|
1146
|
+
}
|
|
1147
|
+
const aud = payload.aud;
|
|
1148
|
+
const now = expected.now ?? Date.now();
|
|
1149
|
+
if (payload.iss !== SIWC_ISSUER || !(aud === expected.clientId || Array.isArray(aud) && aud.every((item) => typeof item === "string") && aud.includes(expected.clientId)) || Array.isArray(aud) && aud.length > 1 && payload.azp !== expected.clientId || payload.azp !== undefined && payload.azp !== expected.clientId || typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || !Number.isFinite(now) || payload.exp * 1000 <= now || payload.nbf !== undefined && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf * 1000 > now) || typeof payload.sub !== "string" || !payload.sub || payload.sub.length > 512 || payload.sub.trim() !== payload.sub || /[\u0000-\u001f\u007f]/.test(payload.sub) || expected.nonce !== undefined && payload.nonce !== expected.nonce || expected.subject !== undefined && payload.sub !== expected.subject)
|
|
1150
|
+
return invalid();
|
|
1151
|
+
return { subject: payload.sub, email: typeof payload.email === "string" && payload.email.length <= 512 && !/[\u0000-\u001f\u007f]/.test(payload.email) ? payload.email : undefined };
|
|
1152
|
+
}
|
|
1153
|
+
function tokenString(record, key, fallback) {
|
|
1154
|
+
if (record[key] === undefined && fallback !== undefined)
|
|
1155
|
+
return fallback;
|
|
1156
|
+
const value = record[key];
|
|
1157
|
+
if (typeof value !== "string" || !value || value.trim() !== value || value.length > 32768 || /[\r\n\0]/.test(value))
|
|
1158
|
+
return invalid();
|
|
1159
|
+
return value;
|
|
1160
|
+
}
|
|
1161
|
+
async function siwcTokenSet(record, expected, options, previous) {
|
|
1162
|
+
const accessToken = tokenString(record, "access_token");
|
|
1163
|
+
const refreshToken = tokenString(record, "refresh_token", previous?.refreshToken);
|
|
1164
|
+
const idToken = tokenString(record, "id_token", previous?.idToken);
|
|
1165
|
+
if (record.token_type !== undefined && (typeof record.token_type !== "string" || record.token_type.toLowerCase() !== "bearer"))
|
|
1166
|
+
return invalid();
|
|
1167
|
+
if (typeof record.expires_in !== "number" || !Number.isFinite(record.expires_in) || record.expires_in <= 0 || record.expires_in * 1000 > Number.MAX_SAFE_INTEGER - Date.now())
|
|
1168
|
+
return invalid();
|
|
1169
|
+
const scopes = record.scope === undefined && previous?.siwc ? [...previous.siwc.scopes] : tokenString(record, "scope").split(/\s+/);
|
|
1170
|
+
const verified = record.id_token === undefined && previous?.siwc ? { subject: previous.siwc.subject, email: previous.identity?.email } : await verifySiwcIdToken(idToken, expected, options);
|
|
1171
|
+
const siwc = { clientId: expected.clientId, subject: verified.subject, scopes };
|
|
1172
|
+
if (!validSiwcMetadata(siwc))
|
|
1173
|
+
return invalid();
|
|
1174
|
+
return {
|
|
1175
|
+
accessToken,
|
|
1176
|
+
refreshToken,
|
|
1177
|
+
idToken,
|
|
1178
|
+
expiresIn: record.expires_in,
|
|
1179
|
+
expiresAt: Date.now() + record.expires_in * 1000,
|
|
1180
|
+
tokenType: "Bearer",
|
|
1181
|
+
identity: { email: verified.email },
|
|
1182
|
+
identityStatus: "parsed",
|
|
1183
|
+
siwc
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
async function exchangeSiwcCode(code, codeVerifier, clientId, nonce, options = {}) {
|
|
1187
|
+
if (!code || !codeVerifier || !nonce || !CLIENT_ID.test(clientId))
|
|
1188
|
+
return invalid();
|
|
1189
|
+
const body = new URLSearchParams({ grant_type: "authorization_code", client_id: clientId, code, code_verifier: codeVerifier, redirect_uri: SIWC_REDIRECT_URI, resource: SIWC_RESOURCE });
|
|
1190
|
+
const record = await requestJson(SIWC_TOKEN_URL, { method: "POST", headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" }, body: body.toString() }, options);
|
|
1191
|
+
return siwcTokenSet(record, { clientId, nonce }, options);
|
|
1192
|
+
}
|
|
1193
|
+
async function refreshSiwcToken(previous, options = {}) {
|
|
1194
|
+
if (!validSiwcMetadata(previous.siwc) || !previous.refreshToken)
|
|
1195
|
+
return invalid();
|
|
1196
|
+
const body = new URLSearchParams({ grant_type: "refresh_token", client_id: previous.siwc.clientId, refresh_token: previous.refreshToken, resource: SIWC_RESOURCE });
|
|
1197
|
+
const record = await requestJson(SIWC_TOKEN_URL, { method: "POST", headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded" }, body: body.toString() }, options, true);
|
|
1198
|
+
return siwcTokenSet(record, { clientId: previous.siwc.clientId, subject: previous.siwc.subject }, options, previous);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
522
1201
|
// plugins/chatgpt-oauth/server/accounts.ts
|
|
523
|
-
import { randomUUID } from "crypto";
|
|
1202
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
524
1203
|
var ACCOUNT_STORE_KEY = "accounts.v1";
|
|
525
1204
|
var ACCOUNT_SCHEMA_VERSION = 1;
|
|
526
1205
|
|
|
@@ -553,6 +1232,7 @@ function copyIdentity(value) {
|
|
|
553
1232
|
function copyAccount(value) {
|
|
554
1233
|
return {
|
|
555
1234
|
...value,
|
|
1235
|
+
siwc: value.siwc && { ...value.siwc, scopes: [...value.siwc.scopes] },
|
|
556
1236
|
identity: copyIdentity(value.identity),
|
|
557
1237
|
refreshLock: value.refreshLock && { ...value.refreshLock },
|
|
558
1238
|
autoResetAttempts: value.autoResetAttempts?.map((item) => ({ ...item }))
|
|
@@ -565,7 +1245,7 @@ function validStoredAccount(value) {
|
|
|
565
1245
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
566
1246
|
return false;
|
|
567
1247
|
const account = value;
|
|
568
|
-
return typeof account.id === "string" && account.id.length > 0 && account.id.length <= 128 && typeof account.label === "string" && account.label.length > 0 && account.label.length <= MAX_TEXT && ["active", "disabled", "reauth_required", "revoked"].includes(account.status) && Number.isSafeInteger(account.generation) && account.generation > 0 && (account.loginFence === undefined || Number.isSafeInteger(account.loginFence) && account.loginFence > 0) && (account.credentialValid === undefined || typeof account.credentialValid === "boolean") && (account.autoResetCredits === undefined || typeof account.autoResetCredits === "boolean") && validAutoResetAttempts(account.autoResetAttempts) && Number.isFinite(account.createdAt) && Number.isFinite(account.updatedAt);
|
|
1248
|
+
return typeof account.id === "string" && account.id.length > 0 && account.id.length <= 128 && typeof account.label === "string" && account.label.length > 0 && account.label.length <= MAX_TEXT && ["active", "disabled", "reauth_required", "revoked"].includes(account.status) && Number.isSafeInteger(account.generation) && account.generation > 0 && (account.loginFence === undefined || Number.isSafeInteger(account.loginFence) && account.loginFence > 0) && (account.credentialValid === undefined || typeof account.credentialValid === "boolean") && (account.autoResetCredits === undefined || typeof account.autoResetCredits === "boolean") && (account.siwc === undefined || validSiwcMetadata(account.siwc)) && validAutoResetAttempts(account.autoResetAttempts) && Number.isFinite(account.createdAt) && Number.isFinite(account.updatedAt);
|
|
569
1249
|
}
|
|
570
1250
|
function decode(value) {
|
|
571
1251
|
if (value === undefined)
|
|
@@ -601,6 +1281,7 @@ function statusReason(account) {
|
|
|
601
1281
|
function accountListItem(account) {
|
|
602
1282
|
const pending = account.autoResetAttempts?.find((item) => !item.completed);
|
|
603
1283
|
return {
|
|
1284
|
+
authType: account.siwc ? "siwc" : "codex",
|
|
604
1285
|
id: account.id,
|
|
605
1286
|
label: account.label,
|
|
606
1287
|
status: account.status,
|
|
@@ -608,10 +1289,25 @@ function accountListItem(account) {
|
|
|
608
1289
|
reason: statusReason(account),
|
|
609
1290
|
expiresAt: account.expiresAt,
|
|
610
1291
|
identity: copyIdentity(account.identity),
|
|
611
|
-
autoResetCredits: account.autoResetCredits === true,
|
|
612
|
-
pendingAutoReset:
|
|
1292
|
+
autoResetCredits: !account.siwc && account.autoResetCredits === true,
|
|
1293
|
+
pendingAutoReset: !account.siwc && pending ? { ...pending } : undefined
|
|
613
1294
|
};
|
|
614
1295
|
}
|
|
1296
|
+
function validateTokenIdentity(token) {
|
|
1297
|
+
if (token.identityStatus !== "parsed" || (token.siwc === undefined ? !token.identity?.accountId : !validSiwcMetadata(token.siwc))) {
|
|
1298
|
+
throw new AccountControlError("invalid_identity");
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function validateSiwcBinding(account, token) {
|
|
1302
|
+
if (Boolean(account.siwc) !== Boolean(token.siwc))
|
|
1303
|
+
throw new AccountControlError("identity_mismatch");
|
|
1304
|
+
if (account.siwc) {
|
|
1305
|
+
if (!validSiwcMetadata(token.siwc) || token.identityStatus !== "parsed")
|
|
1306
|
+
throw new AccountControlError("invalid_identity");
|
|
1307
|
+
if (account.siwc.clientId !== token.siwc.clientId || account.siwc.subject !== token.siwc.subject)
|
|
1308
|
+
throw new AccountControlError("identity_mismatch");
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
615
1311
|
|
|
616
1312
|
class AccountStore {
|
|
617
1313
|
secretStore;
|
|
@@ -646,6 +1342,19 @@ class AccountStore {
|
|
|
646
1342
|
async list() {
|
|
647
1343
|
return (await this.read()).aggregate.accounts.map(accountListItem);
|
|
648
1344
|
}
|
|
1345
|
+
async remove(id) {
|
|
1346
|
+
text(id, 128);
|
|
1347
|
+
await this.mutate((aggregate) => {
|
|
1348
|
+
aggregate.accounts = aggregate.accounts.filter((account) => account.id !== id);
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
async purgeRevoked() {
|
|
1352
|
+
if (!(await this.read()).aggregate.accounts.some((account) => account.status === "revoked"))
|
|
1353
|
+
return;
|
|
1354
|
+
await this.mutate((aggregate) => {
|
|
1355
|
+
aggregate.accounts = aggregate.accounts.filter((account) => account.status !== "revoked");
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
649
1358
|
async get(id) {
|
|
650
1359
|
const account = (await this.read()).aggregate.accounts.find((item) => item.id === id);
|
|
651
1360
|
if (!account)
|
|
@@ -654,14 +1363,13 @@ class AccountStore {
|
|
|
654
1363
|
}
|
|
655
1364
|
async create(label, token, canCommit = () => true) {
|
|
656
1365
|
text(label);
|
|
657
|
-
|
|
658
|
-
throw new AccountControlError("invalid_identity");
|
|
1366
|
+
validateTokenIdentity(token);
|
|
659
1367
|
return this.mutate((aggregate) => {
|
|
660
1368
|
if (aggregate.accounts.length >= MAX_ACCOUNTS)
|
|
661
1369
|
throw new AccountControlError("invalid_input");
|
|
662
1370
|
const now = Date.now();
|
|
663
1371
|
const account = {
|
|
664
|
-
id:
|
|
1372
|
+
id: randomUUID2(),
|
|
665
1373
|
label,
|
|
666
1374
|
status: token.expiresAt === undefined ? "reauth_required" : "active",
|
|
667
1375
|
generation: 1,
|
|
@@ -673,7 +1381,8 @@ class AccountStore {
|
|
|
673
1381
|
refreshToken: text(token.refreshToken, MAX_TOKEN),
|
|
674
1382
|
idToken: optionalText(token.idToken, MAX_TOKEN),
|
|
675
1383
|
expiresAt: token.expiresAt,
|
|
676
|
-
identity: copyIdentity(token.identity)
|
|
1384
|
+
identity: copyIdentity(token.identity),
|
|
1385
|
+
siwc: token.siwc && { ...token.siwc, scopes: [...token.siwc.scopes] }
|
|
677
1386
|
};
|
|
678
1387
|
aggregate.accounts.push(account);
|
|
679
1388
|
return copyAccount(account);
|
|
@@ -697,7 +1406,9 @@ class AccountStore {
|
|
|
697
1406
|
if (token.identity?.accountId && account.identity?.accountId && token.identity.accountId !== account.identity.accountId) {
|
|
698
1407
|
throw new AccountControlError("identity_mismatch");
|
|
699
1408
|
}
|
|
1409
|
+
validateSiwcBinding(account, token);
|
|
700
1410
|
const now = Date.now();
|
|
1411
|
+
account.siwc = token.siwc && { ...token.siwc, scopes: [...token.siwc.scopes] };
|
|
701
1412
|
account.accessToken = text(token.accessToken, MAX_TOKEN);
|
|
702
1413
|
account.refreshToken = text(token.refreshToken, MAX_TOKEN);
|
|
703
1414
|
if (token.idToken !== undefined && token.identityStatus !== "invalid")
|
|
@@ -739,9 +1450,9 @@ class AccountStore {
|
|
|
739
1450
|
if (account.generation !== expected.generation || account.loginFence !== expected.loginFence) {
|
|
740
1451
|
throw new AccountControlError("stale_refresh");
|
|
741
1452
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (account.identity?.accountId && token.identity
|
|
1453
|
+
validateTokenIdentity(token);
|
|
1454
|
+
validateSiwcBinding(account, token);
|
|
1455
|
+
if (account.identity?.accountId && token.identity?.accountId !== account.identity.accountId) {
|
|
745
1456
|
throw new AccountControlError("identity_mismatch");
|
|
746
1457
|
}
|
|
747
1458
|
account.accessToken = text(token.accessToken, MAX_TOKEN);
|
|
@@ -758,6 +1469,7 @@ class AccountStore {
|
|
|
758
1469
|
account.status = "active";
|
|
759
1470
|
}
|
|
760
1471
|
account.identity = copyIdentity(token.identity);
|
|
1472
|
+
account.siwc = token.siwc && { ...token.siwc, scopes: [...token.siwc.scopes] };
|
|
761
1473
|
account.generation += 1;
|
|
762
1474
|
account.updatedAt = Date.now();
|
|
763
1475
|
delete account.refreshLock;
|
|
@@ -769,22 +1481,12 @@ class AccountStore {
|
|
|
769
1481
|
const account = aggregate.accounts.find((item) => item.id === id);
|
|
770
1482
|
if (!account)
|
|
771
1483
|
throw new AccountControlError("not_found");
|
|
772
|
-
if (account.status === "revoked")
|
|
773
|
-
if (status === "revoked")
|
|
774
|
-
return copyAccount(account);
|
|
1484
|
+
if (account.status === "revoked")
|
|
775
1485
|
throw new AccountControlError("revoked");
|
|
776
|
-
}
|
|
777
1486
|
account.status = status;
|
|
778
1487
|
account.generation += 1;
|
|
779
1488
|
account.updatedAt = Date.now();
|
|
780
1489
|
delete account.refreshLock;
|
|
781
|
-
if (status === "revoked") {
|
|
782
|
-
delete account.accessToken;
|
|
783
|
-
delete account.refreshToken;
|
|
784
|
-
delete account.idToken;
|
|
785
|
-
delete account.expiresAt;
|
|
786
|
-
account.credentialValid = false;
|
|
787
|
-
}
|
|
788
1490
|
return copyAccount(account);
|
|
789
1491
|
});
|
|
790
1492
|
}
|
|
@@ -810,6 +1512,8 @@ class AccountStore {
|
|
|
810
1512
|
throw new AccountControlError("not_found");
|
|
811
1513
|
if (account.status === "revoked")
|
|
812
1514
|
throw new AccountControlError("revoked");
|
|
1515
|
+
if (account.siwc)
|
|
1516
|
+
throw new AccountControlError("unsupported_operation");
|
|
813
1517
|
account.autoResetCredits = enabled;
|
|
814
1518
|
account.updatedAt = Date.now();
|
|
815
1519
|
return copyAccount(account);
|
|
@@ -818,7 +1522,7 @@ class AccountStore {
|
|
|
818
1522
|
async claimAutoReset(id, credit, now) {
|
|
819
1523
|
return this.mutate((aggregate) => {
|
|
820
1524
|
const account = aggregate.accounts.find((item) => item.id === id);
|
|
821
|
-
if (!account || !accountListItem(account).available || account.autoResetCredits !== true)
|
|
1525
|
+
if (!account || account.siwc || !accountListItem(account).available || account.autoResetCredits !== true)
|
|
822
1526
|
return;
|
|
823
1527
|
const attempts = account.autoResetAttempts ?? [];
|
|
824
1528
|
if (attempts.some((item) => !item.completed || item.creditId === credit.id))
|
|
@@ -826,7 +1530,7 @@ class AccountStore {
|
|
|
826
1530
|
account.autoResetAttempts = attempts.filter((item) => !item.completed || item.expiresAt === undefined || item.expiresAt > now);
|
|
827
1531
|
if (account.autoResetAttempts.length >= MAX_AUTO_RESET_ATTEMPTS || credit.expiresAt === undefined || credit.expiresAt <= now)
|
|
828
1532
|
return;
|
|
829
|
-
const attempt = { creditId: credit.id, redeemRequestId:
|
|
1533
|
+
const attempt = { creditId: credit.id, redeemRequestId: randomUUID2(), expiresAt: credit.expiresAt, completed: false };
|
|
830
1534
|
account.autoResetAttempts.push(attempt);
|
|
831
1535
|
account.updatedAt = Date.now();
|
|
832
1536
|
return { ...attempt };
|
|
@@ -837,6 +1541,8 @@ class AccountStore {
|
|
|
837
1541
|
const account = aggregate.accounts.find((item) => item.id === id);
|
|
838
1542
|
if (!account)
|
|
839
1543
|
throw new AccountControlError("not_found");
|
|
1544
|
+
if (account.siwc)
|
|
1545
|
+
throw new AccountControlError("unsupported_operation");
|
|
840
1546
|
const attempts = account.autoResetAttempts ?? [];
|
|
841
1547
|
const pending = attempts.find((item) => !item.completed);
|
|
842
1548
|
if (pending) {
|
|
@@ -945,7 +1651,7 @@ class AccountStore {
|
|
|
945
1651
|
}
|
|
946
1652
|
|
|
947
1653
|
// plugins/chatgpt-oauth/server/sessions.ts
|
|
948
|
-
import { randomUUID as
|
|
1654
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
949
1655
|
class LoginSessionError extends Error {
|
|
950
1656
|
code;
|
|
951
1657
|
name = "LoginSessionError";
|
|
@@ -988,7 +1694,7 @@ class LoginSessionManager {
|
|
|
988
1694
|
this.hostSignal.addEventListener("abort", onHostAbort, { once: true });
|
|
989
1695
|
const issuedAt = this.now();
|
|
990
1696
|
const session = {
|
|
991
|
-
id:
|
|
1697
|
+
id: randomUUID3(),
|
|
992
1698
|
kind,
|
|
993
1699
|
state: "pending",
|
|
994
1700
|
issuedAt,
|
|
@@ -1076,6 +1782,19 @@ class LoginSessionManager {
|
|
|
1076
1782
|
session.codeVerifier = pkce.codeVerifier;
|
|
1077
1783
|
return { sessionId: session.id, authorizationUrl: buildCodexAuthorizationUrl(pkce), expiresAt: session.expiresAt };
|
|
1078
1784
|
}
|
|
1785
|
+
startSiwc(hostId, accountRef, clientId, fence) {
|
|
1786
|
+
if (accountRef !== undefined && (typeof accountRef !== "string" || !accountRef || accountRef.length > 128))
|
|
1787
|
+
throw new LoginSessionError("invalid_input");
|
|
1788
|
+
const pkce = createPKCE(this.deps.random);
|
|
1789
|
+
const nonce = createSiwcNonce(this.deps.random);
|
|
1790
|
+
const authorizationUrl = buildSiwcAuthorizationUrl(pkce, nonce, hostId, clientId);
|
|
1791
|
+
const session = this.newSession("siwc", accountRef, fence, CODEX_CALLBACK_TTL_MS);
|
|
1792
|
+
session.stateValue = pkce.state;
|
|
1793
|
+
session.codeVerifier = pkce.codeVerifier;
|
|
1794
|
+
session.nonce = nonce;
|
|
1795
|
+
session.clientId = clientId;
|
|
1796
|
+
return { sessionId: session.id, authorizationUrl, expiresAt: session.expiresAt };
|
|
1797
|
+
}
|
|
1079
1798
|
setFence(id, fence) {
|
|
1080
1799
|
const session = this.get(id);
|
|
1081
1800
|
if (session.state !== "pending")
|
|
@@ -1188,6 +1907,24 @@ class LoginSessionManager {
|
|
|
1188
1907
|
throw error;
|
|
1189
1908
|
}
|
|
1190
1909
|
}
|
|
1910
|
+
async completeSiwc(id, callbackUrl, commit) {
|
|
1911
|
+
const session = this.claim(id, "siwc");
|
|
1912
|
+
try {
|
|
1913
|
+
const callback = parseSiwcCallbackUrl(callbackUrl, { expectedState: session.stateValue, expectedClientId: session.clientId, issuedAt: session.issuedAt, now: this.now() });
|
|
1914
|
+
const token = await exchangeSiwcCode(callback.code, session.codeVerifier, callback.clientId, session.nonce, safeOptions(this.deps, session.controller.signal));
|
|
1915
|
+
return await this.finish(session, token, commit);
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
if (session.controller.signal.aborted && session.state === "cancelled")
|
|
1918
|
+
throw new LoginSessionError("cancelled");
|
|
1919
|
+
if (session.state === "exchanging") {
|
|
1920
|
+
session.state = session.controller.signal.aborted ? "cancelled" : "failed";
|
|
1921
|
+
session.errorCode = session.controller.signal.aborted ? "cancelled" : "login_failed";
|
|
1922
|
+
if (session.state === "failed")
|
|
1923
|
+
throw new LoginSessionError("failed");
|
|
1924
|
+
}
|
|
1925
|
+
throw error;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1191
1928
|
cancel(id) {
|
|
1192
1929
|
const session = this.get(id);
|
|
1193
1930
|
if (session.state === "success" || session.state === "cancelled" || session.state === "failed" || session.state === "committing")
|
|
@@ -1956,6 +2693,7 @@ class AutoResetScheduler {
|
|
|
1956
2693
|
|
|
1957
2694
|
// plugins/chatgpt-oauth/server/control.ts
|
|
1958
2695
|
var CHATGPT_TARGET = "https://chatgpt.com";
|
|
2696
|
+
var SIWC_TARGET = "https://api.openai.com";
|
|
1959
2697
|
var CONTROL_HOST_CONTRACT_GAP = "ControlRpcContext.binding.bindingOptions.accountRef is required and host-resolved; RPC payload is never used as a fallback.";
|
|
1960
2698
|
|
|
1961
2699
|
class ControlError extends Error {
|
|
@@ -1982,7 +2720,8 @@ function safeError(error) {
|
|
|
1982
2720
|
identity_mismatch: "identity_mismatch",
|
|
1983
2721
|
invalid_identity: "invalid_identity",
|
|
1984
2722
|
disposed: "disposed",
|
|
1985
|
-
stale_refresh: "stale_refresh"
|
|
2723
|
+
stale_refresh: "stale_refresh",
|
|
2724
|
+
unsupported_operation: "unsupported_operation"
|
|
1986
2725
|
};
|
|
1987
2726
|
return new ControlError(map[error.code] ?? "refresh_failed");
|
|
1988
2727
|
}
|
|
@@ -2147,6 +2886,11 @@ function lease(account) {
|
|
|
2147
2886
|
throw new ControlError(account.status);
|
|
2148
2887
|
if (!account.accessToken)
|
|
2149
2888
|
throw new ControlError("reauth_required");
|
|
2889
|
+
if (account.siwc) {
|
|
2890
|
+
if (!validSiwcMetadata(account.siwc) || !Number.isFinite(account.expiresAt))
|
|
2891
|
+
throw new ControlError("reauth_required");
|
|
2892
|
+
return { version: account.generation, expiresAt: account.expiresAt, headers: { Authorization: `Bearer ${account.accessToken}` } };
|
|
2893
|
+
}
|
|
2150
2894
|
if (!account.identity?.accountId)
|
|
2151
2895
|
throw new ControlError("identity_missing");
|
|
2152
2896
|
if (!Number.isFinite(account.expiresAt))
|
|
@@ -2165,7 +2909,7 @@ class ChatgptControl {
|
|
|
2165
2909
|
accounts;
|
|
2166
2910
|
sessions;
|
|
2167
2911
|
now;
|
|
2168
|
-
workerOwner = `chatgpt-control-${
|
|
2912
|
+
workerOwner = `chatgpt-control-${randomUUID4()}`;
|
|
2169
2913
|
inFlight = new Map;
|
|
2170
2914
|
refreshControllers = new Map;
|
|
2171
2915
|
apiLifetime = new AbortController;
|
|
@@ -2184,7 +2928,7 @@ class ChatgptControl {
|
|
|
2184
2928
|
now: this.now,
|
|
2185
2929
|
timeoutMs: deps.timeoutMs,
|
|
2186
2930
|
hostSignal: this.apiLifetime.signal,
|
|
2187
|
-
credential: (id, signal) => this.credential(id, signal),
|
|
2931
|
+
credential: (id, signal) => this.credential(id, signal, "codex"),
|
|
2188
2932
|
rejectAccess: (id, version) => this.accounts.rejectAccess(id, version)
|
|
2189
2933
|
});
|
|
2190
2934
|
this.autoReset = new AutoResetScheduler(this.accounts, this.usage, this.apiLifetime.signal, this.now, deps.autoResetTimer);
|
|
@@ -2197,6 +2941,12 @@ class ChatgptControl {
|
|
|
2197
2941
|
host.signal.addEventListener("abort", this.abortListener, { once: true });
|
|
2198
2942
|
this.api = this.buildApi();
|
|
2199
2943
|
this.rpc = this.buildRpc();
|
|
2944
|
+
host.services?.rpc?.publish(businessRpc, Object.fromEntries(this.rpc.map((declaration) => [declaration.name, (payload, context) => {
|
|
2945
|
+
const frame = host.resolveRpcCallee?.(context.callee);
|
|
2946
|
+
if (!frame?.binding || !frame.attempt || frame.binding.plugin !== "chatgpt-oauth")
|
|
2947
|
+
throw new ControlError("binding_options_unavailable");
|
|
2948
|
+
return declaration.invoke(payload, { ...host, binding: frame.binding, attempt: { ...frame.attempt, signal: AbortSignal.any([frame.attempt.signal, context.signal]) } });
|
|
2949
|
+
}])));
|
|
2200
2950
|
}
|
|
2201
2951
|
assertAlive(signal) {
|
|
2202
2952
|
if (this.disposed || this.host.signal.aborted)
|
|
@@ -2206,7 +2956,7 @@ class ChatgptControl {
|
|
|
2206
2956
|
}
|
|
2207
2957
|
async start() {
|
|
2208
2958
|
this.assertAlive();
|
|
2209
|
-
await this.accounts.
|
|
2959
|
+
await this.accounts.purgeRevoked();
|
|
2210
2960
|
this.assertAlive();
|
|
2211
2961
|
this.autoReset.start();
|
|
2212
2962
|
}
|
|
@@ -2229,7 +2979,8 @@ class ChatgptControl {
|
|
|
2229
2979
|
}
|
|
2230
2980
|
const generation = lock.account.generation;
|
|
2231
2981
|
try {
|
|
2232
|
-
const
|
|
2982
|
+
const refreshOptions = { fetchImpl: this.deps.fetchImpl, signal, timeoutMs: this.deps.timeoutMs };
|
|
2983
|
+
const token = lock.account.siwc ? await refreshSiwcToken({ accessToken: lock.account.accessToken, refreshToken: lock.account.refreshToken, idToken: lock.account.idToken, identity: lock.account.identity, identityStatus: "parsed", siwc: lock.account.siwc }, refreshOptions) : await refreshCodexToken(lock.account.refreshToken, refreshOptions);
|
|
2233
2984
|
this.assertAlive(signal);
|
|
2234
2985
|
const updated = await this.accounts.replaceCredentials(id, token, { owner: this.workerOwner, generation });
|
|
2235
2986
|
this.assertAlive(signal);
|
|
@@ -2238,12 +2989,14 @@ class ChatgptControl {
|
|
|
2238
2989
|
if (this.disposed || this.host.signal.aborted)
|
|
2239
2990
|
throw new ControlError("disposed");
|
|
2240
2991
|
const oauthCode = error.kind;
|
|
2241
|
-
if (oauthCode === "refresh_token_reused") {
|
|
2992
|
+
if (oauthCode === "refresh_token_reused" || lock.account.siwc && (error.upstreamCode === "invalid_grant" || oauthCode === "invalid_response" || error instanceof AccountControlError && ["identity_mismatch", "invalid_identity"].includes(error.code))) {
|
|
2242
2993
|
const fenced = await this.accounts.failRefreshReauth(id, this.workerOwner, generation).catch(() => false);
|
|
2243
2994
|
if (!fenced) {
|
|
2244
2995
|
const latest = await this.accounts.get(id).catch(() => {
|
|
2245
2996
|
return;
|
|
2246
2997
|
});
|
|
2998
|
+
if (!latest)
|
|
2999
|
+
throw new ControlError("not_found");
|
|
2247
3000
|
if (latest?.status === "revoked")
|
|
2248
3001
|
throw new ControlError("revoked");
|
|
2249
3002
|
if (latest?.status === "disabled")
|
|
@@ -2260,6 +3013,8 @@ class ChatgptControl {
|
|
|
2260
3013
|
const latest = await this.accounts.get(id).catch(() => {
|
|
2261
3014
|
return;
|
|
2262
3015
|
});
|
|
3016
|
+
if (!latest)
|
|
3017
|
+
throw new ControlError("not_found");
|
|
2263
3018
|
if (latest?.status === "revoked")
|
|
2264
3019
|
throw new ControlError("revoked");
|
|
2265
3020
|
if (latest?.status === "disabled")
|
|
@@ -2274,10 +3029,38 @@ class ChatgptControl {
|
|
|
2274
3029
|
}
|
|
2275
3030
|
throw new ControlError("refresh_in_progress");
|
|
2276
3031
|
}
|
|
2277
|
-
async
|
|
3032
|
+
async requireKind(id, kind, error = "source_mismatch") {
|
|
3033
|
+
const account = await this.accounts.get(id);
|
|
3034
|
+
if (Boolean(account.siwc) !== (kind === "siwc"))
|
|
3035
|
+
throw new ControlError(error);
|
|
3036
|
+
return account;
|
|
3037
|
+
}
|
|
3038
|
+
async bindingAccount(context) {
|
|
3039
|
+
const id = accountRefFromBinding(context);
|
|
3040
|
+
const source = context.binding.contributionId;
|
|
3041
|
+
if (source === "chatgpt-siwc")
|
|
3042
|
+
await this.requireKind(id, "siwc");
|
|
3043
|
+
else if (source === "chatgpt" || source === "upstream")
|
|
3044
|
+
await this.requireKind(id, "codex");
|
|
3045
|
+
else
|
|
3046
|
+
throw new ControlError("source_mismatch");
|
|
3047
|
+
return id;
|
|
3048
|
+
}
|
|
3049
|
+
async draft(context, kind) {
|
|
3050
|
+
const body = await this.readJson(context);
|
|
3051
|
+
exactKeys(body, ["accountRef"]);
|
|
3052
|
+
const account = await this.requireKind(requiredText(body.accountRef, 128), kind);
|
|
3053
|
+
if (!accountListItem(account).available)
|
|
3054
|
+
throw new ControlError(account.status === "active" ? "reauth_required" : account.status);
|
|
3055
|
+
const draft = { target: kind === "siwc" ? SIWC_TARGET : CHATGPT_TARGET, bindingOptions: { accountRef: account.id } };
|
|
3056
|
+
return jsonResponse(draft);
|
|
3057
|
+
}
|
|
3058
|
+
async credential(id, signal, kind) {
|
|
2278
3059
|
this.assertAlive(signal);
|
|
2279
3060
|
const account = await this.accounts.get(id);
|
|
2280
3061
|
this.assertAlive(signal);
|
|
3062
|
+
if (kind === "codex" && account.siwc)
|
|
3063
|
+
throw new ControlError("unsupported_operation");
|
|
2281
3064
|
if (account.status !== "active")
|
|
2282
3065
|
throw new ControlError(account.status);
|
|
2283
3066
|
if (!isRefreshable(account, this.now())) {
|
|
@@ -2318,6 +3101,8 @@ class ChatgptControl {
|
|
|
2318
3101
|
return readRequestJson(context.request, context.requestSignal, context.signal, this.apiLifetime.signal);
|
|
2319
3102
|
}
|
|
2320
3103
|
async startDevice(accountRef) {
|
|
3104
|
+
if (accountRef !== undefined)
|
|
3105
|
+
await this.requireKind(accountRef, "codex");
|
|
2321
3106
|
const started = await this.sessions.startDevice(accountRef, () => this.loginFence(accountRef));
|
|
2322
3107
|
this.sessions.completeDevice(started.sessionId, (token, info, guard) => this.commitLogin(token, info, guard)).catch(() => {
|
|
2323
3108
|
return;
|
|
@@ -2342,11 +3127,17 @@ class ChatgptControl {
|
|
|
2342
3127
|
};
|
|
2343
3128
|
return [
|
|
2344
3129
|
{ path: "/accounts", methods: ["GET"], handler: "listAccounts", invoke: invoke(async () => jsonResponse({ accounts: await this.accounts.list() })) },
|
|
2345
|
-
{ path: "/accounts/
|
|
3130
|
+
{ path: "/accounts/codex", methods: ["GET"], handler: "listCodexAccounts", invoke: invoke(async () => jsonResponse({ accounts: (await this.accounts.list()).filter((account) => account.authType === "codex") })) },
|
|
3131
|
+
{ path: "/accounts/siwc", methods: ["GET"], handler: "listSiwcAccounts", invoke: invoke(async () => jsonResponse({ accounts: (await this.accounts.list()).filter((account) => account.authType === "siwc") })) },
|
|
3132
|
+
{ path: "/accounts/usage", methods: ["GET"], handler: "getAccountUsage", invoke: invoke(async (context) => {
|
|
3133
|
+
const id = this.accountRefQuery(context.request);
|
|
3134
|
+
return jsonResponse(await this.usage.get(id, context.requestSignal));
|
|
3135
|
+
}) },
|
|
2346
3136
|
{ path: "/accounts/usage/reset", methods: ["POST"], handler: "resetAccountUsage", invoke: invoke(async (context) => {
|
|
2347
3137
|
const body = await this.readJson(context);
|
|
2348
3138
|
exactKeys(body, ["accountRef", "redeemRequestId", "creditId"]);
|
|
2349
3139
|
const accountRef = requiredText(body.accountRef, 128);
|
|
3140
|
+
await this.requireKind(accountRef, "codex", "unsupported_operation");
|
|
2350
3141
|
const redeemRequestId = requiredText(body.redeemRequestId, 128);
|
|
2351
3142
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(redeemRequestId))
|
|
2352
3143
|
throw new ControlError("invalid_input");
|
|
@@ -2373,14 +3164,18 @@ class ChatgptControl {
|
|
|
2373
3164
|
throw new ControlError("invalid_input");
|
|
2374
3165
|
return jsonResponse({ account: accountListItem(await this.accounts.setAutoResetCredits(requiredText(body.accountRef, 128), body.enabled)) });
|
|
2375
3166
|
}) },
|
|
2376
|
-
{ path: "/accounts/draft", methods: ["POST"], handler: "createDraft", invoke: invoke(
|
|
3167
|
+
{ path: "/accounts/draft", methods: ["POST"], handler: "createDraft", invoke: invoke((context) => this.draft(context, "codex")) },
|
|
3168
|
+
{ path: "/accounts/siwc/draft", methods: ["POST"], handler: "createSiwcDraft", invoke: invoke((context) => this.draft(context, "siwc")) },
|
|
3169
|
+
{ path: "/login/siwc", methods: ["POST"], handler: "startSiwcLogin", invoke: invoke(async (context) => {
|
|
2377
3170
|
const body = await this.readJson(context);
|
|
2378
3171
|
exactKeys(body, ["accountRef"]);
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
3172
|
+
const accountRef = optionalText3(body.accountRef, 128);
|
|
3173
|
+
const account = accountRef === undefined ? undefined : await this.requireKind(accountRef, "siwc");
|
|
3174
|
+
const hostId = await getSiwcHostId(this.host.secretStore, () => !this.disposed && !this.host.signal.aborted);
|
|
3175
|
+
this.assertAlive(context.requestSignal);
|
|
3176
|
+
const fence = await this.loginFence(accountRef);
|
|
3177
|
+
this.assertAlive(context.requestSignal);
|
|
3178
|
+
return jsonResponse(this.sessions.startSiwc(hostId, accountRef, account?.siwc?.clientId, fence));
|
|
2384
3179
|
}) },
|
|
2385
3180
|
{ path: "/login/device", methods: ["POST"], handler: "startDeviceLogin", invoke: invoke(async (context) => {
|
|
2386
3181
|
const body = await this.readJson(context);
|
|
@@ -2391,6 +3186,8 @@ class ChatgptControl {
|
|
|
2391
3186
|
const body = await this.readJson(context);
|
|
2392
3187
|
exactKeys(body, ["accountRef"]);
|
|
2393
3188
|
const accountRef = optionalText3(body.accountRef, 128);
|
|
3189
|
+
if (accountRef !== undefined)
|
|
3190
|
+
await this.requireKind(accountRef, "codex");
|
|
2394
3191
|
const started = this.sessions.startPkce(accountRef);
|
|
2395
3192
|
try {
|
|
2396
3193
|
if (accountRef !== undefined)
|
|
@@ -2415,7 +3212,8 @@ class ChatgptControl {
|
|
|
2415
3212
|
const status = this.sessions.status(sessionId);
|
|
2416
3213
|
if (status.state !== "pending")
|
|
2417
3214
|
return jsonResponse(status);
|
|
2418
|
-
const
|
|
3215
|
+
const complete = status.kind === "siwc" ? this.sessions.completeSiwc.bind(this.sessions) : this.sessions.completePkce.bind(this.sessions);
|
|
3216
|
+
const result = await complete(sessionId, requiredText(body.callbackUrl, 4096), (token, info, guard) => this.commitLogin(token, info, guard));
|
|
2419
3217
|
return jsonResponse({ account: result.result });
|
|
2420
3218
|
}) },
|
|
2421
3219
|
{ path: "/login/device/complete", methods: ["POST"], handler: "completeDeviceLogin", invoke: invoke(async (context) => {
|
|
@@ -2457,9 +3255,9 @@ class ChatgptControl {
|
|
|
2457
3255
|
const body = await this.readJson(context);
|
|
2458
3256
|
exactKeys(body, ["accountRef"]);
|
|
2459
3257
|
const accountRef = requiredText(body.accountRef, 128);
|
|
2460
|
-
|
|
3258
|
+
await this.accounts.remove(accountRef);
|
|
2461
3259
|
this.usage.invalidateAccount(accountRef);
|
|
2462
|
-
return jsonResponse({
|
|
3260
|
+
return jsonResponse({ deleted: true });
|
|
2463
3261
|
}) }
|
|
2464
3262
|
];
|
|
2465
3263
|
}
|
|
@@ -2468,7 +3266,7 @@ class ChatgptControl {
|
|
|
2468
3266
|
{ name: "getCredential", handler: "getCredential", invoke: async (_payload, context) => {
|
|
2469
3267
|
try {
|
|
2470
3268
|
this.assertAlive(context.attempt.signal);
|
|
2471
|
-
return await this.credential(
|
|
3269
|
+
return await this.credential(await this.bindingAccount(context), context.attempt.signal);
|
|
2472
3270
|
} catch (error) {
|
|
2473
3271
|
throw safeError(error);
|
|
2474
3272
|
}
|
|
@@ -2478,7 +3276,7 @@ class ChatgptControl {
|
|
|
2478
3276
|
this.assertAlive(context.attempt.signal);
|
|
2479
3277
|
const body = record(payload);
|
|
2480
3278
|
exactKeys(body, ["version"]);
|
|
2481
|
-
return { rejected: await this.accounts.rejectAccess(
|
|
3279
|
+
return { rejected: await this.accounts.rejectAccess(await this.bindingAccount(context), requiredVersion(body.version)) };
|
|
2482
3280
|
} catch (error) {
|
|
2483
3281
|
throw safeError(error);
|
|
2484
3282
|
}
|
|
@@ -2504,10 +3302,14 @@ function createControl(context, dependencies = {}) {
|
|
|
2504
3302
|
}
|
|
2505
3303
|
var api = Object.freeze([
|
|
2506
3304
|
{ path: "/accounts", methods: ["GET"], handler: "listAccounts" },
|
|
3305
|
+
{ path: "/accounts/codex", methods: ["GET"], handler: "listCodexAccounts" },
|
|
3306
|
+
{ path: "/accounts/siwc", methods: ["GET"], handler: "listSiwcAccounts" },
|
|
2507
3307
|
{ path: "/accounts/usage", methods: ["GET"], handler: "getAccountUsage" },
|
|
2508
3308
|
{ path: "/accounts/usage/reset", methods: ["POST"], handler: "resetAccountUsage" },
|
|
2509
3309
|
{ path: "/accounts/auto-reset", methods: ["POST"], handler: "setAutoResetCredits" },
|
|
2510
3310
|
{ path: "/accounts/draft", methods: ["POST"], handler: "createDraft" },
|
|
3311
|
+
{ path: "/accounts/siwc/draft", methods: ["POST"], handler: "createSiwcDraft" },
|
|
3312
|
+
{ path: "/login/siwc", methods: ["POST"], handler: "startSiwcLogin" },
|
|
2511
3313
|
{ path: "/login/device", methods: ["POST"], handler: "startDeviceLogin" },
|
|
2512
3314
|
{ path: "/login/pkce", methods: ["POST"], handler: "startPkceLogin" },
|
|
2513
3315
|
{ path: "/login/status", methods: ["GET"], handler: "getLoginStatus" },
|
|
@@ -2527,6 +3329,7 @@ export {
|
|
|
2527
3329
|
CHATGPT_TARGET,
|
|
2528
3330
|
CONTROL_HOST_CONTRACT_GAP,
|
|
2529
3331
|
ControlError,
|
|
3332
|
+
SIWC_TARGET,
|
|
2530
3333
|
api,
|
|
2531
3334
|
controlApi,
|
|
2532
3335
|
controlRpc,
|
|
@@ -2535,4 +3338,4 @@ export {
|
|
|
2535
3338
|
rpc
|
|
2536
3339
|
};
|
|
2537
3340
|
|
|
2538
|
-
//# debugId=
|
|
3341
|
+
//# debugId=08B71A7DC584C3AB64756E2164756E21
|