openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
330 lines (329 loc) • 12.9 kB
JavaScript
import { i as registerSecretValueForRedaction } from "./secret-redaction-registry-BOLC6DkF.js";
import { c as resolvePluginMetadataSnapshot } from "./plugin-metadata-snapshot-w-4EjxI4.js";
import { c as resolveSharedMainAuthAgentDir } from "./path-resolve-oRkRBkQd.js";
import { i as isUserModelAuthProfileId } from "./profile-usage-stats-dkZh2v8y.js";
import { a as listUserProfileAuthLinks, i as listUserModelAccounts, n as connectUserModelAccount, s as readUserModelAccountSummary, t as clearUserProfileAuthLink, u as setUserProfileAuthLink } from "./user-model-accounts-BMkC9h8s.js";
import { a as resolveDiscoverableProviderOwnerPluginIds } from "./providers-DCe4UB6a.js";
import { a as ensureAuthProfileStoreWithoutExternalProfiles } from "./store-F1B2duCT.js";
import { r as resolveManifestProviderAuthChoices } from "./provider-auth-choices-DsRM415K.js";
import { a as runProviderPluginAuthMethodUnpersisted } from "./provider-auth-choice-DsHvi0o6.js";
import { n as sanitizeWizardStepForClient, t as WizardSession } from "./session-7QOyBCVr.js";
import { randomUUID } from "node:crypto";
//#region src/plugins/personal-account-auth.ts
/** Personal auth is opt-in; discovery never enables, installs, or imports host credentials. */
function listPersonalAccountAuthChoices(config) {
const metadataSnapshot = resolvePluginMetadataSnapshot({ config });
const choices = resolveManifestProviderAuthChoices({
config,
metadataSnapshot,
includeUntrustedWorkspacePlugins: false
}).filter((choice) => choice.personalAccount);
const allowed = new Set(resolveDiscoverableProviderOwnerPluginIds({
config,
pluginIds: choices.map((choice) => choice.pluginId),
registry: metadataSnapshot.index,
manifestRegistry: metadataSnapshot.manifestRegistry,
includeUntrustedWorkspacePlugins: false
}));
return choices.filter((choice) => allowed.has(choice.pluginId));
}
async function resolvePersonalAccountAuthMethod(config, providerId, methodId) {
const choice = listPersonalAccountAuthChoices(config).find((entry) => entry.providerId === providerId && entry.methodId === methodId);
if (!choice) return;
const { resolvePluginProvidersCore } = await import("./providers.runtime.js");
return resolvePluginProvidersCore({
config,
onlyPluginIds: [choice.pluginId],
mode: "setup",
cache: true,
activate: false,
includeUntrustedWorkspacePlugins: false
}).find((entry) => entry.pluginId === choice.pluginId && entry.id === providerId)?.auth.find((method) => method.id === methodId);
}
//#endregion
//#region src/gateway/model-account-connect.ts
const CONNECT_TTL_MS = 9e5;
const MAX_ACTIVE_CONNECTS = 8;
const MAX_RETAINED_CONNECTS = 64;
var ModelAccountConnectAuthorityError = class extends Error {
constructor() {
super("This account action requires a current authorized connection; reconnect and try again.");
}
};
var ModelAccountConnectInputError = class extends Error {};
function matchesLiteralCredential(credential, existing) {
if (credential.provider !== existing.provider) return false;
return credential.type === "api_key" && existing.type === "api_key" ? credential.key === existing.key : credential.type === "token" && existing.type === "token" && credential.token === existing.token;
}
function resolveOwnedAccountProvider(owner, authProfileId) {
const account = readUserModelAccountSummary({
profileId: owner,
authProfileId
});
if (!account) throw new ModelAccountConnectInputError("Select an account from your personal account list, or add it first.");
return account.provider;
}
function resolveLinkableAuthProfileProvider(cfg, owner, authProfileId) {
if (isUserModelAuthProfileId(authProfileId)) return resolveOwnedAccountProvider(owner, authProfileId);
return ensureAuthProfileStoreWithoutExternalProfiles(resolveSharedMainAuthAgentDir(), { readOnly: true }).profiles[authProfileId]?.provider ?? cfg.auth?.profiles?.[authProfileId]?.provider;
}
/** One Gateway lifetime owns sign-in steps and authority; provider methods only stage credentials. */
function createModelAccountConnectService(options) {
const operations = /* @__PURE__ */ new Map();
let stopped = false;
const finish = (operation, result) => {
if (operation.terminal) return operation.terminal;
operation.terminal = result;
clearTimeout(operation.timeout);
operation.session?.cancel();
return result;
};
const snapshot = (operation) => {
if (operation.terminal) return operation.terminal;
if (operation.expiresAtMs <= Date.now()) return finish(operation, { status: "expired" });
try {
operation.action.assertCurrent();
operation.answerAction?.assertCurrent();
} catch {
return finish(operation, {
status: "failed",
reason: "authority"
});
}
};
const assertRunning = (action) => {
if (stopped) throw new ModelAccountConnectAuthorityError();
action.assertCurrent();
};
const assertLive = (operation) => {
if (stopped || operations.get(operation.id) !== operation || snapshot(operation)) throw new ModelAccountConnectAuthorityError();
};
const projectResult = (action, operation) => {
const result = snapshot(operation);
if (!result) {
const step = operation.session?.getCurrentStep();
return {
status: "pending",
...step ? { step: sanitizeWizardStepForClient(step) } : {}
};
}
if (result.status !== "connected") return result;
assertRunning(action);
return {
...result,
links: listUserProfileAuthLinks(operation.owner)
};
};
const findOperation = (action, connectId) => {
assertRunning(action);
const operation = operations.get(connectId);
return operation?.owner === action.owner ? operation : void 0;
};
const supersede = (owner, provider) => {
for (const operation of operations.values()) if (operation.owner === owner && operation.provider === provider) finish(operation, { status: "cancelled" });
};
const setLink = (action, provider, authProfileId) => {
const links = setUserProfileAuthLink({
profileId: action.owner,
provider,
authProfileId,
assertCurrent: () => assertRunning(action)
});
supersede(action.owner, provider);
options.onChanged?.();
return { links };
};
return {
listLinks(action) {
assertRunning(action);
return { links: listUserProfileAuthLinks(action.owner) };
},
link(action, authProfileId) {
assertRunning(action);
const provider = resolveLinkableAuthProfileProvider(options.getConfig(), action.owner, authProfileId);
if (!provider) throw new ModelAccountConnectInputError(`unknown auth profile "${authProfileId}"; sign the account in first with "openclaw models auth login --provider <id> --profile-id ${authProfileId}", then link it`);
return setLink(action, provider, authProfileId);
},
unlink(action, provider) {
assertRunning(action);
const links = clearUserProfileAuthLink({
profileId: action.owner,
provider,
assertCurrent: () => assertRunning(action)
});
supersede(action.owner, provider);
options.onChanged?.();
return { links };
},
list(action, cursor) {
assertRunning(action);
return {
profileId: action.owner,
...listUserModelAccounts({
profileId: action.owner,
cursor
}),
links: listUserProfileAuthLinks(action.owner)
};
},
catalog(action) {
assertRunning(action);
const providers = /* @__PURE__ */ new Map();
for (const choice of listPersonalAccountAuthChoices(options.getConfig())) {
let provider = providers.get(choice.providerId);
if (!provider) {
provider = {
id: choice.providerId,
label: choice.groupLabel ?? choice.providerId,
methods: []
};
providers.set(choice.providerId, provider);
}
if (!provider.methods.some((method) => method.id === choice.methodId)) provider.methods.push({
id: choice.methodId,
label: choice.choiceLabel,
...choice.choiceHint ? { hint: choice.choiceHint } : {}
});
}
assertRunning(action);
return { providers: [...providers.values()] };
},
select(action, authProfileId) {
assertRunning(action);
return setLink(action, resolveOwnedAccountProvider(action.owner, authProfileId), authProfileId);
},
async start(action, provider, methodId) {
assertRunning(action);
for (const operation of operations.values()) snapshot(operation);
supersede(action.owner, provider);
if ([...operations.values()].filter((operation) => !operation.terminal).length >= MAX_ACTIVE_CONNECTS) throw new Error("Too many model-account sign-ins are in progress; try again shortly.");
for (const [id, operation] of operations) {
if (operations.size < MAX_RETAINED_CONNECTS) break;
if (operation.terminal) operations.delete(id);
}
const id = randomUUID();
const operation = {
id,
owner: action.owner,
provider,
action,
expiresAtMs: Date.now() + CONNECT_TTL_MS,
timeout: setTimeout(() => finish(operation, { status: "expired" }), CONNECT_TTL_MS)
};
operation.timeout.unref();
operations.set(id, operation);
let resolvedMethod;
try {
resolvedMethod = await resolvePersonalAccountAuthMethod(options.getConfig(), provider, methodId);
assertLive(operation);
if (!resolvedMethod) throw new ModelAccountConnectInputError("This sign-in method is unavailable for personal accounts. Choose a method from Connected accounts.");
} catch (error) {
snapshot(operation);
finish(operation, {
status: "failed",
reason: "unavailable"
});
throw error;
}
const method = resolvedMethod;
operation.session = new WizardSession(async (prompter, signal) => {
await Promise.resolve();
let failure = "exchange";
try {
assertLive(operation);
const result = await runProviderPluginAuthMethodUnpersisted({
config: {},
env: {},
method,
prompter,
signal,
assertCurrent: () => assertLive(operation),
isRemote: true,
secretInputMode: "plaintext",
allowSecretRefPrompt: false,
runtime: {
log: () => {},
error: () => {},
exit: () => {
throw new Error("Provider sign-in stopped.");
}
}
});
assertLive(operation);
const profile = result.profiles[0];
if (result.profiles.length !== 1 || !profile || profile.credential.provider !== provider || profile.secretStorage) {
finish(operation, {
status: "failed",
reason: "identity"
});
return;
}
failure = "unavailable";
const connected = connectUserModelAccount({
ownerProfileId: operation.owner,
credential: profile.credential,
matchesCredential: (existing) => (method.matchesPersonalAccount ?? matchesLiteralCredential)(profile.credential, existing),
assertCurrent: () => assertLive(operation)
});
finish(operation, {
status: "connected",
authProfileId: connected.authProfileId
});
options.onChanged?.();
} catch {
snapshot(operation);
finish(operation, {
status: "failed",
reason: failure
});
}
});
return {
connectId: id,
expiresAtMs: operation.expiresAtMs
};
},
status(action, connectId) {
const operation = findOperation(action, connectId);
return operation ? projectResult(action, operation) : { status: "expired" };
},
async answer(action, connectId, stepId, value) {
const operation = findOperation(action, connectId);
if (!operation) return { status: "expired" };
if (snapshot(operation)) return projectResult(action, operation);
const step = operation.session?.getCurrentStep();
if (!operation.session || step?.id !== stepId || step.type === "progress") {
const result = projectResult(action, operation);
return result.status === "pending" ? {
...result,
error: "This step has changed. Follow the current sign-in instructions."
} : result;
}
if (step.sensitive && typeof value === "string") registerSecretValueForRedaction(value);
operation.answerAction = action;
const error = await operation.session.answer(stepId, value);
assertRunning(action);
const result = projectResult(action, operation);
if (error && result.status === "pending" && result.step?.id === stepId) return {
...result,
error: "That answer is not valid. Check the sign-in instructions and try again."
};
return result;
},
cancel(action, connectId) {
const operation = findOperation(action, connectId);
if (!operation) return { status: "expired" };
snapshot(operation);
finish(operation, { status: "cancelled" });
return projectResult(action, operation);
},
supersede,
async stop() {
stopped = true;
for (const operation of operations.values()) finish(operation, { status: "cancelled" });
operations.clear();
}
};
}
//#endregion
export { ModelAccountConnectInputError as n, createModelAccountConnectService as r, ModelAccountConnectAuthorityError as t };