UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

276 lines (275 loc) 14.2 kB
import { n as ok, t as err } from "./result-BQGgYouL.js"; import { n as ENV_SECRET_REF_ID_RE } from "./types.secrets-kC0nOetj.js"; import { i as registerSecretValueForRedaction } from "./secret-redaction-registry-BOLC6DkF.js"; import { t as normalizeExactAllowedHost } from "./exact-hostname-B5MIU7_E.js"; import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { r as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-BRgmrGHt.js"; import { A as ensureSecretStoreSchema, i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction, vt as normalizeSqliteNumber } from "./openclaw-state-db-BRTnL-D8.js"; import { l as sealSecretSentinel } from "./sentinel-ZgmfESkJ.js"; import { n as SecretStoreValidationError, t as SECRET_STORE_VALUE_MAX_BYTES } from "./secret-store-validation-error-Bzzf_1MN.js"; import { i as classifyHiddenGitHubStoreName, n as GITHUB_SETUP_HANDOFF_MAX_AGE_MS, t as GITHUB_DEVICE_STORE_MAX_AGE_MS } from "./secret-store-hidden-github-ax_HMEXG.js"; //#region src/secrets/store/secret-store.ts const SECRET_STORE_RETENTION_MS = 2592e6; function normalizeScope(_scope) { return { scopeKind: "team", scopeId: "" }; } function assertSecretStoreEnvName(name) { if (!ENV_SECRET_REF_ID_RE.test(name)) throw new SecretStoreValidationError("SECRET_STORE_INVALID_NAME", `Secret store name must match ${String(ENV_SECRET_REF_ID_RE)}.`); } function assertSecretStoreMutationName(name) { if (!ENV_SECRET_REF_ID_RE.test(name) && classifyHiddenGitHubStoreName(name) !== "setup") throw new SecretStoreValidationError("SECRET_STORE_INVALID_NAME", `Secret store name must match ${String(ENV_SECRET_REF_ID_RE)} or github-setup-<32 lowercase hex characters>.`); } function assertSecretStoreValue(value, kind) { if (Buffer.byteLength(value, "utf8") > 65536) throw new SecretStoreValidationError("SECRET_STORE_VALUE_TOO_LARGE", `Secret store value exceeds ${SECRET_STORE_VALUE_MAX_BYTES} UTF-8 bytes.`); if (kind === "secret" && value.length === 0) throw new SecretStoreValidationError("SECRET_STORE_VALUE_EMPTY", "Secret store value is empty. Secret entries require a value; check the command that produced it."); } function normalizeSecretAllowedHost(raw) { try { return normalizeExactAllowedHost(raw); } catch (error) { throw new SecretStoreValidationError("SECRET_STORE_INVALID_ALLOWED_HOST", error instanceof Error ? error.message : `Allowed host "${raw}" is not a valid hostname.`); } } function normalizeSecretAllowedHosts(hosts) { if (hosts.length > 128) throw new SecretStoreValidationError("SECRET_STORE_INVALID_ALLOWED_HOST", `A secret can allow at most 128 hosts.`); return [...new Set(hosts.map(normalizeSecretAllowedHost))].toSorted(); } function parseSecretAllowedHosts(raw) { if (!raw) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) && parsed.every((host) => typeof host === "string") ? normalizeSecretAllowedHosts(parsed) : []; } catch { return []; } } function isMissingSecretStoreTableError(error) { return error instanceof Error && error.code === "ERR_SQLITE_ERROR" && error.message === "no such table: secret_store_entries"; } function toMetadata(row) { if (row.kind === "secret") registerSecretValueForRedaction(row.value); return { name: row.name, kind: row.kind, scopeKind: row.scope_kind, scopeId: row.scope_id, updatedAtMs: normalizeSqliteNumber(row.updated_at_ms) ?? 0, createdAtMs: normalizeSqliteNumber(row.created_at_ms) ?? 0, updatedBy: row.updated_by, ...row.kind === "secret" ? { allowedHosts: parseSecretAllowedHosts(row.allowed_hosts) } : {}, ...row.kind === "env" ? { valuePreview: row.value } : {} }; } function listSecretStoreEntries(params) { const { scopeKind, scopeId } = normalizeScope(params.scope); try { return withExistingOpenClawStateDatabaseReadOnly(({ db: sqlite }) => { let query = getNodeSqliteKysely(sqlite).selectFrom("secret_store_entries").selectAll().where("scope_kind", "=", scopeKind).where("scope_id", "=", scopeId).orderBy("name", "asc"); if (!params.includeDeleted) query = query.where("deleted_at_ms", "is", null); return executeSqliteQuerySync(sqlite, query).rows.filter((row) => classifyHiddenGitHubStoreName(row.name) === void 0).map(toMetadata); }, params.database ?? {}) ?? []; } catch (error) { if (isMissingSecretStoreTableError(error)) return []; throw error; } } /** Atomically returns and hard-deletes one exact fresh, non-egress GitHub setup handoff. */ function consumeGitHubSetupHandoff(params) { if (classifyHiddenGitHubStoreName(params.name) !== "setup") return; const now = params.nowMs ?? Date.now(); try { let value; runOpenClawStateWriteTransaction(({ db: sqlite }) => { const db = getNodeSqliteKysely(sqlite); const row = executeSqliteQueryTakeFirstSync(sqlite, db.selectFrom("secret_store_entries").select("value").where("scope_kind", "=", "team").where("scope_id", "=", "").where("name", "=", params.name).where("kind", "=", "secret").where("allowed_hosts", "is", null).where("created_at_ms", ">=", now - GITHUB_SETUP_HANDOFF_MAX_AGE_MS).where("created_at_ms", "<=", now).where("deleted_at_ms", "is", null)); if (!row) return; executeSqliteQuerySync(sqlite, db.deleteFrom("secret_store_entries").where("scope_kind", "=", "team").where("scope_id", "=", "").where("name", "=", params.name)); value = row.value; }, params.database, { operationLabel: "secrets.store.consume-github-setup-handoff" }); if (value !== void 0) registerSecretValueForRedaction(value); return value; } catch (error) { if (isMissingSecretStoreTableError(error)) return; throw error; } } /** Captures one coherent team-store snapshot for an agent run's exec environment. */ function readSecretStoreExecEnvironment(params) { try { return withExistingOpenClawStateDatabaseReadOnly(({ db: sqlite }) => { const db = getNodeSqliteKysely(sqlite); const rows = executeSqliteQuerySync(sqlite, db.selectFrom("secret_store_entries").selectAll().where("scope_kind", "=", "team").where("scope_id", "=", "").where("deleted_at_ms", "is", null).orderBy("name", "asc")).rows; const env = {}; const secretSentinels = {}; const secretEgressBindings = []; const excludedNames = new Set(params.excludeNames ?? []); for (const row of rows) { if (classifyHiddenGitHubStoreName(row.name) !== void 0 || excludedNames.has(row.name)) continue; if (row.kind === "env") { env[row.name] = row.value; continue; } registerSecretValueForRedaction(row.value); if (params.includeSecretSentinels) { const sentinel = sealSecretSentinel(row.value, { label: `exec-store:${row.name}` }); secretSentinels[row.name] = sentinel; secretEgressBindings.push({ name: row.name, sentinel, allowedHosts: parseSecretAllowedHosts(row.allowed_hosts) }); } } return { ...Object.keys(env).length > 0 ? { env } : {}, ...Object.keys(secretSentinels).length > 0 ? { secretSentinels } : {}, ...secretEgressBindings.length > 0 ? { secretEgressBindings } : {} }; }, params.database ?? {}) ?? {}; } catch (error) { if (isMissingSecretStoreTableError(error)) return {}; throw error; } } function readSecretStoreValue(params) { try { assertSecretStoreEnvName(params.name); const { scopeKind, scopeId } = normalizeScope(params.scope); const row = withExistingOpenClawStateDatabaseReadOnly(({ db: sqlite }) => { const db = getNodeSqliteKysely(sqlite); return executeSqliteQueryTakeFirstSync(sqlite, db.selectFrom("secret_store_entries").select(["value", "kind"]).where("scope_kind", "=", scopeKind).where("scope_id", "=", scopeId).where("name", "=", params.name).where("deleted_at_ms", "is", null)); }, params.database ?? {}); if (!row) return err({ code: "SECRET_STORE_NOT_FOUND", message: `Secret store entry "${params.name}" was not found.` }); if (row.kind === "secret") registerSecretValueForRedaction(row.value); return ok(row.value); } catch (error) { if (isMissingSecretStoreTableError(error)) return err({ code: "SECRET_STORE_NOT_FOUND", message: `Secret store entry "${params.name}" was not found.` }); if (error instanceof SecretStoreValidationError) return err({ code: "SECRET_STORE_INVALID_NAME", message: error.message }); return err({ code: "SECRET_STORE_UNAVAILABLE", message: "Secret store database is unavailable.", cause: error }); } } function writeSecretStoreEntry(params) { assertSecretStoreMutationName(params.name); assertSecretStoreValue(params.value, params.kind); if (params.kind === "env" && params.allowedHosts !== void 0) throw new SecretStoreValidationError("SECRET_STORE_INVALID_ALLOWED_HOST", "Allowed hosts apply only to secret entries."); const allowedHosts = params.kind === "secret" && params.allowedHosts !== void 0 ? normalizeSecretAllowedHosts(params.allowedHosts) : void 0; const allowedHostsJson = allowedHosts?.length ? JSON.stringify(allowedHosts) : null; const { scopeKind, scopeId } = normalizeScope(params.scope); const now = Date.now(); runOpenClawStateWriteTransaction(({ db: sqlite }) => { ensureSecretStoreSchema(sqlite); const db = getNodeSqliteKysely(sqlite); executeSqliteQuerySync(sqlite, db.insertInto("secret_store_entries").values({ scope_kind: scopeKind, scope_id: scopeId, name: params.name, value: params.value, kind: params.kind, created_at_ms: now, updated_at_ms: now, updated_by: params.updatedBy, deleted_at_ms: null, allowed_hosts: allowedHostsJson }).onConflict((conflict) => conflict.columns([ "scope_kind", "scope_id", "name" ]).doUpdateSet({ value: params.value, kind: params.kind, updated_at_ms: now, updated_by: params.updatedBy, deleted_at_ms: null, ...params.kind === "env" ? { allowed_hosts: null } : allowedHosts !== void 0 ? { allowed_hosts: allowedHostsJson } : {} }))); }, params.database, { operationLabel: "secrets.store.write" }); } function updateSecretStoreAllowedHosts(params) { assertSecretStoreEnvName(params.name); const allowedHosts = normalizeSecretAllowedHosts(params.allowedHosts); const { scopeKind, scopeId } = normalizeScope(params.scope); const now = Date.now(); runOpenClawStateWriteTransaction(({ db: sqlite }) => { ensureSecretStoreSchema(sqlite); const db = getNodeSqliteKysely(sqlite); const updated = executeSqliteQuerySync(sqlite, db.updateTable("secret_store_entries").set({ allowed_hosts: allowedHosts.length ? JSON.stringify(allowedHosts) : null, updated_at_ms: now, updated_by: params.updatedBy }).where("scope_kind", "=", scopeKind).where("scope_id", "=", scopeId).where("name", "=", params.name).where("kind", "=", "secret").where("deleted_at_ms", "is", null)); if (Number(updated.numAffectedRows ?? 0n) !== 1) throw new SecretStoreValidationError("SECRET_STORE_INVALID_ALLOWED_HOST", `Secret store entry "${params.name}" is missing or is not a secret entry.`); }, params.database, { operationLabel: "secrets.store.allowed-hosts" }); } function deleteSecretStoreEntry(params) { assertSecretStoreMutationName(params.name); const { scopeKind, scopeId } = normalizeScope(params.scope); const state = openOpenClawStateDatabase(params.database); const now = Date.now(); try { runOpenClawStateWriteTransaction(({ db: sqlite }) => { const db = getNodeSqliteKysely(sqlite); const query = classifyHiddenGitHubStoreName(params.name) === "setup" ? db.deleteFrom("secret_store_entries").where("scope_kind", "=", scopeKind).where("scope_id", "=", scopeId).where("name", "=", params.name) : db.updateTable("secret_store_entries").set({ deleted_at_ms: now, updated_at_ms: now }).where("scope_kind", "=", scopeKind).where("scope_id", "=", scopeId).where("name", "=", params.name).where("deleted_at_ms", "is", null); executeSqliteQuerySync(sqlite, query); }, { ...params.database, database: state }, { operationLabel: "secrets.store.delete" }); } catch (error) { if (!isMissingSecretStoreTableError(error)) throw error; } } function purgeExpiredSecretStoreEntries(params = {}) { const state = openOpenClawStateDatabase(params.database); const threshold = Date.now() - SECRET_STORE_RETENTION_MS; const handoffThreshold = Date.now() - GITHUB_SETUP_HANDOFF_MAX_AGE_MS; const deviceThreshold = Date.now() - GITHUB_DEVICE_STORE_MAX_AGE_MS; try { return runOpenClawStateWriteTransaction(({ db: sqlite }) => { const db = getNodeSqliteKysely(sqlite); const deleted = executeSqliteQuerySync(sqlite, db.deleteFrom("secret_store_entries").where("deleted_at_ms", "is not", null).where("deleted_at_ms", "<", threshold)); const hiddenRows = executeSqliteQuerySync(sqlite, db.selectFrom("secret_store_entries").select([ "scope_kind", "scope_id", "name", "created_at_ms" ]).where("deleted_at_ms", "is", null).where("created_at_ms", "<=", Math.max(handoffThreshold, deviceThreshold))).rows.filter((row) => { const kind = classifyHiddenGitHubStoreName(row.name); const createdAtMs = normalizeSqliteNumber(row.created_at_ms); return createdAtMs !== void 0 && (kind === "setup" && createdAtMs < handoffThreshold || kind === "device" && createdAtMs <= deviceThreshold); }); let expiredHidden = 0; for (const row of hiddenRows) { const result = executeSqliteQuerySync(sqlite, db.deleteFrom("secret_store_entries").where("scope_kind", "=", row.scope_kind).where("scope_id", "=", row.scope_id).where("name", "=", row.name)); expiredHidden += Number(result.numAffectedRows ?? 0n); } return Number(deleted.numAffectedRows ?? 0n) + expiredHidden; }, { ...params.database, database: state }, { operationLabel: "secrets.store.purge" }); } catch (error) { if (isMissingSecretStoreTableError(error)) return 0; throw error; } } //#endregion export { normalizeSecretAllowedHosts as a, readSecretStoreValue as c, listSecretStoreEntries as i, updateSecretStoreAllowedHosts as l, consumeGitHubSetupHandoff as n, purgeExpiredSecretStoreEntries as o, deleteSecretStoreEntry as r, readSecretStoreExecEnvironment as s, assertSecretStoreValue as t, writeSecretStoreEntry as u };