openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
713 lines (712 loc) • 33.8 kB
JavaScript
import { a as resolveGlobalSingleton } from "./global-singleton-0m-JziJW.mjs";
import { m as normalizeUniqueSingleOrTrimmedStringList } from "./string-normalization-CECOqf64.mjs";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-K7ftY-g4.mjs";
import { a as readSqliteDataVersion } from "./sqlite-schema-facts-CG0Tk84j.mjs";
import { a as tableHasColumn, i as tableExists } from "./openclaw-state-db-schema-helpers-BlZefrPy.mjs";
import { i as stageSqliteTransactionState } from "./sqlite-post-commit-CQ_AyDCk.mjs";
import { r as runSqliteDeferredTransactionSync } from "./sqlite-transaction-DsZ3X6nD.mjs";
import { n as sha256Hex } from "./node-crypto-BG4u3UnX.mjs";
import { t as sha256Base64Url } from "./crypto-digest-y6oZN-_F.mjs";
import { i as requestSqliteWorkerOperationAdmission } from "./sqlite-worker-operation-admission-cNGSH5v0.mjs";
import { i as getSqliteWorkerStateContext } from "./sqlite-worker-state-context-BeVaUlhr.mjs";
import { l as runOpenClawStateWriteTransaction, r as openOpenClawStateDatabase } from "./openclaw-state-db-D1jWDzK3.mjs";
import { a as ensureDevicePairSetupBootstrapSchema, o as ensureDevicePairSetupCompletionSchema, v as ensureWorkerEnvironmentNodeEnrollmentSchema } from "./openclaw-state-db-schema-additive-CzAgdzE5.mjs";
import { t as clearDeviceAuthTokenFromDatabase } from "./device-auth-store.kernel-BoMg89oU.mjs";
import { n as MAX_NODE_BOOTSTRAP_TIMEOUT_MS } from "./bootstrap-timeouts-CDaMuZ4A.mjs";
import { v as resolvePairingSetupAccess } from "./device-bootstrap-profile-DPDpvU9k.mjs";
import { r as isNodeHostStats } from "./node-host-stats-B8fF3nyX.mjs";
import { r as pruneExpiredPending } from "./pairing-files-Cjm85rvg.mjs";
import { AsyncLocalStorage } from "node:async_hooks";
import { createHash } from "node:crypto";
//#region src/infra/device-bootstrap.worker-types.ts
const DEVICE_BOOTSTRAP_TOKEN_TTL_MS = 6e5;
function resolveDeviceBootstrapTokenExpiresAtMs(record) {
const ttlMs = record.setupId && record.profile?.purpose === "cloud-worker" ? MAX_NODE_BOOTSTRAP_TIMEOUT_MS : DEVICE_BOOTSTRAP_TOKEN_TTL_MS;
return record.issuedAtMs + ttlMs;
}
//#endregion
//#region src/infra/device-pairing-mutation.worker.ts
const admission = new AsyncLocalStorage();
/** Recheck live host custody while the authoritative SQLite rows remain locked. */
function requestDevicePairingMutationAdmission(facts) {
const requests = admission.getStore();
if (!requests) throw new Error("Pairing mutation requires its worker transaction");
const captured = structuredClone(facts);
requestSqliteWorkerOperationAdmission({
stage: "transaction",
facts: [captured]
});
requests.push(captured);
}
function withDevicePairingMutationAdmission(operate) {
return admission.run([], () => {
requestSqliteWorkerOperationAdmission({
stage: "transaction",
facts: []
});
const result = operate();
requestSqliteWorkerOperationAdmission({
stage: "commit",
facts: admission.getStore()
});
return result;
});
}
//#endregion
//#region src/infra/device-pairing-cloud-worker.ts
function hasCloudWorkerSetupDeviceBinding(params) {
const kysely = getNodeSqliteKysely(params.db);
return executeSqliteQueryTakeFirstSync(params.db, kysely.selectFrom("worker_environments").select("node_device_id").where("node_setup_id", "=", params.setupId))?.node_device_id === params.deviceId;
}
/** Bind one environment-owned setup completion inside the pairing transaction. */
function bindCloudWorkerSetupCompletion(params) {
ensureWorkerEnvironmentNodeEnrollmentSchema(params.db);
const kysely = getNodeSqliteKysely(params.db);
const environment = executeSqliteQueryTakeFirstSync(params.db, kysely.selectFrom("worker_environments").select([
"environment_id",
"state",
"destroy_requested_at_ms",
"node_device_id",
"provision_operation_id",
"owner_epoch",
"updated_at_ms"
]).where("node_setup_id", "=", params.completion.setupId));
if (!environment) throw new Error("Cloud worker setup completion has no owning environment");
const isFirstProvisioningDevice = environment.state === "provisioning" && environment.node_device_id === null;
const isSameLiveDevice = environment.node_device_id === params.completion.deviceId && [
"provisioning",
"bootstrapping",
"ready",
"idle",
"attached"
].includes(environment.state);
if (environment.destroy_requested_at_ms !== null || !isFirstProvisioningDevice && !isSameLiveDevice) throw new Error("Cloud worker setup completion owner is no longer pending");
if (isFirstProvisioningDevice) requestDevicePairingMutationAdmission({
kind: "bootstrap.cloudWorkerSetup",
environmentId: environment.environment_id,
setupId: params.completion.setupId,
credentialDigest: params.credentialDigest,
provisionOperationId: environment.provision_operation_id,
ownerEpoch: environment.owner_epoch
});
executeSqliteQuerySync(params.db, kysely.updateTable("worker_environments").set({
node_device_id: params.completion.deviceId,
updated_at_ms: Math.max(environment.updated_at_ms, params.completion.completedAtMs)
}).where("environment_id", "=", environment.environment_id));
return {
environmentId: environment.environment_id,
nodeDeviceId: params.completion.deviceId,
updatedAtMs: Math.max(environment.updated_at_ms, params.completion.completedAtMs)
};
}
//#endregion
//#region src/infra/device-pairing-store-cache.ts
const cache = resolveGlobalSingleton(Symbol.for("openclaw.devicePairingStoreCache"), () => ({ value: void 0 }));
function invalidateDevicePairingStoreCache(database) {
if (cache.value?.connection === database.db && cache.value.path === database.path) cache.value = void 0;
}
/** A content revision is comparable across reader and writer connections. */
function resolveDevicePairingStoreRevision(paired) {
return createHash("sha256").update(JSON.stringify(Object.values(paired).toSorted((a, b) => a.deviceId < b.deviceId ? -1 : a.deviceId > b.deviceId ? 1 : 0))).digest("hex");
}
/** Borrowed worker facts; projections must not mutate this revision's cached rows. */
function readCachedDevicePairingStoreSnapshot(db, path, read) {
if (db.isTransaction) {
const state = read();
return {
state,
revision: resolveDevicePairingStoreRevision(state.pairedByDeviceId)
};
}
const dataVersion = readSqliteDataVersion(db);
const cached = cache.value;
if (cached?.connection === db && cached.path === path && cached.dataVersion === dataVersion) return cached;
const state = runSqliteDeferredTransactionSync(db, read);
const revision = resolveDevicePairingStoreRevision(state.pairedByDeviceId);
cache.value = {
connection: db,
path,
state,
dataVersion,
revision
};
return cache.value;
}
//#endregion
//#region src/infra/push-apns-store-transaction.ts
/** Advances a registration/tombstone version without reusing an observed owner version. */
function nextApnsRegistrationVersion(nodeId, previousVersions, nowMs = Date.now()) {
let latest = -1;
for (const version of previousVersions) {
if (!Number.isSafeInteger(version) || version < 0) throw new Error(`invalid APNs registration version for node ${nodeId}`);
latest = Math.max(latest, version);
}
if (latest === Number.MAX_SAFE_INTEGER) throw new Error(`APNs registration version exhausted for node ${nodeId}`);
return Math.max(nowMs, latest + 1);
}
/** Tombstones and deletes one APNs owner inside the caller's shared-state transaction. */
function clearApnsRegistrationFromDatabase(db, nodeId, nowMs = Date.now()) {
const normalizedNodeId = nodeId.trim();
if (!normalizedNodeId) return false;
const stateDb = getNodeSqliteKysely(db);
const currentRow = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("apns_registrations").select("updated_at_ms").where("node_id", "=", normalizedNodeId));
const tombstone = executeSqliteQueryTakeFirstSync(db, stateDb.selectFrom("apns_registration_tombstones").select("deleted_at_ms").where("node_id", "=", normalizedNodeId));
const deletedAtMs = nextApnsRegistrationVersion(normalizedNodeId, [currentRow?.updated_at_ms, tombstone?.deleted_at_ms].filter((version) => version !== void 0), nowMs);
executeSqliteQuerySync(db, stateDb.insertInto("apns_registration_tombstones").values({
node_id: normalizedNodeId,
deleted_at_ms: deletedAtMs
}).onConflict((conflict) => conflict.column("node_id").doUpdateSet({ deleted_at_ms: deletedAtMs })));
executeSqliteQuerySync(db, stateDb.deleteFrom("apns_registrations").where("node_id", "=", normalizedNodeId));
return currentRow !== void 0;
}
//#endregion
//#region src/infra/device-pairing-store.ts
const DEVICE_BOOTSTRAP_TOKEN_COLUMNS_WITHOUT_SETUP = [
"device_id",
"issued_at_ms",
"last_used_at_ms",
"pending_profile_json",
"profile_json",
"public_key",
"redeemed_profile_json",
"token",
"token_key",
"ts"
];
const boundDatabase = new AsyncLocalStorage();
/** Keep nested domain kernels on the broker's admitted physical connection. */
function withDevicePairingStoreDatabase(database, operate) {
return boundDatabase.run(database, operate);
}
/** Route an explicit pairing base dir (tests, alternate state roots) to that dir's DB. */
function resolveDevicePairingStateDbOptions(baseDir) {
const database = boundDatabase.getStore();
if (database) return {
database,
path: database.path,
env: getSqliteWorkerStateContext().environment
};
return baseDir ? { env: {
...process.env,
OPENCLAW_STATE_DIR: baseDir
} } : {};
}
function runDevicePairingStoreMutation(baseDir, mutate) {
const databaseOptions = resolveDevicePairingStateDbOptions(baseDir);
const database = openOpenClawStateDatabase(databaseOptions);
return runOpenClawStateWriteTransaction((transactionDatabase) => {
const result = mutate(transactionDatabase);
if (result.mutated) stageSqliteTransactionState(transactionDatabase.db, {
stage: () => void 0,
rollback: () => void 0,
commit: () => invalidateDevicePairingStoreCache(transactionDatabase)
});
return result.value;
}, {
...databaseOptions,
database
});
}
const APPROVAL_KINDS = new Set(Object.keys({
owner: true,
silent: true,
"trusted-cidr": true,
"trusted-proxy": true,
"ssh-verified": true,
bootstrap: true
}));
function toJsonColumn(value) {
return value === void 0 ? null : JSON.stringify(value);
}
function fromJsonColumn(value) {
return value === null ? void 0 : JSON.parse(value);
}
function toBooleanColumn(value) {
return value === void 0 ? null : value ? 1 : 0;
}
function optional(key, value) {
return value === null ? {} : { [key]: value };
}
function toPendingRow(record) {
return {
request_id: record.requestId,
device_id: record.deviceId,
public_key: record.publicKey,
display_name: record.displayName ?? null,
platform: record.platform ?? null,
device_family: record.deviceFamily ?? null,
client_id: record.clientId ?? null,
client_mode: record.clientMode ?? null,
browser_origin: record.browserOrigin ?? null,
role: record.role ?? null,
roles_json: toJsonColumn(record.roles),
scopes_json: toJsonColumn(record.scopes),
remote_ip: record.remoteIp ?? null,
silent: toBooleanColumn(record.silent),
is_repair: toBooleanColumn(record.isRepair),
ts: record.ts,
refreshed_at_ms: record.refreshedAtMs ?? null
};
}
function fromPendingRow(row) {
return {
requestId: row.request_id,
deviceId: row.device_id,
publicKey: row.public_key,
...optional("displayName", row.display_name),
...optional("platform", row.platform),
...optional("deviceFamily", row.device_family),
...optional("clientId", row.client_id),
...optional("clientMode", row.client_mode),
...optional("browserOrigin", row.browser_origin),
...optional("role", row.role),
...optional("roles", fromJsonColumn(row.roles_json) ?? null),
...optional("scopes", fromJsonColumn(row.scopes_json) ?? null),
...optional("remoteIp", row.remote_ip),
...optional("silent", row.silent === null ? null : row.silent !== 0),
...optional("isRepair", row.is_repair === null ? null : row.is_repair !== 0),
ts: row.ts,
...optional("refreshedAtMs", row.refreshed_at_ms)
};
}
function toPairedRow(device) {
return {
device_id: device.deviceId,
public_key: device.publicKey,
display_name: device.displayName ?? null,
operator_label: device.operatorLabel ?? null,
platform: device.platform ?? null,
device_family: device.deviceFamily ?? null,
client_id: device.clientId ?? null,
client_mode: device.clientMode ?? null,
browser_origin: device.browserOrigin ?? null,
role: device.role ?? null,
roles_json: toJsonColumn(device.roles),
scopes_json: toJsonColumn(device.scopes),
approved_scopes_json: toJsonColumn(device.approvedScopes),
remote_ip: device.remoteIp ?? null,
tokens_json: toJsonColumn(device.tokens),
approved_via: device.approvedVia ?? null,
node_surface_json: toJsonColumn(device.nodeSurface),
pending_node_surface_json: toJsonColumn(device.pendingNodeSurface),
created_at_ms: device.createdAtMs,
approved_at_ms: device.approvedAtMs,
last_seen_at_ms: device.lastSeenAtMs ?? null,
last_seen_reason: device.lastSeenReason ?? null
};
}
function fromApprovedViaColumn(value) {
return value !== null && APPROVAL_KINDS.has(value) ? value : null;
}
const PAIRING_SETUP_ACCESS_VALUES = new Set(Object.keys({
full: true,
limited: true,
node: true
}));
function fromSetupCompletionAccessColumn(value) {
return PAIRING_SETUP_ACCESS_VALUES.has(value) ? value : "limited";
}
function fromSetupCompletionDeliveryStateColumn(value) {
return value === "confirmed" ? "confirmed" : "uncertain";
}
function fromSetupCompletionRow(row) {
return {
setupId: row.setup_id,
deviceId: row.device_id,
...optional("deviceName", row.device_name),
access: fromSetupCompletionAccessColumn(row.access),
completedAtMs: row.completed_at_ms,
deliveryState: fromSetupCompletionDeliveryStateColumn(row.delivery_state),
retainUntilMs: row.retain_until_ms
};
}
function fromPairedRow(row) {
const nodeSurface = fromJsonColumn(row.node_surface_json);
if (nodeSurface?.lastHostStats !== void 0 && !isNodeHostStats(nodeSurface.lastHostStats)) delete nodeSurface.lastHostStats;
return {
deviceId: row.device_id,
publicKey: row.public_key,
...optional("displayName", row.display_name),
...optional("operatorLabel", row.operator_label),
...optional("platform", row.platform),
...optional("deviceFamily", row.device_family),
...optional("clientId", row.client_id),
...optional("clientMode", row.client_mode),
...optional("browserOrigin", row.browser_origin),
...optional("role", row.role),
...optional("roles", fromJsonColumn(row.roles_json) ?? null),
...optional("scopes", fromJsonColumn(row.scopes_json) ?? null),
...optional("approvedScopes", fromJsonColumn(row.approved_scopes_json) ?? null),
...optional("remoteIp", row.remote_ip),
...optional("tokens", fromJsonColumn(row.tokens_json) ?? null),
...optional("approvedVia", fromApprovedViaColumn(row.approved_via)),
...optional("nodeSurface", nodeSurface ?? null),
...optional("pendingNodeSurface", fromJsonColumn(row.pending_node_surface_json) ?? null),
createdAtMs: row.created_at_ms,
approvedAtMs: row.approved_at_ms,
...optional("lastSeenAtMs", row.last_seen_at_ms),
...optional("lastSeenReason", row.last_seen_reason)
};
}
function toBootstrapRow(tokenKey, record) {
return {
token_key: tokenKey,
token: record.token,
setup_id: record.setupId ?? null,
ts: record.ts,
device_id: record.deviceId ?? null,
public_key: record.publicKey ?? null,
profile_json: toJsonColumn(record.profile),
redeemed_profile_json: toJsonColumn(record.redeemedProfile),
pending_profile_json: toJsonColumn(record.pendingProfile),
issued_at_ms: record.issuedAtMs,
last_used_at_ms: record.lastUsedAtMs ?? null
};
}
function fromBootstrapRow(row) {
return {
token: row.token,
...optional("setupId", row.setup_id),
ts: row.ts,
...optional("deviceId", row.device_id),
...optional("publicKey", row.public_key),
...optional("profile", fromJsonColumn(row.profile_json) ?? null),
...optional("redeemedProfile", fromJsonColumn(row.redeemed_profile_json) ?? null),
...optional("pendingProfile", fromJsonColumn(row.pending_profile_json) ?? null),
issuedAtMs: row.issued_at_ms,
...optional("lastUsedAtMs", row.last_used_at_ms)
};
}
function readDevicePairingStoreStateFromDatabase(db) {
const kysely = getNodeSqliteKysely(db);
const pendingById = {};
for (const row of executeSqliteQuerySync(db, kysely.selectFrom("device_pairing_pending").selectAll()).rows) pendingById[row.request_id] = fromPendingRow(row);
return {
pendingById,
pairedByDeviceId: readPairedDevicePairingRecordsFromDatabase(db)
};
}
function readPairedDevicePairingRecordsFromDatabase(db) {
const kysely = getNodeSqliteKysely(db);
return Object.fromEntries(executeSqliteQuerySync(db, kysely.selectFrom("device_pairing_paired").selectAll()).rows.map((row) => [row.device_id, fromPairedRow(row)]));
}
/** Load the full pending + paired device snapshot from the shared state DB. */
function loadDevicePairingStoreState(baseDir) {
const database = openOpenClawStateDatabase(resolveDevicePairingStateDbOptions(baseDir));
return structuredClone(readCachedDevicePairingStoreSnapshot(database.db, database.path, () => readDevicePairingStoreStateFromDatabase(database.db)).state);
}
/** Load one paired-device row from an existing shared-state transaction. */
function loadPairedDevicePairingStoreRecordFromDatabase(db, deviceId) {
const normalizedDeviceId = deviceId.trim();
if (!normalizedDeviceId) return null;
const kysely = getNodeSqliteKysely(db);
const row = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("device_pairing_paired").selectAll().where("device_id", "=", normalizedDeviceId));
return row ? fromPairedRow(row) : null;
}
/** Read and patch one device under the worker's transaction and commit admission. */
function updatePairedDeviceInTransaction(deviceId, baseDir, update) {
return runDevicePairingStoreMutation(baseDir, ({ db }) => {
const normalizedDeviceId = deviceId.trim();
const device = loadPairedDevicePairingStoreRecordFromDatabase(db, normalizedDeviceId);
const result = update(device);
const patch = result.patch;
if (!patch || Object.keys(patch).length === 0) return {
mutated: false,
value: result.value
};
if (!device) throw new Error("cannot update a missing paired device");
const kysely = getNodeSqliteKysely(db);
executeSqliteQuerySync(db, kysely.updateTable("device_pairing_paired").set({
..."displayName" in patch ? { display_name: patch.displayName ?? null } : {},
..."operatorLabel" in patch ? { operator_label: patch.operatorLabel ?? null } : {},
..."platform" in patch ? { platform: patch.platform ?? null } : {},
..."clientId" in patch ? { client_id: patch.clientId ?? null } : {},
..."clientMode" in patch ? { client_mode: patch.clientMode ?? null } : {},
..."remoteIp" in patch ? { remote_ip: patch.remoteIp ?? null } : {},
..."lastSeenAtMs" in patch ? { last_seen_at_ms: patch.lastSeenAtMs ?? null } : {},
..."lastSeenReason" in patch ? { last_seen_reason: patch.lastSeenReason ?? null } : {},
..."tokens" in patch ? { tokens_json: toJsonColumn(patch.tokens) } : {},
..."nodeSurface" in patch ? { node_surface_json: toJsonColumn(patch.nodeSurface) } : {}
}).where("device_id", "=", normalizedDeviceId));
return {
mutated: true,
value: result.value
};
});
}
/** Replace the pending and/or paired table contents with the given snapshot. */
function persistDevicePairingStoreState(state, baseDir, target, options) {
runDevicePairingStoreMutation(baseDir, ({ db }) => {
const kysely = getNodeSqliteKysely(db);
if (target !== "paired") {
executeSqliteQuerySync(db, kysely.deleteFrom("device_pairing_pending"));
const rows = Object.values(state.pendingById).map(toPendingRow);
if (rows.length > 0) executeSqliteQuerySync(db, kysely.insertInto("device_pairing_pending").values(rows));
}
if (target !== "pending") {
executeSqliteQuerySync(db, kysely.deleteFrom("device_pairing_paired"));
const rows = Object.values(state.pairedByDeviceId).map(toPairedRow);
if (rows.length > 0) executeSqliteQuerySync(db, kysely.insertInto("device_pairing_paired").values(rows));
}
for (const nodeId of new Set(options?.clearApnsNodeIds ?? [])) clearApnsRegistrationFromDatabase(db, nodeId);
if (options?.retiredNodeToken) clearDeviceAuthTokenFromDatabase(db, {
...options.retiredNodeToken,
role: "node"
});
return {
mutated: true,
value: void 0
};
});
}
/** Load all bootstrap token records keyed by token key. */
function loadDeviceBootstrapTokenRecords(baseDir) {
const { db } = openOpenClawStateDatabase(resolveDevicePairingStateDbOptions(baseDir));
return readDeviceBootstrapTokenRecordsFromDatabase(db);
}
function readDeviceBootstrapTokenRecordsFromDatabase(db) {
const kysely = getNodeSqliteKysely(db);
const state = {};
const rows = tableHasColumn(db, "device_bootstrap_tokens", "setup_id") ? executeSqliteQuerySync(db, kysely.selectFrom("device_bootstrap_tokens").selectAll()).rows : executeSqliteQuerySync(db, kysely.selectFrom("device_bootstrap_tokens").select(DEVICE_BOOTSTRAP_TOKEN_COLUMNS_WITHOUT_SETUP)).rows.map((row) => Object.assign(row, { setup_id: null }));
for (const row of rows) state[row.token_key] = fromBootstrapRow(row);
return state;
}
/** Replace the bootstrap token table contents with the given snapshot. */
function persistDeviceBootstrapTokenRecords(state, baseDir) {
runOpenClawStateWriteTransaction(({ db }) => {
const rows = Object.entries(state).map(([tokenKey, record]) => toBootstrapRow(tokenKey, record));
if (rows.some((row) => row.setup_id !== null)) ensureDevicePairSetupBootstrapSchema(db);
const kysely = getNodeSqliteKysely(db);
executeSqliteQuerySync(db, kysely.deleteFrom("device_bootstrap_tokens"));
if (rows.length > 0) {
if (tableHasColumn(db, "device_bootstrap_tokens", "setup_id")) executeSqliteQuerySync(db, kysely.insertInto("device_bootstrap_tokens").values(rows));
else {
const rowsWithoutSetup = rows.map(({ setup_id: _setupId, ...row }) => row);
executeSqliteQuerySync(db, kysely.insertInto("device_bootstrap_tokens").values(rowsWithoutSetup));
}
}
}, resolveDevicePairingStateDbOptions(baseDir));
}
/** Consume one bound bootstrap credential and record its setup outcome atomically. */
function consumeDeviceBootstrapTokenWithSetupCompletionInTransaction(params) {
const token = params.token.trim();
const deviceId = params.deviceId.trim();
if (!token || !deviceId) return null;
return runOpenClawStateWriteTransaction(({ db }) => {
ensureDevicePairSetupBootstrapSchema(db);
ensureDevicePairSetupCompletionSchema(db);
const kysely = getNodeSqliteKysely(db);
const tokenRow = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("device_bootstrap_tokens").selectAll().where("token_key", "=", token));
if (!tokenRow || tokenRow.token !== token || tokenRow.device_id?.trim() !== deviceId) return null;
const record = fromBootstrapRow(tokenRow);
if (params.nowMs > resolveDeviceBootstrapTokenExpiresAtMs(record)) return null;
const paired = record.setupId || params.pairedDeviceMatches ? loadPairedDevicePairingStoreRecordFromDatabase(db, deviceId) : null;
if (params.pairedDeviceMatches && !params.pairedDeviceMatches(paired, record)) return null;
const deviceName = paired?.operatorLabel ?? paired?.displayName;
const completion = record.setupId ? {
setupId: record.setupId,
deviceId,
...deviceName ? { deviceName } : {},
access: resolvePairingSetupAccess(record.profile),
completedAtMs: params.completedAtMs,
deliveryState: "uncertain",
retainUntilMs: params.retainUntilMs
} : void 0;
if (!completion || record.profile?.purpose !== "cloud-worker") executeSqliteQuerySync(db, kysely.deleteFrom("device_bootstrap_tokens").where("token_key", "=", tokenRow.token_key));
if (completion) {
if (record.profile?.purpose === "cloud-worker") params.recordWorkerEnvironment(bindCloudWorkerSetupCompletion({
db,
completion,
credentialDigest: sha256Base64Url(tokenRow.token)
}));
executeSqliteQuerySync(db, kysely.deleteFrom("device_pair_setup_completions").where((eb) => eb.or([eb("retain_until_ms", "<=", params.retentionNowMs), eb("setup_id", "=", completion.setupId)])));
executeSqliteQuerySync(db, kysely.insertInto("device_pair_setup_completions").values({
setup_id: completion.setupId,
device_id: completion.deviceId,
device_name: completion.deviceName ?? null,
access: completion.access,
completed_at_ms: completion.completedAtMs,
delivery_state: completion.deliveryState,
retain_until_ms: completion.retainUntilMs
}));
}
return {
record,
...completion ? { completion } : {}
};
}, resolveDevicePairingStateDbOptions(params.baseDir));
}
/** Mark one consumed setup handoff as delivered without reviving an expired or replaced row. */
function confirmDevicePairSetupCompletionDeliveryInTransaction(params) {
const setupId = params.setupId.trim();
const deviceId = params.deviceId.trim();
if (!setupId || !deviceId) return null;
return runOpenClawStateWriteTransaction(({ db }) => {
ensureDevicePairSetupCompletionSchema(db);
const kysely = getNodeSqliteKysely(db);
const row = executeSqliteQueryTakeFirstSync(db, kysely.updateTable("device_pair_setup_completions").set({ delivery_state: "confirmed" }).where("setup_id", "=", setupId).where("device_id", "=", deviceId).where("retain_until_ms", ">", params.nowMs).returningAll());
if (!row) return null;
executeSqliteQuerySync(db, kysely.deleteFrom("device_bootstrap_tokens").where("setup_id", "=", setupId).where("device_id", "=", deviceId));
return fromSetupCompletionRow(row);
}, resolveDevicePairingStateDbOptions(params.baseDir));
}
/** Prune retained setup outcomes when the Gateway maintenance owner ticks. */
function pruneExpiredDevicePairSetupCompletionRecords(nowMs, baseDir) {
const databaseOptions = resolveDevicePairingStateDbOptions(baseDir);
const database = openOpenClawStateDatabase(databaseOptions);
if (!tableExists(database.db, "device_pair_setup_completions")) return 0;
return runOpenClawStateWriteTransaction(({ db }) => {
const kysely = getNodeSqliteKysely(db);
const result = executeSqliteQuerySync(db, kysely.deleteFrom("device_pair_setup_completions").where("retain_until_ms", "<=", nowMs));
return Number(result.numAffectedRows ?? 0);
}, {
...databaseOptions,
database
});
}
/** Prune elapsed setup completions, then read one live record. */
function loadDevicePairSetupCompletionRecord(setupId, nowMs, baseDir) {
const databaseOptions = resolveDevicePairingStateDbOptions(baseDir);
const database = openOpenClawStateDatabase(databaseOptions);
ensureDevicePairSetupCompletionSchema(database.db);
return runOpenClawStateWriteTransaction(({ db }) => {
const kysely = getNodeSqliteKysely(db);
executeSqliteQuerySync(db, kysely.deleteFrom("device_pair_setup_completions").where("retain_until_ms", "<=", nowMs));
const row = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("device_pair_setup_completions").selectAll().where("setup_id", "=", setupId));
return row ? fromSetupCompletionRow(row) : null;
}, {
...databaseOptions,
database
});
}
//#endregion
//#region src/infra/device-pairing-state.kernel.ts
const DEVICE_PAIRING_PENDING_TTL_MS = 3e5;
/** Read authoritative rows inside the worker transaction or an admitted migration. */
function loadDevicePairingStateForMutation(nowMs, baseDir) {
const state = loadDevicePairingStoreState(baseDir);
pruneExpiredPending(state.pendingById, nowMs, DEVICE_PAIRING_PENDING_TTL_MS);
return state;
}
/** Resolve the expiry timestamp for one pending device-pairing request. */
function resolvePairingRequestExpiry(timestampMs) {
return timestampMs + DEVICE_PAIRING_PENDING_TTL_MS;
}
/** Normalize one requested or approved pairing role. */
function normalizeDevicePairingRole(role) {
const trimmed = role?.trim();
return trimmed ? trimmed : null;
}
/** Merge pairing roles while preserving first-seen order. */
function mergeDevicePairingRoles(...items) {
const roles = /* @__PURE__ */ new Set();
for (const item of items) for (const role of normalizeUniqueSingleOrTrimmedStringList(item)) roles.add(role);
if (roles.size === 0) return;
return [...roles];
}
/** Merge pairing scopes while preserving first-seen order and explicit emptiness. */
function mergeDevicePairingScopes(...items) {
const scopes = /* @__PURE__ */ new Set();
let sawExplicitScopeList = false;
for (const item of items) {
if (!Array.isArray(item)) continue;
sawExplicitScopeList = true;
for (const scope of normalizeUniqueSingleOrTrimmedStringList(item)) scopes.add(scope);
}
if (scopes.size === 0) return sawExplicitScopeList ? [] : void 0;
return [...scopes];
}
/** Preserve only approval scopes owned by one pairing role. */
function preserveDeviceRoleScopes(role, scopes) {
return normalizeUniqueSingleOrTrimmedStringList(scopes).filter((scope) => role === "operator" ? scope.startsWith("operator.") : !scope.startsWith("operator."));
}
/** Compare pairing role or scope lists as unordered sets. */
function sameDevicePairingStringSet(left, right) {
if (left.length !== right.length) return false;
const rightSet = new Set(right);
for (const value of left) if (!rightSet.has(value)) return false;
return true;
}
/** Resolve the normalized role set requested by a pairing record. */
function resolveRequestedDeviceRoles(input) {
return mergeDevicePairingRoles(input.roles, input.role) ?? [];
}
//#endregion
//#region src/infra/device-pairing-identity.ts
function listActiveTokenRoles(tokens) {
if (!tokens) return;
return mergeDevicePairingRoles(Object.values(tokens).filter((entry) => !entry.revokedAtMs).map((entry) => entry.role));
}
/** List the durable roles an owner approved for a paired device record. */
function listApprovedPairedDeviceRoles(device) {
return mergeDevicePairingRoles(device.roles, device.role) ?? [];
}
/** List active-token roles, bounded by the durable approved pairing roles. */
function listEffectivePairedDeviceRoles(device) {
const activeTokenRoles = listActiveTokenRoles(device.tokens);
if (activeTokenRoles && activeTokenRoles.length > 0) {
const approvedRoles = new Set(listApprovedPairedDeviceRoles(device));
return activeTokenRoles.filter((role) => approvedRoles.has(role));
}
return [];
}
/** Return whether a paired device currently has an active token for one role. */
function hasEffectivePairedDeviceRole(device, role) {
const normalized = normalizeDevicePairingRole(role);
if (!normalized) return false;
return listEffectivePairedDeviceRoles(device).includes(normalized);
}
/** Resolve the authenticated node pairing independently of surface approval. */
function resolveNodePairingIdentity(device) {
if (!device || !hasEffectivePairedDeviceRole(device, "node")) return null;
const nodeToken = device.tokens?.node;
if (!nodeToken) return null;
const key = sha256Hex([
device.publicKey,
device.createdAtMs,
nodeToken.token,
nodeToken.createdAtMs,
nodeToken.rotatedAtMs ?? "",
nodeToken.revokedAtMs ?? ""
].join("\0"));
return {
nodeId: device.deviceId,
key
};
}
/** Resolve the durable node-owned identity used to admit asynchronous work. */
function resolveNodePairingGeneration(device) {
if (!device || !hasEffectivePairedDeviceRole(device, "node") || !device.nodeSurface) return null;
const nodeToken = device.tokens?.node;
const nodeSurface = device.nodeSurface;
const key = sha256Hex([
device.publicKey,
device.createdAtMs,
nodeToken?.token ?? "",
nodeToken?.revokedAtMs ?? "",
nodeSurface.createdAtMs,
nodeSurface.approvedAtMs
].join("\0"));
return {
nodeId: device.deviceId,
key
};
}
/** Clear node runtime facts when their owning pairing generation changes. */
function clearNodePairingGenerationState(device, previousGeneration) {
const nextGeneration = resolveNodePairingGeneration(device);
if (previousGeneration?.key === nextGeneration?.key || !device.nodeSurface) return;
delete device.nodeSurface.bins;
delete device.nodeSurface.sessionHost;
}
/** Resolve connection identity and optional approved surface generation from one row. */
function resolveNodePairingState(device) {
const identity = resolveNodePairingIdentity(device);
if (!identity) return null;
return {
identity,
generation: resolveNodePairingGeneration(device)
};
}
//#endregion
export { hasCloudWorkerSetupDeviceBinding as A, readDevicePairingStoreStateFromDatabase as C, clearApnsRegistrationFromDatabase as D, withDevicePairingStoreDatabase as E, withDevicePairingMutationAdmission as M, DEVICE_BOOTSTRAP_TOKEN_TTL_MS as N, nextApnsRegistrationVersion as O, resolveDeviceBootstrapTokenExpiresAtMs as P, pruneExpiredDevicePairSetupCompletionRecords as S, updatePairedDeviceInTransaction as T, loadDeviceBootstrapTokenRecords as _, resolveNodePairingGeneration as a, persistDeviceBootstrapTokenRecords as b, mergeDevicePairingRoles as c, preserveDeviceRoleScopes as d, resolvePairingRequestExpiry as f, consumeDeviceBootstrapTokenWithSetupCompletionInTransaction as g, confirmDevicePairSetupCompletionDeliveryInTransaction as h, listEffectivePairedDeviceRoles as i, requestDevicePairingMutationAdmission as j, resolveDevicePairingStoreRevision as k, mergeDevicePairingScopes as l, sameDevicePairingStringSet as m, hasEffectivePairedDeviceRole as n, resolveNodePairingState as o, resolveRequestedDeviceRoles as p, listApprovedPairedDeviceRoles as r, loadDevicePairingStateForMutation as s, clearNodePairingGenerationState as t, normalizeDevicePairingRole as u, loadDevicePairSetupCompletionRecord as v, readPairedDevicePairingRecordsFromDatabase as w, persistDevicePairingStoreState as x, loadPairedDevicePairingStoreRecordFromDatabase as y };