@theokit/sdk 4.11.0 → 4.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/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from 'module';
3
3
  import { createHash, randomUUID, randomBytes } from 'crypto';
4
4
  import { readFile, stat, readdir, mkdir, rename, open, unlink, statfs, access } from 'fs/promises';
5
5
  import { join, dirname, relative, resolve, sep, isAbsolute } from 'path';
6
- import { existsSync, rmSync, mkdirSync, renameSync, readFileSync, realpathSync, lstatSync, readlinkSync, readdirSync } from 'fs';
6
+ import { existsSync, rmSync, mkdirSync, renameSync, readFileSync, realpathSync, lstatSync, readlinkSync, readdirSync, statSync, chmodSync, openSync, writeFileSync, fsyncSync, closeSync, unlinkSync } from 'fs';
7
7
  import { AsyncLocalStorage } from 'async_hooks';
8
8
  import { homedir } from 'os';
9
9
  import { spawn } from 'child_process';
@@ -573,10 +573,10 @@ function buildToolPrompt(prompt) {
573
573
  Respond by calling the \`output\` tool with the structured answer that matches the schema.`;
574
574
  }
575
575
  function setupStructuredOutput(schema, maxRetries) {
576
- const z11 = requireZod();
576
+ const z12 = requireZod();
577
577
  const jsonSchema = toJsonSchema(schema, { unrepresentable: "any" });
578
578
  return {
579
- z: z11,
579
+ z: z12,
580
580
  jsonSchema,
581
581
  maxRetries: maxRetries ?? 1,
582
582
  initialUsage: { inputTokens: 0, outputTokens: 0 }
@@ -13972,6 +13972,358 @@ var OPENAI = {
13972
13972
  hostname: "api.openai.com",
13973
13973
  fallbackModels: ["gpt-4o", "gpt-4o-mini"]
13974
13974
  };
13975
+ function credentialHome(config, env = {}) {
13976
+ const override = config.homeEnvVar !== void 0 ? env[config.homeEnvVar]?.trim() : void 0;
13977
+ return override !== void 0 && override.length > 0 ? override : join(config.home, config.dirName);
13978
+ }
13979
+ function authFilePath(config, env = {}) {
13980
+ return join(credentialHome(config, env), config.fileName);
13981
+ }
13982
+ var CredentialError = class extends Error {
13983
+ constructor(message) {
13984
+ super(message);
13985
+ this.name = "CredentialError";
13986
+ }
13987
+ };
13988
+ var apiFileSchema = z.object({
13989
+ type: z.literal("api").optional(),
13990
+ provider: z.string().min(1).optional(),
13991
+ api_key: z.string()
13992
+ }).strict();
13993
+ var oauthFileSchema = z.object({
13994
+ type: z.literal("oauth"),
13995
+ provider: z.string().min(1),
13996
+ access: z.string().min(1),
13997
+ refresh: z.string().min(1),
13998
+ expires: z.number(),
13999
+ account_id: z.string().optional()
14000
+ }).strict();
14001
+ var fileSchema = z.union([oauthFileSchema, apiFileSchema]);
14002
+ function assertSecureModes(dirPath, path) {
14003
+ const dirMode = statSync(dirPath).mode & 511;
14004
+ if ((dirMode & 18) !== 0) {
14005
+ throw new CredentialError(
14006
+ `${dirPath} is writable by other users (mode ${dirMode.toString(8)}), so the credential file inside it can be replaced. Fix it with: chmod 700 ${dirPath}`
14007
+ );
14008
+ }
14009
+ const mode = statSync(path).mode & 511;
14010
+ if ((mode & 63) !== 0) {
14011
+ throw new CredentialError(
14012
+ `${path} is readable by other users (mode ${mode.toString(8)}). A credential file must not be. Fix it with: chmod 600 ${path}`
14013
+ );
14014
+ }
14015
+ }
14016
+ function describeUnionError(parsed, err, path) {
14017
+ const looksOAuth = typeof parsed === "object" && parsed !== null && parsed.type === "oauth";
14018
+ const specific = looksOAuth ? oauthFileSchema.safeParse(parsed) : apiFileSchema.safeParse(parsed);
14019
+ let issue;
14020
+ if (!specific.success) {
14021
+ issue = specific.error.issues[0];
14022
+ } else if (err instanceof z.ZodError) {
14023
+ issue = err.issues[0];
14024
+ }
14025
+ return new CredentialError(
14026
+ `${path}: ${issue?.message ?? String(err)} [${issue?.path.join(".") || "root"}]`
14027
+ );
14028
+ }
14029
+ function parseStoredFile(raw, path) {
14030
+ let parsed;
14031
+ try {
14032
+ parsed = JSON.parse(raw);
14033
+ } catch {
14034
+ throw new CredentialError(
14035
+ `${path} is not valid JSON. Expected: {"provider": "<name>", "api_key": "..."}`
14036
+ );
14037
+ }
14038
+ try {
14039
+ return fileSchema.parse(parsed);
14040
+ } catch (err) {
14041
+ throw describeUnionError(parsed, err, path);
14042
+ }
14043
+ }
14044
+ function readAuthFile(config, env = {}) {
14045
+ const path = authFilePath(config, env);
14046
+ let raw;
14047
+ try {
14048
+ raw = readFileSync(path, "utf8");
14049
+ } catch (err) {
14050
+ if (err.code === "ENOENT") return void 0;
14051
+ throw new CredentialError(`cannot read ${path}: ${err.message}`);
14052
+ }
14053
+ assertSecureModes(credentialHome(config, env), path);
14054
+ return parseStoredFile(raw, path);
14055
+ }
14056
+ function readStoredOAuth(config, env = {}) {
14057
+ const stored = readAuthFile(config, env);
14058
+ return stored !== void 0 && stored.type === "oauth" ? stored : void 0;
14059
+ }
14060
+ function isOAuthWrite(c) {
14061
+ return "type" in c && c.type === "oauth";
14062
+ }
14063
+ function buildStorePayload(cred) {
14064
+ if (isOAuthWrite(cred)) {
14065
+ if (cred.access.length === 0 || cred.refresh.length === 0) {
14066
+ throw new CredentialError(
14067
+ "refusing to write an oauth credential with an empty access/refresh token"
14068
+ );
14069
+ }
14070
+ return {
14071
+ type: "oauth",
14072
+ provider: cred.provider,
14073
+ access: cred.access,
14074
+ refresh: cred.refresh,
14075
+ expires: cred.expires,
14076
+ ...cred.account_id !== void 0 ? { account_id: cred.account_id } : {}
14077
+ };
14078
+ }
14079
+ if (typeof cred.apiKey !== "string" || cred.apiKey.length === 0) {
14080
+ throw new CredentialError("refusing to write an empty API key");
14081
+ }
14082
+ return { provider: cred.provider, api_key: cred.apiKey };
14083
+ }
14084
+ function writeCredential(cred, config, env = {}) {
14085
+ const payload = buildStorePayload(cred);
14086
+ const dir = credentialHome(config, env);
14087
+ mkdirSync(dir, { recursive: true, mode: 448 });
14088
+ chmodSync(dir, 448);
14089
+ const path = authFilePath(config, env);
14090
+ const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
14091
+ try {
14092
+ const fd = openSync(tmp, "wx", 384);
14093
+ try {
14094
+ writeFileSync(fd, `${JSON.stringify(payload, null, 2)}
14095
+ `);
14096
+ fsyncSync(fd);
14097
+ } finally {
14098
+ closeSync(fd);
14099
+ }
14100
+ chmodSync(tmp, 384);
14101
+ renameSync(tmp, path);
14102
+ } catch (err) {
14103
+ try {
14104
+ unlinkSync(tmp);
14105
+ } catch {
14106
+ }
14107
+ throw new CredentialError(`cannot write ${path}: ${err.message}`);
14108
+ }
14109
+ return path;
14110
+ }
14111
+
14112
+ // src/server/auth/errors.ts
14113
+ var AuthCallbackError = class extends Error {
14114
+ name = "AuthCallbackError";
14115
+ code;
14116
+ constructor(code, message) {
14117
+ super(message ?? `OAuth callback error: ${code}`);
14118
+ this.code = code;
14119
+ }
14120
+ };
14121
+
14122
+ // src/internal/auth/oauth-engine.ts
14123
+ var REFRESH_SKEW_MS = 6e4;
14124
+ function parseTokenResponse(body, now) {
14125
+ const b = body;
14126
+ if (typeof b.access_token !== "string" || b.access_token.length === 0) {
14127
+ throw new AuthCallbackError(
14128
+ "oauth_token_exchange_failed",
14129
+ "token response had no access_token"
14130
+ );
14131
+ }
14132
+ if (typeof b.refresh_token !== "string" || b.refresh_token.length === 0) {
14133
+ throw new AuthCallbackError(
14134
+ "oauth_token_exchange_failed",
14135
+ "token response had no refresh_token"
14136
+ );
14137
+ }
14138
+ const expiresIn = typeof b.expires_in === "number" ? b.expires_in : 3600;
14139
+ return {
14140
+ access: b.access_token,
14141
+ refresh: b.refresh_token,
14142
+ expires: now + expiresIn * 1e3,
14143
+ ...typeof b.account_id === "string" ? { accountId: b.account_id } : {}
14144
+ };
14145
+ }
14146
+ async function postGrant(config, form, deps) {
14147
+ let res;
14148
+ try {
14149
+ res = await deps.fetch(config.tokenEndpoint, {
14150
+ method: "POST",
14151
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
14152
+ body: new URLSearchParams(form).toString()
14153
+ });
14154
+ } catch (err) {
14155
+ throw new AuthCallbackError(
14156
+ "oauth_token_exchange_failed",
14157
+ `token endpoint request failed: ${err.message}`
14158
+ );
14159
+ }
14160
+ if (!res.ok) {
14161
+ throw new AuthCallbackError(
14162
+ "oauth_token_exchange_failed",
14163
+ `token endpoint returned HTTP ${res.status}`
14164
+ );
14165
+ }
14166
+ let json;
14167
+ try {
14168
+ json = await res.json();
14169
+ } catch {
14170
+ throw new AuthCallbackError("oauth_token_exchange_failed", "token response was not valid JSON");
14171
+ }
14172
+ return parseTokenResponse(json, deps.now());
14173
+ }
14174
+ function refreshOAuthTokens(config, refresh, deps) {
14175
+ return postGrant(
14176
+ config,
14177
+ { grant_type: "refresh_token", refresh_token: refresh, client_id: config.clientId },
14178
+ deps
14179
+ );
14180
+ }
14181
+ function persistOAuthTokens(provider, tokens, store, env = {}) {
14182
+ return writeCredential(
14183
+ {
14184
+ type: "oauth",
14185
+ provider,
14186
+ access: tokens.access,
14187
+ refresh: tokens.refresh,
14188
+ expires: tokens.expires,
14189
+ ...tokens.accountId !== void 0 ? { account_id: tokens.accountId } : {}
14190
+ },
14191
+ store,
14192
+ env
14193
+ );
14194
+ }
14195
+ var inFlightRefresh = /* @__PURE__ */ new Map();
14196
+ async function ensureFreshCredential(resolved, opts, deps) {
14197
+ if (resolved.kind !== "oauth") return resolved;
14198
+ const now = deps.now();
14199
+ if (resolved.expiresAt !== void 0 && resolved.expiresAt > now + REFRESH_SKEW_MS) {
14200
+ return resolved;
14201
+ }
14202
+ const env = opts.env ?? {};
14203
+ const path = authFilePath(opts.store, env);
14204
+ let refresh = inFlightRefresh.get(path);
14205
+ if (refresh === void 0) {
14206
+ refresh = (async () => {
14207
+ const stored = readStoredOAuth(opts.store, env);
14208
+ if (stored === void 0) {
14209
+ throw new AuthCallbackError(
14210
+ "oauth_token_exchange_failed",
14211
+ "no stored oauth credential to refresh"
14212
+ );
14213
+ }
14214
+ const fresh2 = await refreshOAuthTokens(opts.config, stored.refresh, deps);
14215
+ const merged = {
14216
+ ...fresh2,
14217
+ accountId: fresh2.accountId ?? stored.account_id
14218
+ };
14219
+ persistOAuthTokens(resolved.provider, merged, opts.store, env);
14220
+ return merged;
14221
+ })();
14222
+ inFlightRefresh.set(path, refresh);
14223
+ refresh.finally(() => inFlightRefresh.delete(path)).catch(() => {
14224
+ });
14225
+ }
14226
+ const fresh = await refresh;
14227
+ return {
14228
+ kind: "oauth",
14229
+ provider: resolved.provider,
14230
+ apiKey: fresh.access,
14231
+ source: resolved.source,
14232
+ inferred: false,
14233
+ expiresAt: fresh.expires
14234
+ };
14235
+ }
14236
+
14237
+ // src/internal/auth/resolve-credential.ts
14238
+ async function resolveOAuth(stored, path, opts, env) {
14239
+ if (stored.provider !== opts.provider) return void 0;
14240
+ const base = {
14241
+ kind: "oauth",
14242
+ provider: opts.provider,
14243
+ apiKey: stored.access,
14244
+ source: path,
14245
+ inferred: false,
14246
+ expiresAt: stored.expires
14247
+ };
14248
+ if (opts.oauth === void 0) return base;
14249
+ const deps = {
14250
+ fetch: opts.deps?.fetch ?? fetch,
14251
+ now: opts.deps?.now ?? (() => Date.now())
14252
+ };
14253
+ return ensureFreshCredential(base, { config: opts.oauth, store: opts.store, env }, deps);
14254
+ }
14255
+ async function resolveCredential(opts) {
14256
+ const env = opts.env ?? {};
14257
+ const stored = readAuthFile(opts.store, env);
14258
+ if (stored === void 0) return void 0;
14259
+ const path = authFilePath(opts.store, env);
14260
+ if (stored.type === "oauth") {
14261
+ return resolveOAuth(stored, path, opts, env);
14262
+ }
14263
+ if (stored.api_key.length === 0) return void 0;
14264
+ if (stored.provider !== opts.provider) return void 0;
14265
+ return {
14266
+ kind: "api",
14267
+ provider: opts.provider,
14268
+ apiKey: stored.api_key,
14269
+ source: path,
14270
+ inferred: false
14271
+ };
14272
+ }
14273
+
14274
+ // src/internal/providers/builtin/openai-chatgpt.ts
14275
+ var DEFAULT_STORE = {
14276
+ home: homedir(),
14277
+ dirName: ".theokit",
14278
+ fileName: "auth.json",
14279
+ homeEnvVar: "THEOKIT_HOME"
14280
+ };
14281
+ var OPENAI_OAUTH_CONFIG = {
14282
+ provider: "openai",
14283
+ authorizeEndpoint: "https://auth.openai.com/oauth/authorize",
14284
+ tokenEndpoint: "https://auth.openai.com/oauth/token",
14285
+ clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
14286
+ scopes: ["openid", "profile", "email", "offline_access"],
14287
+ redirectUri: "https://auth.openai.com/deviceauth/callback"
14288
+ };
14289
+ function codexFetch() {
14290
+ return (async (input, init) => {
14291
+ const env = process.env;
14292
+ const resolved = await resolveCredential({
14293
+ provider: "openai",
14294
+ store: DEFAULT_STORE,
14295
+ oauth: OPENAI_OAUTH_CONFIG,
14296
+ env
14297
+ });
14298
+ if (resolved === void 0) {
14299
+ throw new Error(
14300
+ 'openai-chatgpt: no ChatGPT credential found \u2014 run the OpenAI device login (e.g. "/login openai") first.'
14301
+ );
14302
+ }
14303
+ const accountId = readStoredOAuth(DEFAULT_STORE, env)?.account_id;
14304
+ const headers = new Headers(init?.headers);
14305
+ headers.set("authorization", `Bearer ${resolved.apiKey}`);
14306
+ if (accountId !== void 0) headers.set("ChatGPT-Account-Id", accountId);
14307
+ return fetch(input, { ...init, headers });
14308
+ });
14309
+ }
14310
+ var OPENAI_CHATGPT = {
14311
+ name: "openai-chatgpt",
14312
+ apiMode: "responses_api",
14313
+ authType: "oauth_device_code",
14314
+ baseUrl: "https://chatgpt.com/backend-api/codex",
14315
+ envVars: [],
14316
+ fallbackModels: [
14317
+ "openai-chatgpt/gpt-5.4",
14318
+ "openai-chatgpt/gpt-5.4-mini",
14319
+ "openai-chatgpt/gpt-5.5"
14320
+ ],
14321
+ extraHeaders: { originator: "codex_cli_rs" },
14322
+ transform: {
14323
+ // Only `fetch` (async) can await the credential refresh; `headers` is sync and cannot.
14324
+ fetch: () => codexFetch()
14325
+ }
14326
+ };
13975
14327
 
13976
14328
  // src/internal/providers/builtin/openrouter.ts
13977
14329
  var OPENROUTER = {
@@ -14025,6 +14377,7 @@ function registerBuiltins() {
14025
14377
  registered2 = true;
14026
14378
  registerProvider(ANTHROPIC);
14027
14379
  registerProvider(OPENAI);
14380
+ registerProvider(OPENAI_CHATGPT);
14028
14381
  registerProvider(OPENROUTER);
14029
14382
  registerProvider(GEMINI);
14030
14383
  registerProvider(OLLAMA);
@@ -16288,6 +16641,15 @@ function selectTransport(profile, apiKey) {
16288
16641
  const ctx = { apiKey };
16289
16642
  return { fetch: profile.transform.fetch?.(ctx), headers: profile.transform.headers?.(ctx) };
16290
16643
  };
16644
+ const assertOAuthResolved = (t) => {
16645
+ if (apiKey !== "__oauth_lazy_token__") return;
16646
+ const auth = t.headers?.authorization ?? t.headers?.Authorization;
16647
+ if (t.fetch === void 0 && auth === void 0) {
16648
+ throw new ConfigurationError(
16649
+ `provider "${profile.name}" uses OAuth (authType: ${profile.authType}) but no credential was resolved. Register a ProviderProfile.transform whose fetch (or headers.authorization) supplies the bearer \u2014 typically via resolveCredential() from "@theokit/sdk/auth".`
16650
+ );
16651
+ }
16652
+ };
16291
16653
  if (profile.apiMode === "chat_completions") {
16292
16654
  if (profile.name === "ollama") {
16293
16655
  const ollamaBase = process.env.OLLAMA_HOST ?? profile.baseUrl;
@@ -16305,6 +16667,7 @@ function selectTransport(profile, apiKey) {
16305
16667
  const envOverride = resolveBaseUrlEnvOverride(profile.name);
16306
16668
  if (envOverride !== void 0) opts.baseUrl = envOverride;
16307
16669
  const t = applyTransform();
16670
+ assertOAuthResolved(t);
16308
16671
  if (t.fetch !== void 0) opts.fetch = t.fetch;
16309
16672
  const merged = profile.extraHeaders !== void 0 || t.headers !== void 0 ? { ...profile.extraHeaders, ...t.headers } : void 0;
16310
16673
  if (merged !== void 0) opts.extraHeaders = merged;
@@ -16325,6 +16688,7 @@ function selectTransport(profile, apiKey) {
16325
16688
  }
16326
16689
  if (profile.apiMode === "responses_api") {
16327
16690
  const t = applyTransform();
16691
+ assertOAuthResolved(t);
16328
16692
  const mergedHeaders = profile.extraHeaders !== void 0 || t.headers !== void 0 ? { ...profile.extraHeaders, ...t.headers } : void 0;
16329
16693
  return new ResponsesApiClient({
16330
16694
  apiKey,