feat: add app-owned Copilot and Grok subscription connections

This commit is contained in:
amosblomqvist
2026-10-25 00:27:52 +03:00
parent e8c81faad9
commit d6e317c6fe
49 changed files with 1709 additions and 248 deletions
+30 -1
View File
@@ -27,6 +27,8 @@
const { app, BrowserWindow, Menu, ipcMain, session, shell, dialog } = require("electron");
const { createChatGPT, fileStore } = require("./chatgpt.cjs");
const { attachChatGPTChild, trustedSender, chatGPTControl } = require("./chatgpt-ipc.cjs");
const { createSubscriptions, loadSubscriptionSDK } = require("./subscriptions.cjs");
const { attachSubscriptionChild, subscriptionControl } = require("./subscription-ipc.cjs");
const { desktopAPIHeaders } = require("./desktop-api.cjs");
const { randomBytes } = require("node:crypto");
const { spawn } = require("node:child_process");
@@ -134,6 +136,8 @@ if (process.argv.includes("--browser-service")) {
let serverProcess = null;
let chatgpt = null;
let chatgptChild = null;
let subscriptions = null;
let subscriptionChild = null;
// Per-launch request capability, never OAuth tokens and never renderer state.
let desktopAPISecret = null;
const trustedAppWindows = new Set();
@@ -222,6 +226,7 @@ function startServer(port) {
);
serverProcess = child;
chatgptChild = attachChatGPTChild(child, chatgpt);
subscriptionChild = attachSubscriptionChild(child, subscriptions);
const fwd = (buf) => buf.toString().trimEnd().split("\n").forEach((l) => l && log(`server: ${l}`));
child.stdout.on("data", fwd);
child.stderr.on("data", fwd);
@@ -230,7 +235,10 @@ function startServer(port) {
serverProcess = null;
chatgptChild?.dispose();
chatgptChild = null;
subscriptionChild?.dispose();
subscriptionChild = null;
chatgpt?.close();
subscriptions?.close();
});
resolve(child);
});
@@ -255,6 +263,7 @@ function waitUntilUp(url, tries = 150) {
function killServer() {
chatgpt?.close();
subscriptions?.close();
if (!serverProcess) return;
const child = serverProcess;
serverProcess = null;
@@ -569,6 +578,7 @@ function installMenu() {
async function main() {
app.on("window-all-closed", () => {
chatgpt?.pause();
subscriptions?.pause();
// macOS convention: the app stays resident, reactivated from the Dock.
if (process.platform !== "darwin") app.quit();
});
@@ -600,6 +610,16 @@ async function main() {
for (const win of BrowserWindow.getAllWindows()) win.webContents.send("lh:chatgpt:changed");
},
});
subscriptions = createSubscriptions({
directory: app.getPath("userData"),
loadSDK: () => loadSubscriptionSDK(path.dirname(serverEntry())),
openExternal: (target) => shell.openExternal(target),
onChange: (snapshot) => {
subscriptionChild?.send(snapshot);
for (const win of BrowserWindow.getAllWindows()) win.webContents.send("lh:subscriptions:changed");
},
});
await subscriptions.ready;
log(`starting server: ${serverEntry()}`);
log(`data dir: ${DATA_DIR()}${fs.existsSync(AUTH_FILE()) ? " · auth.json: present" : " · auth.json: ABSENT (no BYOK keys yet)"}`);
await startServer(new URL(url).port);
@@ -617,6 +637,15 @@ async function main() {
return chatGPTControl(command, chatgpt);
});
ipcMain.handle("lh:subscriptions:control", async (event, command) => {
if (!trustedAppWindows.has(BrowserWindow.fromWebContents(event.sender)) || !trustedSender(event, new URL(url).origin, BrowserWindow)) throw new Error("Untrusted desktop request.");
if (!subscriptions || !serverProcess?.connected) {
if (command?.op === "status") return [];
throw new Error("Subscription sign-in requires the desktop app's private server connection.");
}
return subscriptionControl(command, subscriptions);
});
installMenu();
// Auto-update (Wave C): wire the updater before the window so the
@@ -642,7 +671,7 @@ async function main() {
if (width < target) win.setBounds({ x, y, width: target, height });
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) { chatgpt?.resume(); createWindow(url); }
if (BrowserWindow.getAllWindows().length === 0) { chatgpt?.resume(); subscriptions?.resume(); createWindow(url); }
});
createWindow(url);
log(`window open: ${url}`);
+15
View File
@@ -43,6 +43,21 @@ contextBridge.exposeInMainWorld("lh", {
return () => ipcRenderer.removeListener("lh:chatgpt:changed", listener);
},
},
subscriptions: {
status: () => ipcRenderer.invoke("lh:subscriptions:control", { op: "status" }),
login: (providerId, enabled) => ipcRenderer.invoke("lh:subscriptions:control", { op: "login", providerId, enabled }),
cancel: (providerId) => ipcRenderer.invoke("lh:subscriptions:control", { op: "cancel", providerId }),
respond: (providerId, attemptId, promptId, value) => ipcRenderer.invoke("lh:subscriptions:control", { op: "respond", providerId, attemptId, promptId, value }),
open: (providerId, attemptId, actionId) => ipcRenderer.invoke("lh:subscriptions:control", { op: "open", providerId, attemptId, actionId }),
retry: (providerId) => ipcRenderer.invoke("lh:subscriptions:control", { op: "retry", providerId }),
disconnect: (providerId) => ipcRenderer.invoke("lh:subscriptions:control", { op: "disconnect", providerId }),
consent: (providerId, enabled) => ipcRenderer.invoke("lh:subscriptions:control", { op: "consent", providerId, enabled }),
onChange: (cb) => {
const listener = () => cb();
ipcRenderer.on("lh:subscriptions:changed", listener);
return () => ipcRenderer.removeListener("lh:subscriptions:changed", listener);
},
},
expandWindow: () => ipcRenderer.send("lh:expand-window"),
onZoom: (cb) => {
const listener = (_event, factor) => cb(factor);
+45
View File
@@ -0,0 +1,45 @@
"use strict";
const { PROVIDERS } = require("./subscription-store.cjs");
const { publicError } = require("./subscriptions.cjs");
function attachSubscriptionChild(child, controller) {
let disposed = false;
const send = (packet) => { if (!disposed && child.connected) { try { child.send(packet, () => {}); } catch { /* child exited */ } } };
const listener = async (message) => {
if (disposed || !child.connected || message?.channel !== "lh:subscriptions" || typeof message.id !== "string") return;
const { id, op } = message;
try {
if (!controller || !["snapshot", "lease"].includes(op)) throw new Error();
if (op === "lease" && (typeof message.background !== "boolean" || !validScope(message.scope))) throw new Error();
const value = op === "snapshot" ? await controller.snapshot() : await controller.lease(message.scope, message.background);
send({ channel: "lh:subscriptions", id, op, ok: true, value });
} catch (error) { send({ channel: "lh:subscriptions", id, op, ok: false, error: publicError(error) }); }
};
child.on("message", listener);
return {
send(snapshot) { send({ channel: "lh:subscriptions", op: "changed", snapshot }); },
dispose() { disposed = true; child.removeListener("message", listener); },
};
}
function validScope(scope) {
return scope && PROVIDERS.includes(scope.providerId) && typeof scope.connectionId === "string" && !!scope.connectionId && typeof scope.selectedModelId === "string" && !!scope.selectedModelId && [scope.epoch, scope.authorityRevision].every((n) => Number.isSafeInteger(n) && n >= 0);
}
/** Caller authenticates the exact trusted main-frame origin before dispatch. */
async function subscriptionControl(command, controller) {
try {
if (!controller || !command || typeof command !== "object") throw new Error();
if (command.op === "status") return await controller.status();
if (!PROVIDERS.includes(command.providerId)) throw new Error();
const id = command.providerId;
switch (command.op) {
case "login": return await controller.login(id, command.enabled);
case "cancel": return controller.cancel(id);
case "respond": return await controller.respond(id, command.attemptId, command.promptId, command.value);
case "open": return await controller.open(id, command.attemptId, command.actionId);
case "retry": return await controller.retry(id);
case "disconnect": return await controller.disconnect(id);
case "consent": return await controller.consent(id, command.enabled);
default: throw new Error();
}
} catch (error) { throw new Error(publicError(error)); }
}
module.exports = { attachSubscriptionChild, subscriptionControl, validScope };
+64
View File
@@ -0,0 +1,64 @@
"use strict";
const fs = require("node:fs/promises");
const path = require("node:path");
const { randomUUID } = require("node:crypto");
const PROVIDERS = ["github-copilot", "xai"];
const failure = (code) => Object.assign(new Error(code), { code });
const nonempty = (value) => typeof value === "string" && value.length > 0 && !/[\x00-\x20\x7f]/.test(value);
function validateCredential(providerId, credential) {
if (!credential || credential.type !== "oauth" || !nonempty(credential.access) || !nonempty(credential.refresh) || !Number.isFinite(credential.expires) || credential.expires < 0) throw failure("storage");
if (providerId === "github-copilot") {
if (credential.enterpriseUrl !== undefined && !safeDomain(credential.enterpriseUrl)) throw failure("storage");
if (credential.availableModelIds !== undefined && (!Array.isArray(credential.availableModelIds) || !credential.availableModelIds.every(nonempty))) throw failure("storage");
}
return credential;
}
function safeDomain(value) {
if (typeof value !== "string" || !value || /[\s/:@?#\\]/.test(value)) return false;
try { return new URL(`https://${value}`).hostname === value; } catch { return false; }
}
function safeHttps(value) {
try {
const url = new URL(value);
return url.protocol === "https:" && !url.username && !url.password;
} catch { return false; }
}
function validateState(providerId, state) {
if (!PROVIDERS.includes(providerId) || !state || state.version !== 1 || typeof state.allowBackgroundUse !== "boolean") throw failure("storage");
const hasId = Object.hasOwn(state, "connectionId");
if (hasId !== Object.hasOwn(state, "credential") || (hasId && !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(state.connectionId))) throw failure("storage");
if (hasId) validateCredential(providerId, state.credential);
return state;
}
/** Main-process-only, one independent owner-only file per provider. No recovery writes on corruption. */
function fileStore(file, providerId, io = fs) {
return {
async read() {
try {
const stat = await io.lstat(file);
if (!stat.isFile() || (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || (typeof process.getuid === "function" && stat.uid !== process.getuid())))) throw failure("storage");
return validateState(providerId, JSON.parse(await io.readFile(file, "utf8")));
}
catch (error) { if (error.code === "ENOENT") return { version: 1, allowBackgroundUse: true }; throw failure("storage"); }
},
async write(state) {
validateState(providerId, state);
const temporary = `${file}.${randomUUID()}.tmp`;
let handle;
try {
await io.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
handle = await io.open(temporary, "wx", 0o600);
await handle.writeFile(`${JSON.stringify(state)}\n`);
await handle.sync(); await handle.close(); handle = null;
await io.rename(temporary, file);
if (process.platform !== "win32") {
const directory = await io.open(path.dirname(file), "r");
try { await directory.sync(); } finally { await directory.close(); }
}
} catch { throw failure("storage"); }
finally { await handle?.close().catch(() => {}); await io.unlink(temporary).catch(() => {}); }
},
};
}
module.exports = { PROVIDERS, failure, validateCredential, validateState, safeDomain, safeHttps, fileStore };
+251
View File
@@ -0,0 +1,251 @@
"use strict";
const { randomUUID } = require("node:crypto");
const path = require("node:path");
const { createRequire, registerHooks } = require("node:module");
const { pathToFileURL } = require("node:url");
const { PROVIDERS, fileStore, validateCredential, safeHttps, failure } = require("./subscription-store.cjs");
const LABELS = { "github-copilot": "GitHub Copilot", xai: "Grok" };
const APIS = new Set(["openai-responses", "openai-completions", "anthropic-messages"]);
const clone = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
const MESSAGES = {
storage: "The saved connection could not be read or written safely. Reopen the app or reconnect after resolving the credential-file problem.",
login: "Sign-in did not complete. Try connecting again.",
refresh: "The connection could not be refreshed. Retry or reconnect in Providers.",
cancel: "Sign-in cancelled.",
unavailable: "This subscription connection is unavailable. Reconnect or explicitly choose another connection.",
background: "Background use is off for this connection. Automatic follow-up remains deferred in its queue.",
stale: "The connection or model catalog changed. Select an available model and retry.",
interaction: "This sign-in action is no longer available.",
};
function publicError(error) { return MESSAGES[error?.code] ?? MESSAGES.unavailable; }
/** Resolve the SAME SDK installation as the standalone server, through public exports. */
async function loadSubscriptionSDK(baseDirectory) {
const resolve = createRequire(path.join(baseDirectory, "package.json"));
const load = (id) => {
// Pi exports are import-only. Resolve the public export with ESM
// conditions, then remove this synchronous, exact-specifier hook
// BEFORE evaluation; no private package path or global hook remains.
const hook = registerHooks({ resolve(specifier, context, next) {
return next(specifier, specifier === id ? { ...context, conditions: [...context.conditions, "import"] } : context);
} });
let file;
try { file = resolve.resolve(id); } finally { hook.deregister(); }
return import(pathToFileURL(file).href);
};
const [sdk, copilot, xai] = await Promise.all([load("@earendil-works/pi-ai"), load("@earendil-works/pi-ai/providers/github-copilot"), load("@earendil-works/pi-ai/providers/xai")]);
return { ...sdk, ...copilot, ...xai };
}
function validEndpoint(providerId, auth, credential) {
if (!auth || (!(typeof auth.apiKey === "string" && auth.apiKey.length > 0) && !Object.entries(auth.headers ?? {}).some(([key, value]) => ["authorization", "x-api-key", "api-key"].includes(key.toLowerCase()) && typeof value === "string" && value.trim().length > 0))) return false;
const value = auth.baseUrl ?? (providerId === "xai" ? "https://api.x.ai/v1" : "https://api.individual.githubcopilot.com");
if (!safeHttps(value)) return false;
const url = new URL(value);
if (providerId === "xai") return url.origin === "https://api.x.ai" && url.pathname.replace(/\/$/, "") === "/v1" && !url.search && !url.hash;
const domain = credential?.enterpriseUrl;
return !url.search && !url.hash && url.pathname === "/" && (!url.port || url.port === "443") && (url.hostname.endsWith(".githubcopilot.com") || (domain && (url.hostname === domain || url.hostname.endsWith(`.${domain}`))));
}
/** Main owns durable credentials and refresh. Everything returned by status/snapshot is explicitly projected. */
function createSubscriptions(options) {
let sdk;
let closed = false, paused = false;
const entries = new Map(PROVIDERS.map((providerId) => [providerId, {
providerId, state: { version: 1, allowBackgroundUse: true }, disk: options.stores?.[providerId] ?? fileStore(path.join(options.directory, `${providerId}-credentials.json`), providerId),
epoch: 0, authorityRevision: 0, revision: 0, tail: Promise.resolve(), fatal: false, blocked: false, ready: false, error: undefined, authFailed: false, attempt: null, catalog: [], endpoint: null,
}]));
const entryFor = (id) => { const entry = entries.get(id); if (!entry) throw failure("unavailable"); return entry; };
function serialized(entry, fn) { const result = entry.tail.then(fn); entry.tail = result.catch(() => {}); return result; }
function statusOf(entry) {
const available = entry.ready && !closed && !paused && !entry.fatal;
const attempt = entry.attempt;
return {
providerId: entry.providerId, available, connected: !!(available && entry.state.credential && !entry.blocked && !entry.authFailed), pending: !!attempt, committing: attempt?.committing === true,
allowBackgroundUse: entry.state.allowBackgroundUse, connectionId: entry.state.connectionId ?? null,
epoch: entry.epoch, authorityRevision: entry.authorityRevision, revision: entry.revision,
...(entry.error ? { error: entry.error } : {}),
...(attempt ? { interaction: clone(attempt.public) } : {}),
};
}
function snapshotNow() {
return { statuses: [...entries.values()].map(statusOf), models: Object.fromEntries([...entries.values()].map((entry) => [entry.providerId, statusOf(entry).connected ? clone(entry.catalog) : []])) };
}
function changed(entry) { entry.revision++; try { options.onChange?.(snapshotNow()); } catch { /* observer cannot break credential writes */ } }
function catalog(entry) {
let models = entry.provider?.getModels().filter((model) => APIS.has(model.api)) ?? [];
if (!entry.state.credential) models = [];
else if (entry.providerId === "github-copilot" && !Array.isArray(entry.state.credential.availableModelIds)) models = [];
else if (entry.provider?.filterModels) models = [...entry.provider.filterModels(models, entry.state.credential)];
if (JSON.stringify(models) !== JSON.stringify(entry.catalog)) { entry.catalog = models; entry.authorityRevision++; }
}
async function updateEndpoint(entry) {
let endpoint = null;
if (entry.state.credential) {
const auth = await entry.provider.auth.oauth.toAuth(entry.state.credential);
if (!validEndpoint(entry.providerId, auth, entry.state.credential)) throw failure("storage");
endpoint = auth.baseUrl ?? (entry.providerId === "xai" ? "https://api.x.ai/v1" : "https://api.individual.githubcopilot.com");
}
if (entry.endpoint !== endpoint) { entry.endpoint = endpoint; entry.authorityRevision++; }
}
function storageFailed(entry) { entry.fatal = true; entry.blocked = true; entry.epoch++; entry.error = MESSAGES.storage; changed(entry); }
async function save(entry, state) {
try { await entry.disk.write(state); } catch { storageFailed(entry); throw failure("storage"); }
entry.state = clone(state);
try { await updateEndpoint(entry); catalog(entry); } catch { storageFailed(entry); throw failure("storage"); }
changed(entry);
}
const ready = (async () => {
try { sdk = options.sdk ?? await options.loadSDK(); }
catch { for (const entry of entries.values()) { entry.error = MESSAGES.unavailable; entry.fatal = true; changed(entry); } return; }
await Promise.all([...entries.values()].map(async (entry) => {
try {
entry.state = clone(await entry.disk.read());
const native = entry.providerId === "github-copilot" ? sdk.githubCopilotProvider() : sdk.xaiProvider();
// No API-key auth method exists in this collection: ambient keys cannot become a fallback.
entry.provider = { ...native, auth: { oauth: native.auth.oauth } };
const store = {
read: async (id) => id === entry.providerId ? clone(entry.state.credential) : undefined,
list: async () => entry.state.credential ? [{ providerId: entry.providerId, type: "oauth" }] : [],
modify: (id, fn, operation) => serialized(entry, async () => {
if (id !== entry.providerId || entry.fatal || closed || paused || operation?.signal?.aborted) throw failure("unavailable");
const next = await fn(clone(entry.state.credential));
// A returned rotation must be saved even if the app paused while
// refresh was in flight. Admission is rechecked after resolution;
// queued logout deletes only after this serialized durable write.
if (next !== undefined && JSON.stringify(next) !== JSON.stringify(entry.state.credential)) {
try { validateCredential(id, next); } catch { storageFailed(entry); throw failure("storage"); }
await save(entry, { ...entry.state, credential: next });
}
return clone(entry.state.credential);
}),
delete: (id) => serialized(entry, async () => {
if (id !== entry.providerId || entry.fatal) throw failure("storage");
await save(entry, { version: 1, allowBackgroundUse: entry.state.allowBackgroundUse });
}),
};
entry.models = sdk.createModels({ credentials: store }); entry.models.setProvider(entry.provider);
await updateEndpoint(entry); entry.ready = true; catalog(entry); changed(entry);
} catch { entry.fatal = true; entry.error = MESSAGES.storage; changed(entry); }
}));
})();
function current(entry, scope) {
return statusOf(entry).connected && scope?.providerId === entry.providerId && scope.connectionId === entry.state.connectionId && scope.epoch === entry.epoch && scope.authorityRevision === entry.authorityRevision && entry.catalog.some((m) => m.id === scope.selectedModelId);
}
async function consent(providerId, enabled) {
await ready; const entry = entryFor(providerId);
if (typeof enabled !== "boolean" || entry.fatal || closed || paused) throw failure("unavailable");
await serialized(entry, async () => { if (entry.state.allowBackgroundUse !== enabled) await save(entry, { ...entry.state, allowBackgroundUse: enabled }); });
}
async function login(providerId, enabled) {
await ready; const entry = entryFor(providerId);
if ((enabled !== undefined && typeof enabled !== "boolean") || entry.fatal || closed || paused || !entry.ready) throw failure("unavailable");
if ([...entries.values()].some((e) => e.attempt)) throw failure("interaction");
const abort = new AbortController();
const attempt = { id: randomUUID(), abort, committing: false, actions: new Map(), prompt: null, public: {} };
attempt.public = { attemptId: attempt.id }; entry.attempt = attempt; entry.error = undefined; changed(entry);
const active = () => entry.attempt === attempt && !abort.signal.aborted && !closed && !paused;
const addBrowser = (value) => {
if (!safeHttps(value)) throw failure("interaction");
const id = randomUUID(); attempt.actions.set(id, value);
const url = new URL(value); attempt.public.browser = { id, url: `${url.origin}${url.pathname}` };
};
try {
const staging = sdk.createModels(); staging.setProvider(entry.provider);
const credential = await staging.login(providerId, "oauth", {
signal: abort.signal,
notify: (event) => {
if (!active()) return;
if (event.type === "auth_url") addBrowser(event.url);
if (event.type === "device_code") { addBrowser(event.verificationUri); attempt.public.userCode = event.userCode; }
// Native provider diagnostics are never forwarded. Public prompt wording comes from SDK-owned prompts.
attempt.public.progress = event.type === "device_code" || event.type === "auth_url" ? "Complete sign-in in your browser." : "Waiting for the provider…";
changed(entry);
},
prompt: (prompt) => new Promise((resolve, reject) => {
if (!active() || prompt.signal?.aborted) { reject(failure("cancel")); return; }
const id = randomUUID();
const stop = () => { cleanup(); reject(failure("cancel")); };
const cleanup = () => { abort.signal.removeEventListener("abort", stop); prompt.signal?.removeEventListener("abort", stop); if (attempt.prompt?.id === id) { attempt.prompt = null; delete attempt.public.prompt; } };
attempt.prompt = { id, resolve: (value) => { cleanup(); resolve(value); }, reject: stop };
attempt.public.prompt = { id, type: prompt.type, message: prompt.message, ...(prompt.placeholder ? { placeholder: prompt.placeholder } : {}), ...(prompt.type === "select" ? { options: clone(prompt.options) } : {}) };
abort.signal.addEventListener("abort", stop, { once: true }); prompt.signal?.addEventListener("abort", stop, { once: true }); changed(entry);
}),
}, { agentName: "LearnHarness" });
if (!active()) throw failure("cancel"); validateCredential(providerId, credential);
const auth = await entry.provider.auth.oauth.toAuth(credential);
if (!validEndpoint(providerId, auth, credential) || !active()) throw failure("login");
await serialized(entry, async () => {
if (!active() || entry.fatal) throw failure("cancel");
attempt.committing = true; entry.blocked = true; entry.epoch++; changed(entry);
await save(entry, { version: 1, allowBackgroundUse: enabled ?? entry.state.allowBackgroundUse, connectionId: randomUUID(), credential });
entry.blocked = false; entry.authFailed = false; entry.error = undefined;
});
} catch (error) {
if (active() && !entry.fatal) entry.error = error?.code === "cancel" ? undefined : MESSAGES.login;
if (!abort.signal.aborted) throw failure(entry.fatal ? "storage" : "login");
} finally { attempt.prompt?.reject(); if (entry.attempt === attempt) { entry.attempt = null; changed(entry); } }
}
function cancel(providerId) { const entry = entryFor(providerId); if (entry.attempt && !entry.attempt.committing) { entry.attempt.abort.abort(); entry.attempt = null; if (entry.authFailed && !entry.error) entry.error = MESSAGES.refresh; changed(entry); } }
async function resolveAuth(entry, modelId) {
if (entry.fatal || closed || paused || !entry.state.credential) throw failure("unavailable");
const connectionId = entry.state.connectionId, epoch = entry.epoch;
const unchanged = () => connectionId === entry.state.connectionId && epoch === entry.epoch && !entry.blocked && !closed && !paused;
try {
const model = entry.provider.getModels().find((m) => m.id === modelId);
if (!model) throw failure("stale");
const resolved = await entry.models.getAuth(model);
if (!unchanged()) throw failure("stale");
if (!validEndpoint(entry.providerId, resolved?.auth, entry.state.credential)) throw failure("unavailable");
return {
...(typeof resolved.auth.apiKey === "string" ? { apiKey: resolved.auth.apiKey } : {}),
...(resolved.auth.headers ? { headers: Object.fromEntries(Object.entries(resolved.auth.headers).filter(([, value]) => value === null || typeof value === "string")) } : {}),
...(typeof resolved.auth.baseUrl === "string" ? { baseUrl: resolved.auth.baseUrl } : {}),
};
} catch (error) {
if (!entry.fatal && unchanged()) { entry.authFailed = true; entry.error = MESSAGES.refresh; entry.epoch++; changed(entry); }
throw failure(entry.fatal ? "storage" : error?.code === "stale" ? "stale" : "refresh");
}
}
return {
ready,
async snapshot() { await ready; return snapshotNow(); },
async status() { await ready; return snapshotNow().statuses; },
login, consent, cancel,
async respond(providerId, attemptId, promptId, value) {
const entry = entryFor(providerId), attempt = entry.attempt;
if (!attempt || attempt.id !== attemptId || attempt.prompt?.id !== promptId || typeof value !== "string" || value.length > 4096) throw failure("interaction");
const prompt = attempt.public.prompt;
if (prompt.type === "select" && !prompt.options.some((option) => option.id === value)) throw failure("interaction");
attempt.prompt.resolve(value); changed(entry);
},
async open(providerId, attemptId, actionId) {
const attempt = entryFor(providerId).attempt;
const target = attempt?.id === attemptId ? attempt.actions.get(actionId) : undefined;
if (!target || !safeHttps(target)) throw failure("interaction");
try { await options.openExternal(target); } catch { throw failure("interaction"); }
},
async lease(scope, background) {
await ready; const entry = entryFor(scope?.providerId);
if (!current(entry, scope)) throw failure("stale");
if (background && !entry.state.allowBackgroundUse) throw failure("background");
const auth = await resolveAuth(entry, scope.selectedModelId);
if (!current(entry, scope)) throw failure("stale");
if (background && !entry.state.allowBackgroundUse) throw failure("background");
return { scope: clone(scope), auth: clone(auth) };
},
async retry(providerId) {
await ready; const entry = entryFor(providerId);
const model = entry.catalog[0]; if (!model || entry.fatal || entry.blocked) throw failure("unavailable");
await resolveAuth(entry, model.id); entry.authFailed = false; entry.error = undefined; changed(entry);
},
async disconnect(providerId) {
await ready; const entry = entryFor(providerId); cancel(providerId);
if (entry.fatal) throw failure("storage");
entry.blocked = true; entry.epoch++; changed(entry);
await entry.models.logout(providerId); entry.blocked = false; entry.authFailed = false; entry.error = undefined; changed(entry);
},
pause() { paused = true; for (const entry of entries.values()) { cancel(entry.providerId); entry.epoch++; changed(entry); } },
resume() { if (closed) return; paused = false; for (const entry of entries.values()) changed(entry); },
close() { closed = true; for (const entry of entries.values()) { cancel(entry.providerId); entry.epoch++; changed(entry); } },
};
}
module.exports = { createSubscriptions, loadSubscriptionSDK, publicError, validEndpoint, LABELS };