UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

282 lines (281 loc) • 12.2 kB
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty, p as normalizeStringifiedOptionalString, s as normalizeNullableString } from "./string-coerce-1VuiLaaR.mjs"; import { y as parseDateStringTimestampMs } from "./number-coercion-CuVhdCBs.mjs"; import "./session-key-C09ow4h0.mjs"; import "./account-id-DbRsdayc.mjs"; import { l as runOpenClawStateWriteTransaction } from "./openclaw-state-db-D1jWDzK3.mjs"; import { a as writeChannelPairingStateToDatabase, l as getPairingAdapter, n as readChannelPairingStateFromDatabase, r as resolvePairingRequestAccountId, s as resolveAllowFromAccountId, t as readChannelPairingState } from "./pairing-store-sqlite-D9F6ubE-.mjs"; import crypto from "node:crypto"; //#region src/pairing/pairing-store.ts const PAIRING_CODE_LENGTH = 8; const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; const PAIRING_CODE_MAX_ATTEMPTS = 500; const CHANNEL_PAIRING_PENDING_TTL_MS = 36e5; /** Stable opaque id for approving a request without exposing its human pairing code. */ function resolveChannelPairingRequestId(channel, request) { const accountId = resolvePairingRequestAccountId(request); return crypto.createHash("sha256").update(`${channel}\0${accountId}\0${request.id}\0${request.createdAt}`).digest("base64url").slice(0, 32); } function isExpired(entry, nowMs) { const createdAt = parseDateStringTimestampMs(entry.createdAt); return createdAt === void 0 || nowMs - createdAt > 36e5; } function pruneExpiredRequests(reqs, nowMs) { const requests = reqs.filter((req) => !isExpired(req, nowMs)); return { requests, removed: requests.length !== reqs.length }; } function resolveLastSeenAt(entry) { return parseDateStringTimestampMs(entry.lastSeenAt) ?? parseDateStringTimestampMs(entry.createdAt) ?? 0; } function requestMatchesAccountId(entry, normalizedAccountId) { return !normalizedAccountId || resolvePairingRequestAccountId(entry) === normalizedAccountId; } function pruneExcessRequestsByAccount(reqs) { const grouped = /* @__PURE__ */ new Map(); for (const [index, entry] of reqs.entries()) { const accountId = resolvePairingRequestAccountId(entry); const entries = grouped.get(accountId) ?? []; entries.push([index, entry]); grouped.set(accountId, entries); } const droppedIndexes = /* @__PURE__ */ new Set(); for (const entries of grouped.values()) { const sorted = entries.toSorted(([, left], [, right]) => resolveLastSeenAt(left) - resolveLastSeenAt(right)); for (const [index] of sorted.slice(0, -3)) droppedIndexes.add(index); } return droppedIndexes.size === 0 ? { requests: reqs, removed: false } : { requests: reqs.filter((_, index) => !droppedIndexes.has(index)), removed: true }; } function randomCode() { let out = ""; for (let i = 0; i < PAIRING_CODE_LENGTH; i++) out += PAIRING_CODE_ALPHABET[crypto.randomInt(0, 32)]; return out; } function generateUniqueCode(existing) { for (let attempt = 0; attempt < PAIRING_CODE_MAX_ATTEMPTS; attempt += 1) { const code = randomCode(); if (!existing.has(code)) return code; } throw new Error(`failed to generate unique pairing code after ${PAIRING_CODE_MAX_ATTEMPTS} attempts; existing code count: ${existing.size}`); } function resolvePairingAdapter(channel, pairingAdapter) { return pairingAdapter ?? getPairingAdapter(channel) ?? void 0; } function normalizeAllowFromInput(channel, entry, pairingAdapter) { const trimmed = normalizeStringifiedOptionalString(entry) ?? ""; if (!trimmed || trimmed === "*") return ""; const adapter = resolvePairingAdapter(channel, pairingAdapter); const normalized = adapter?.normalizeAllowEntry ? adapter.normalizeAllowEntry(trimmed) : trimmed; const normalizedEntry = normalizeOptionalString(normalized) ?? ""; return normalizedEntry === "*" ? "" : normalizedEntry; } async function updateAllowFromStoreEntry(params) { const assertCurrent = params.assertCurrent; const env = params.env ?? process.env; const accountId = resolveAllowFromAccountId(params.accountId); const normalized = normalizeAllowFromInput(params.channel, params.entry, params.pairingAdapter); return runOpenClawStateWriteTransaction((database) => { const state = readChannelPairingStateFromDatabase(database, params.channel); const current = (state.allowFrom?.[accountId] ?? []).slice(); if (!normalized) return { changed: false, allowFrom: current }; const next = params.apply(current, normalized); if (!next) return { changed: false, allowFrom: current }; state.allowFrom ??= {}; state.allowFrom[accountId] = next; assertCurrent?.(); writeChannelPairingStateToDatabase(database, params.channel, state); return { changed: true, allowFrom: next }; }, { env }); } async function readChannelAllowFromStore(channel, env = process.env, accountId) { return readChannelAllowFromStoreSync(channel, env, accountId); } function readChannelAllowFromStoreSync(channel, env = process.env, accountId) { const resolvedAccountId = resolveAllowFromAccountId(accountId); return (readChannelPairingState(channel, env).allowFrom?.[resolvedAccountId] ?? []).slice(); } async function addChannelAllowFromStoreEntry(params) { return updateAllowFromStoreEntry({ ...params, apply: (current, normalized) => current.includes(normalized) ? null : [...current, normalized] }); } async function removeChannelAllowFromStoreEntry(params) { return updateAllowFromStoreEntry({ ...params, apply: (current, normalized) => { const next = current.filter((entry) => entry !== normalized); return next.length === current.length ? null : next; } }); } async function listChannelPairingRequests(channel, env = process.env, accountId, assertCurrent) { assertCurrent?.(); return runOpenClawStateWriteTransaction((database) => { assertCurrent?.(); const state = readChannelPairingStateFromDatabase(database, channel); const expired = pruneExpiredRequests(state.requests, Date.now()); const capped = pruneExcessRequestsByAccount(expired.requests); if (expired.removed || capped.removed) { state.requests = capped.requests; writeChannelPairingStateToDatabase(database, channel, state); } const normalizedAccountId = normalizeLowercaseStringOrEmpty(accountId); const requests = capped.requests.filter((entry) => requestMatchesAccountId(entry, normalizedAccountId)).toSorted((left, right) => { const createdOrder = left.createdAt.localeCompare(right.createdAt); if (createdOrder !== 0) return createdOrder; return resolvePairingRequestAccountId(left).localeCompare(resolvePairingRequestAccountId(right)) || left.id.localeCompare(right.id); }); assertCurrent?.(); return requests; }, { env }); } async function upsertChannelPairingRequest(params) { const env = params.env ?? process.env; return runOpenClawStateWriteTransaction((database) => { const now = (/* @__PURE__ */ new Date()).toISOString(); const id = normalizeStringifiedOptionalString(params.id) ?? ""; const accountId = normalizeLowercaseStringOrEmpty(params.accountId) || "default"; const meta = { ...params.meta ? Object.fromEntries(Object.entries(params.meta).map(([key, value]) => [key, normalizeOptionalString(value) ?? ""]).filter(([, value]) => Boolean(value))) : void 0, accountId }; const state = readChannelPairingStateFromDatabase(database, params.channel); const expired = pruneExpiredRequests(state.requests, Date.now()); let requests = expired.requests; const existingIndex = requests.findIndex((request) => request.id === id && requestMatchesAccountId(request, accountId)); const existingCodes = new Set(requests.map((request) => (normalizeOptionalString(request.code) ?? "").toUpperCase())); if (existingIndex >= 0) { const existing = requests[existingIndex]; const code = normalizeOptionalString(existing?.code) || generateUniqueCode(existingCodes); requests[existingIndex] = { id, code, createdAt: existing?.createdAt ?? now, lastSeenAt: now, meta }; state.requests = pruneExcessRequestsByAccount(requests).requests; writeChannelPairingStateToDatabase(database, params.channel, state); return { code, created: false }; } const capped = pruneExcessRequestsByAccount(requests); requests = capped.requests; if (requests.filter((request) => requestMatchesAccountId(request, accountId)).length >= 3) { if (expired.removed || capped.removed) { state.requests = requests; writeChannelPairingStateToDatabase(database, params.channel, state); } return { code: "", created: false }; } const code = generateUniqueCode(existingCodes); state.requests = [...requests, { id, code, createdAt: now, lastSeenAt: now, meta }]; writeChannelPairingStateToDatabase(database, params.channel, state); return { code, created: true }; }, { env }); } async function resolveChannelPairingRequest(params) { const env = params.env ?? process.env; params.assertCurrent?.(); return runOpenClawStateWriteTransaction((database) => { params.assertCurrent?.(); const state = readChannelPairingStateFromDatabase(database, params.channel); const pruned = pruneExpiredRequests(state.requests, Date.now()); const accountId = normalizeLowercaseStringOrEmpty(params.accountId); const index = pruned.requests.findIndex((request) => requestMatchesAccountId(request, accountId) && params.matches(request)); if (index < 0) { if (pruned.removed) { state.requests = pruned.requests; writeChannelPairingStateToDatabase(database, params.channel, state); } params.assertCurrent?.(); return null; } const entry = pruned.requests[index]; if (!entry) return null; pruned.requests.splice(index, 1); state.requests = pruned.requests; if (params.approve) { const allowAccountId = resolveAllowFromAccountId(normalizeOptionalString(params.accountId) ?? normalizeOptionalString(entry.meta?.accountId)); const currentAllow = state.allowFrom?.[allowAccountId] ?? []; const adapter = resolvePairingAdapter(params.channel, params.pairingAdapter); const approvalEntry = adapter?.resolveApprovalStoreEntry ? adapter.resolveApprovalStoreEntry({ id: entry.id, ...entry.meta ? { meta: entry.meta } : {} }) : entry.id; const normalizedAllow = approvalEntry == null ? "" : normalizeAllowFromInput(params.channel, approvalEntry, adapter); if (normalizedAllow && !currentAllow.includes(normalizedAllow)) { state.allowFrom ??= {}; state.allowFrom[allowAccountId] = [...currentAllow, normalizedAllow]; } } writeChannelPairingStateToDatabase(database, params.channel, state); params.assertCurrent?.(); return { id: entry.id, entry }; }, { env }); } async function approveChannelPairingCode(params) { const code = (normalizeNullableString(params.code) ?? "").toUpperCase(); if (!code) return null; return resolveChannelPairingRequest({ ...params, matches: (request) => request.code.toUpperCase() === code, approve: true }); } /** Approves a pending request by opaque id without exposing its pairing code. */ async function approveChannelPairingRequest(params) { const requestId = normalizeOptionalString(params.requestId); if (!requestId) return null; return resolveChannelPairingRequest({ ...params, matches: (request) => resolveChannelPairingRequestId(params.channel, request) === requestId, approve: true }); } /** Dismisses a pending request without blocking the sender from requesting again. */ async function dismissChannelPairingRequest(params) { const requestId = normalizeOptionalString(params.requestId); if (!requestId) return null; return resolveChannelPairingRequest({ ...params, matches: (request) => resolveChannelPairingRequestId(params.channel, request) === requestId, approve: false }); } //#endregion export { dismissChannelPairingRequest as a, readChannelAllowFromStoreSync as c, upsertChannelPairingRequest as d, approveChannelPairingRequest as i, removeChannelAllowFromStoreEntry as l, addChannelAllowFromStoreEntry as n, listChannelPairingRequests as o, approveChannelPairingCode as r, readChannelAllowFromStore as s, CHANNEL_PAIRING_PENDING_TTL_MS as t, resolveChannelPairingRequestId as u };