@jeffusion/bungee-core 5.11.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.
@@ -506,7 +506,7 @@ var businessRpc = defineRpcService({ id: "chatgpt-oauth.credentials.v1", version
506
506
  } });
507
507
 
508
508
  // plugins/chatgpt-oauth/server/control.ts
509
- import { randomUUID as randomUUID3 } from "crypto";
509
+ import { randomUUID as randomUUID4 } from "crypto";
510
510
 
511
511
  // plugins/chatgpt-oauth/server/oauth.ts
512
512
  import { createHash, randomBytes } from "crypto";
@@ -1025,8 +1025,181 @@ function extractCodexIdentity(idToken) {
1025
1025
  };
1026
1026
  }
1027
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
+
1028
1201
  // plugins/chatgpt-oauth/server/accounts.ts
1029
- import { randomUUID } from "crypto";
1202
+ import { randomUUID as randomUUID2 } from "crypto";
1030
1203
  var ACCOUNT_STORE_KEY = "accounts.v1";
1031
1204
  var ACCOUNT_SCHEMA_VERSION = 1;
1032
1205
 
@@ -1059,6 +1232,7 @@ function copyIdentity(value) {
1059
1232
  function copyAccount(value) {
1060
1233
  return {
1061
1234
  ...value,
1235
+ siwc: value.siwc && { ...value.siwc, scopes: [...value.siwc.scopes] },
1062
1236
  identity: copyIdentity(value.identity),
1063
1237
  refreshLock: value.refreshLock && { ...value.refreshLock },
1064
1238
  autoResetAttempts: value.autoResetAttempts?.map((item) => ({ ...item }))
@@ -1071,7 +1245,7 @@ function validStoredAccount(value) {
1071
1245
  if (typeof value !== "object" || value === null || Array.isArray(value))
1072
1246
  return false;
1073
1247
  const account = value;
1074
- 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);
1075
1249
  }
1076
1250
  function decode(value) {
1077
1251
  if (value === undefined)
@@ -1107,6 +1281,7 @@ function statusReason(account) {
1107
1281
  function accountListItem(account) {
1108
1282
  const pending = account.autoResetAttempts?.find((item) => !item.completed);
1109
1283
  return {
1284
+ authType: account.siwc ? "siwc" : "codex",
1110
1285
  id: account.id,
1111
1286
  label: account.label,
1112
1287
  status: account.status,
@@ -1114,10 +1289,25 @@ function accountListItem(account) {
1114
1289
  reason: statusReason(account),
1115
1290
  expiresAt: account.expiresAt,
1116
1291
  identity: copyIdentity(account.identity),
1117
- autoResetCredits: account.autoResetCredits === true,
1118
- pendingAutoReset: pending && { ...pending }
1292
+ autoResetCredits: !account.siwc && account.autoResetCredits === true,
1293
+ pendingAutoReset: !account.siwc && pending ? { ...pending } : undefined
1119
1294
  };
1120
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
+ }
1121
1311
 
1122
1312
  class AccountStore {
1123
1313
  secretStore;
@@ -1152,6 +1342,19 @@ class AccountStore {
1152
1342
  async list() {
1153
1343
  return (await this.read()).aggregate.accounts.map(accountListItem);
1154
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
+ }
1155
1358
  async get(id) {
1156
1359
  const account = (await this.read()).aggregate.accounts.find((item) => item.id === id);
1157
1360
  if (!account)
@@ -1160,14 +1363,13 @@ class AccountStore {
1160
1363
  }
1161
1364
  async create(label, token, canCommit = () => true) {
1162
1365
  text(label);
1163
- if (token.identityStatus !== "parsed" || !token.identity?.accountId)
1164
- throw new AccountControlError("invalid_identity");
1366
+ validateTokenIdentity(token);
1165
1367
  return this.mutate((aggregate) => {
1166
1368
  if (aggregate.accounts.length >= MAX_ACCOUNTS)
1167
1369
  throw new AccountControlError("invalid_input");
1168
1370
  const now = Date.now();
1169
1371
  const account = {
1170
- id: randomUUID(),
1372
+ id: randomUUID2(),
1171
1373
  label,
1172
1374
  status: token.expiresAt === undefined ? "reauth_required" : "active",
1173
1375
  generation: 1,
@@ -1179,7 +1381,8 @@ class AccountStore {
1179
1381
  refreshToken: text(token.refreshToken, MAX_TOKEN),
1180
1382
  idToken: optionalText(token.idToken, MAX_TOKEN),
1181
1383
  expiresAt: token.expiresAt,
1182
- identity: copyIdentity(token.identity)
1384
+ identity: copyIdentity(token.identity),
1385
+ siwc: token.siwc && { ...token.siwc, scopes: [...token.siwc.scopes] }
1183
1386
  };
1184
1387
  aggregate.accounts.push(account);
1185
1388
  return copyAccount(account);
@@ -1203,7 +1406,9 @@ class AccountStore {
1203
1406
  if (token.identity?.accountId && account.identity?.accountId && token.identity.accountId !== account.identity.accountId) {
1204
1407
  throw new AccountControlError("identity_mismatch");
1205
1408
  }
1409
+ validateSiwcBinding(account, token);
1206
1410
  const now = Date.now();
1411
+ account.siwc = token.siwc && { ...token.siwc, scopes: [...token.siwc.scopes] };
1207
1412
  account.accessToken = text(token.accessToken, MAX_TOKEN);
1208
1413
  account.refreshToken = text(token.refreshToken, MAX_TOKEN);
1209
1414
  if (token.idToken !== undefined && token.identityStatus !== "invalid")
@@ -1245,9 +1450,9 @@ class AccountStore {
1245
1450
  if (account.generation !== expected.generation || account.loginFence !== expected.loginFence) {
1246
1451
  throw new AccountControlError("stale_refresh");
1247
1452
  }
1248
- if (token.identityStatus !== "parsed" || !token.identity?.accountId)
1249
- throw new AccountControlError("invalid_identity");
1250
- if (account.identity?.accountId && token.identity.accountId !== account.identity.accountId) {
1453
+ validateTokenIdentity(token);
1454
+ validateSiwcBinding(account, token);
1455
+ if (account.identity?.accountId && token.identity?.accountId !== account.identity.accountId) {
1251
1456
  throw new AccountControlError("identity_mismatch");
1252
1457
  }
1253
1458
  account.accessToken = text(token.accessToken, MAX_TOKEN);
@@ -1264,6 +1469,7 @@ class AccountStore {
1264
1469
  account.status = "active";
1265
1470
  }
1266
1471
  account.identity = copyIdentity(token.identity);
1472
+ account.siwc = token.siwc && { ...token.siwc, scopes: [...token.siwc.scopes] };
1267
1473
  account.generation += 1;
1268
1474
  account.updatedAt = Date.now();
1269
1475
  delete account.refreshLock;
@@ -1275,22 +1481,12 @@ class AccountStore {
1275
1481
  const account = aggregate.accounts.find((item) => item.id === id);
1276
1482
  if (!account)
1277
1483
  throw new AccountControlError("not_found");
1278
- if (account.status === "revoked") {
1279
- if (status === "revoked")
1280
- return copyAccount(account);
1484
+ if (account.status === "revoked")
1281
1485
  throw new AccountControlError("revoked");
1282
- }
1283
1486
  account.status = status;
1284
1487
  account.generation += 1;
1285
1488
  account.updatedAt = Date.now();
1286
1489
  delete account.refreshLock;
1287
- if (status === "revoked") {
1288
- delete account.accessToken;
1289
- delete account.refreshToken;
1290
- delete account.idToken;
1291
- delete account.expiresAt;
1292
- account.credentialValid = false;
1293
- }
1294
1490
  return copyAccount(account);
1295
1491
  });
1296
1492
  }
@@ -1316,6 +1512,8 @@ class AccountStore {
1316
1512
  throw new AccountControlError("not_found");
1317
1513
  if (account.status === "revoked")
1318
1514
  throw new AccountControlError("revoked");
1515
+ if (account.siwc)
1516
+ throw new AccountControlError("unsupported_operation");
1319
1517
  account.autoResetCredits = enabled;
1320
1518
  account.updatedAt = Date.now();
1321
1519
  return copyAccount(account);
@@ -1324,7 +1522,7 @@ class AccountStore {
1324
1522
  async claimAutoReset(id, credit, now) {
1325
1523
  return this.mutate((aggregate) => {
1326
1524
  const account = aggregate.accounts.find((item) => item.id === id);
1327
- if (!account || !accountListItem(account).available || account.autoResetCredits !== true)
1525
+ if (!account || account.siwc || !accountListItem(account).available || account.autoResetCredits !== true)
1328
1526
  return;
1329
1527
  const attempts = account.autoResetAttempts ?? [];
1330
1528
  if (attempts.some((item) => !item.completed || item.creditId === credit.id))
@@ -1332,7 +1530,7 @@ class AccountStore {
1332
1530
  account.autoResetAttempts = attempts.filter((item) => !item.completed || item.expiresAt === undefined || item.expiresAt > now);
1333
1531
  if (account.autoResetAttempts.length >= MAX_AUTO_RESET_ATTEMPTS || credit.expiresAt === undefined || credit.expiresAt <= now)
1334
1532
  return;
1335
- const attempt = { creditId: credit.id, redeemRequestId: randomUUID(), expiresAt: credit.expiresAt, completed: false };
1533
+ const attempt = { creditId: credit.id, redeemRequestId: randomUUID2(), expiresAt: credit.expiresAt, completed: false };
1336
1534
  account.autoResetAttempts.push(attempt);
1337
1535
  account.updatedAt = Date.now();
1338
1536
  return { ...attempt };
@@ -1343,6 +1541,8 @@ class AccountStore {
1343
1541
  const account = aggregate.accounts.find((item) => item.id === id);
1344
1542
  if (!account)
1345
1543
  throw new AccountControlError("not_found");
1544
+ if (account.siwc)
1545
+ throw new AccountControlError("unsupported_operation");
1346
1546
  const attempts = account.autoResetAttempts ?? [];
1347
1547
  const pending = attempts.find((item) => !item.completed);
1348
1548
  if (pending) {
@@ -1451,7 +1651,7 @@ class AccountStore {
1451
1651
  }
1452
1652
 
1453
1653
  // plugins/chatgpt-oauth/server/sessions.ts
1454
- import { randomUUID as randomUUID2 } from "crypto";
1654
+ import { randomUUID as randomUUID3 } from "crypto";
1455
1655
  class LoginSessionError extends Error {
1456
1656
  code;
1457
1657
  name = "LoginSessionError";
@@ -1494,7 +1694,7 @@ class LoginSessionManager {
1494
1694
  this.hostSignal.addEventListener("abort", onHostAbort, { once: true });
1495
1695
  const issuedAt = this.now();
1496
1696
  const session = {
1497
- id: randomUUID2(),
1697
+ id: randomUUID3(),
1498
1698
  kind,
1499
1699
  state: "pending",
1500
1700
  issuedAt,
@@ -1582,6 +1782,19 @@ class LoginSessionManager {
1582
1782
  session.codeVerifier = pkce.codeVerifier;
1583
1783
  return { sessionId: session.id, authorizationUrl: buildCodexAuthorizationUrl(pkce), expiresAt: session.expiresAt };
1584
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
+ }
1585
1798
  setFence(id, fence) {
1586
1799
  const session = this.get(id);
1587
1800
  if (session.state !== "pending")
@@ -1694,6 +1907,24 @@ class LoginSessionManager {
1694
1907
  throw error;
1695
1908
  }
1696
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
+ }
1697
1928
  cancel(id) {
1698
1929
  const session = this.get(id);
1699
1930
  if (session.state === "success" || session.state === "cancelled" || session.state === "failed" || session.state === "committing")
@@ -2462,6 +2693,7 @@ class AutoResetScheduler {
2462
2693
 
2463
2694
  // plugins/chatgpt-oauth/server/control.ts
2464
2695
  var CHATGPT_TARGET = "https://chatgpt.com";
2696
+ var SIWC_TARGET = "https://api.openai.com";
2465
2697
  var CONTROL_HOST_CONTRACT_GAP = "ControlRpcContext.binding.bindingOptions.accountRef is required and host-resolved; RPC payload is never used as a fallback.";
2466
2698
 
2467
2699
  class ControlError extends Error {
@@ -2488,7 +2720,8 @@ function safeError(error) {
2488
2720
  identity_mismatch: "identity_mismatch",
2489
2721
  invalid_identity: "invalid_identity",
2490
2722
  disposed: "disposed",
2491
- stale_refresh: "stale_refresh"
2723
+ stale_refresh: "stale_refresh",
2724
+ unsupported_operation: "unsupported_operation"
2492
2725
  };
2493
2726
  return new ControlError(map[error.code] ?? "refresh_failed");
2494
2727
  }
@@ -2653,6 +2886,11 @@ function lease(account) {
2653
2886
  throw new ControlError(account.status);
2654
2887
  if (!account.accessToken)
2655
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
+ }
2656
2894
  if (!account.identity?.accountId)
2657
2895
  throw new ControlError("identity_missing");
2658
2896
  if (!Number.isFinite(account.expiresAt))
@@ -2671,7 +2909,7 @@ class ChatgptControl {
2671
2909
  accounts;
2672
2910
  sessions;
2673
2911
  now;
2674
- workerOwner = `chatgpt-control-${randomUUID3()}`;
2912
+ workerOwner = `chatgpt-control-${randomUUID4()}`;
2675
2913
  inFlight = new Map;
2676
2914
  refreshControllers = new Map;
2677
2915
  apiLifetime = new AbortController;
@@ -2690,7 +2928,7 @@ class ChatgptControl {
2690
2928
  now: this.now,
2691
2929
  timeoutMs: deps.timeoutMs,
2692
2930
  hostSignal: this.apiLifetime.signal,
2693
- credential: (id, signal) => this.credential(id, signal),
2931
+ credential: (id, signal) => this.credential(id, signal, "codex"),
2694
2932
  rejectAccess: (id, version) => this.accounts.rejectAccess(id, version)
2695
2933
  });
2696
2934
  this.autoReset = new AutoResetScheduler(this.accounts, this.usage, this.apiLifetime.signal, this.now, deps.autoResetTimer);
@@ -2718,7 +2956,7 @@ class ChatgptControl {
2718
2956
  }
2719
2957
  async start() {
2720
2958
  this.assertAlive();
2721
- await this.accounts.read();
2959
+ await this.accounts.purgeRevoked();
2722
2960
  this.assertAlive();
2723
2961
  this.autoReset.start();
2724
2962
  }
@@ -2741,7 +2979,8 @@ class ChatgptControl {
2741
2979
  }
2742
2980
  const generation = lock.account.generation;
2743
2981
  try {
2744
- const token = await refreshCodexToken(lock.account.refreshToken, { fetchImpl: this.deps.fetchImpl, signal });
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);
2745
2984
  this.assertAlive(signal);
2746
2985
  const updated = await this.accounts.replaceCredentials(id, token, { owner: this.workerOwner, generation });
2747
2986
  this.assertAlive(signal);
@@ -2750,12 +2989,14 @@ class ChatgptControl {
2750
2989
  if (this.disposed || this.host.signal.aborted)
2751
2990
  throw new ControlError("disposed");
2752
2991
  const oauthCode = error.kind;
2753
- 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))) {
2754
2993
  const fenced = await this.accounts.failRefreshReauth(id, this.workerOwner, generation).catch(() => false);
2755
2994
  if (!fenced) {
2756
2995
  const latest = await this.accounts.get(id).catch(() => {
2757
2996
  return;
2758
2997
  });
2998
+ if (!latest)
2999
+ throw new ControlError("not_found");
2759
3000
  if (latest?.status === "revoked")
2760
3001
  throw new ControlError("revoked");
2761
3002
  if (latest?.status === "disabled")
@@ -2772,6 +3013,8 @@ class ChatgptControl {
2772
3013
  const latest = await this.accounts.get(id).catch(() => {
2773
3014
  return;
2774
3015
  });
3016
+ if (!latest)
3017
+ throw new ControlError("not_found");
2775
3018
  if (latest?.status === "revoked")
2776
3019
  throw new ControlError("revoked");
2777
3020
  if (latest?.status === "disabled")
@@ -2786,10 +3029,38 @@ class ChatgptControl {
2786
3029
  }
2787
3030
  throw new ControlError("refresh_in_progress");
2788
3031
  }
2789
- async credential(id, signal) {
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) {
2790
3059
  this.assertAlive(signal);
2791
3060
  const account = await this.accounts.get(id);
2792
3061
  this.assertAlive(signal);
3062
+ if (kind === "codex" && account.siwc)
3063
+ throw new ControlError("unsupported_operation");
2793
3064
  if (account.status !== "active")
2794
3065
  throw new ControlError(account.status);
2795
3066
  if (!isRefreshable(account, this.now())) {
@@ -2830,6 +3101,8 @@ class ChatgptControl {
2830
3101
  return readRequestJson(context.request, context.requestSignal, context.signal, this.apiLifetime.signal);
2831
3102
  }
2832
3103
  async startDevice(accountRef) {
3104
+ if (accountRef !== undefined)
3105
+ await this.requireKind(accountRef, "codex");
2833
3106
  const started = await this.sessions.startDevice(accountRef, () => this.loginFence(accountRef));
2834
3107
  this.sessions.completeDevice(started.sessionId, (token, info, guard) => this.commitLogin(token, info, guard)).catch(() => {
2835
3108
  return;
@@ -2854,11 +3127,17 @@ class ChatgptControl {
2854
3127
  };
2855
3128
  return [
2856
3129
  { path: "/accounts", methods: ["GET"], handler: "listAccounts", invoke: invoke(async () => jsonResponse({ accounts: await this.accounts.list() })) },
2857
- { path: "/accounts/usage", methods: ["GET"], handler: "getAccountUsage", invoke: invoke(async (context) => jsonResponse(await this.usage.get(this.accountRefQuery(context.request), context.requestSignal))) },
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
+ }) },
2858
3136
  { path: "/accounts/usage/reset", methods: ["POST"], handler: "resetAccountUsage", invoke: invoke(async (context) => {
2859
3137
  const body = await this.readJson(context);
2860
3138
  exactKeys(body, ["accountRef", "redeemRequestId", "creditId"]);
2861
3139
  const accountRef = requiredText(body.accountRef, 128);
3140
+ await this.requireKind(accountRef, "codex", "unsupported_operation");
2862
3141
  const redeemRequestId = requiredText(body.redeemRequestId, 128);
2863
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))
2864
3143
  throw new ControlError("invalid_input");
@@ -2885,14 +3164,18 @@ class ChatgptControl {
2885
3164
  throw new ControlError("invalid_input");
2886
3165
  return jsonResponse({ account: accountListItem(await this.accounts.setAutoResetCredits(requiredText(body.accountRef, 128), body.enabled)) });
2887
3166
  }) },
2888
- { path: "/accounts/draft", methods: ["POST"], handler: "createDraft", invoke: invoke(async (context) => {
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) => {
2889
3170
  const body = await this.readJson(context);
2890
3171
  exactKeys(body, ["accountRef"]);
2891
- const account = await this.accounts.get(requiredText(body.accountRef, 128));
2892
- if (!accountListItem(account).available)
2893
- throw new ControlError(account.status === "active" ? "reauth_required" : account.status);
2894
- const draft = { target: CHATGPT_TARGET, bindingOptions: { accountRef: account.id } };
2895
- return jsonResponse(draft);
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));
2896
3179
  }) },
2897
3180
  { path: "/login/device", methods: ["POST"], handler: "startDeviceLogin", invoke: invoke(async (context) => {
2898
3181
  const body = await this.readJson(context);
@@ -2903,6 +3186,8 @@ class ChatgptControl {
2903
3186
  const body = await this.readJson(context);
2904
3187
  exactKeys(body, ["accountRef"]);
2905
3188
  const accountRef = optionalText3(body.accountRef, 128);
3189
+ if (accountRef !== undefined)
3190
+ await this.requireKind(accountRef, "codex");
2906
3191
  const started = this.sessions.startPkce(accountRef);
2907
3192
  try {
2908
3193
  if (accountRef !== undefined)
@@ -2927,7 +3212,8 @@ class ChatgptControl {
2927
3212
  const status = this.sessions.status(sessionId);
2928
3213
  if (status.state !== "pending")
2929
3214
  return jsonResponse(status);
2930
- const result = await this.sessions.completePkce(sessionId, requiredText(body.callbackUrl, 4096), (token, info, guard) => this.commitLogin(token, info, guard));
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));
2931
3217
  return jsonResponse({ account: result.result });
2932
3218
  }) },
2933
3219
  { path: "/login/device/complete", methods: ["POST"], handler: "completeDeviceLogin", invoke: invoke(async (context) => {
@@ -2969,9 +3255,9 @@ class ChatgptControl {
2969
3255
  const body = await this.readJson(context);
2970
3256
  exactKeys(body, ["accountRef"]);
2971
3257
  const accountRef = requiredText(body.accountRef, 128);
2972
- const account = await this.accounts.setStatus(accountRef, "revoked");
3258
+ await this.accounts.remove(accountRef);
2973
3259
  this.usage.invalidateAccount(accountRef);
2974
- return jsonResponse({ account: accountListItem(account) });
3260
+ return jsonResponse({ deleted: true });
2975
3261
  }) }
2976
3262
  ];
2977
3263
  }
@@ -2980,7 +3266,7 @@ class ChatgptControl {
2980
3266
  { name: "getCredential", handler: "getCredential", invoke: async (_payload, context) => {
2981
3267
  try {
2982
3268
  this.assertAlive(context.attempt.signal);
2983
- return await this.credential(accountRefFromBinding(context), context.attempt.signal);
3269
+ return await this.credential(await this.bindingAccount(context), context.attempt.signal);
2984
3270
  } catch (error) {
2985
3271
  throw safeError(error);
2986
3272
  }
@@ -2990,7 +3276,7 @@ class ChatgptControl {
2990
3276
  this.assertAlive(context.attempt.signal);
2991
3277
  const body = record(payload);
2992
3278
  exactKeys(body, ["version"]);
2993
- return { rejected: await this.accounts.rejectAccess(accountRefFromBinding(context), requiredVersion(body.version)) };
3279
+ return { rejected: await this.accounts.rejectAccess(await this.bindingAccount(context), requiredVersion(body.version)) };
2994
3280
  } catch (error) {
2995
3281
  throw safeError(error);
2996
3282
  }
@@ -3016,10 +3302,14 @@ function createControl(context, dependencies = {}) {
3016
3302
  }
3017
3303
  var api = Object.freeze([
3018
3304
  { path: "/accounts", methods: ["GET"], handler: "listAccounts" },
3305
+ { path: "/accounts/codex", methods: ["GET"], handler: "listCodexAccounts" },
3306
+ { path: "/accounts/siwc", methods: ["GET"], handler: "listSiwcAccounts" },
3019
3307
  { path: "/accounts/usage", methods: ["GET"], handler: "getAccountUsage" },
3020
3308
  { path: "/accounts/usage/reset", methods: ["POST"], handler: "resetAccountUsage" },
3021
3309
  { path: "/accounts/auto-reset", methods: ["POST"], handler: "setAutoResetCredits" },
3022
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" },
3023
3313
  { path: "/login/device", methods: ["POST"], handler: "startDeviceLogin" },
3024
3314
  { path: "/login/pkce", methods: ["POST"], handler: "startPkceLogin" },
3025
3315
  { path: "/login/status", methods: ["GET"], handler: "getLoginStatus" },
@@ -3039,6 +3329,7 @@ export {
3039
3329
  CHATGPT_TARGET,
3040
3330
  CONTROL_HOST_CONTRACT_GAP,
3041
3331
  ControlError,
3332
+ SIWC_TARGET,
3042
3333
  api,
3043
3334
  controlApi,
3044
3335
  controlRpc,
@@ -3047,4 +3338,4 @@ export {
3047
3338
  rpc
3048
3339
  };
3049
3340
 
3050
- //# debugId=8A09446B1A0261F064756E2164756E21
3341
+ //# debugId=08B71A7DC584C3AB64756E2164756E21