openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
810 lines (809 loc) • 30.7 kB
JavaScript
import { h as parseSecretRef } from "./types.secrets-DsPRn9Dz.mjs";
import { r as isMissingPathError, t as hasErrnoCode } from "./errno-FR4CRyya.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { Fn as object, Jn as string, Pn as number, or as union, xn as literal } from "./schemas-BOYIvvln.mjs";
import { i as pinDirectory, o as publishFileNoClobber, s as requireDirectorySync, t as ensureDurableDirectory } from "./directory-durability-CqSvP1tc.mjs";
import { t as validateProviderSettings } from "./provider-settings-CR-jdYHV.mjs";
import { t as resolveStorageProvider } from "./storage-provider-registry-DusKBTCi.mjs";
import path from "node:path";
import fs from "node:fs/promises";
import { createCipheriv, createDecipheriv, createHmac, hkdfSync, randomBytes, randomUUID, scrypt, timingSafeEqual } from "node:crypto";
import { root } from "@openclaw/fs-safe/root";
//#region src/storage/encryption.ts
const MAGIC = Buffer.from("OCSTOR1\n");
const STORAGE_SEGMENT_BYTES = 1048576;
const TAG_BYTES = 16;
const MAX_HEADER_BYTES = 1024;
/** Reads only the requested bounded window, even when the upstream chunks are uneven. */
var StreamReader = class {
constructor(body) {
this.chunk = /* @__PURE__ */ new Uint8Array();
this.offset = 0;
this.iterator = body[Symbol.asyncIterator]();
}
async read(maxBytes) {
const result = Buffer.allocUnsafe(maxBytes);
let used = 0;
while (used < maxBytes) {
if (this.offset === this.chunk.length) {
const next = await this.iterator.next();
if (next.done) break;
this.chunk = next.value;
this.offset = 0;
if (this.chunk.length === 0) continue;
}
const count = Math.min(maxBytes - used, this.chunk.length - this.offset);
result.set(this.chunk.subarray(this.offset, this.offset + count), used);
used += count;
this.offset += count;
}
return result.subarray(0, used);
}
async close() {
await this.iterator.return?.();
}
};
function headerBytes(objSalt, noncePrefix) {
const json = Buffer.from(JSON.stringify({
v: 1,
objSalt: objSalt.toString("base64"),
noncePrefix: noncePrefix.toString("base64"),
segmentBytes: STORAGE_SEGMENT_BYTES
}));
const length = Buffer.alloc(4);
length.writeUInt32BE(json.length);
return Buffer.concat([
MAGIC,
length,
json
]);
}
const HEADER_BYTES = headerBytes(Buffer.alloc(32), Buffer.alloc(7)).length;
function encryptedStorageSize(plaintextBytes) {
if (!Number.isSafeInteger(plaintextBytes) || plaintextBytes < 0) throw new Error("Storage size must be a nonnegative safe integer.");
const segments = Math.floor(plaintextBytes / STORAGE_SEGMENT_BYTES) + 1;
if (segments > 4294967296) throw new Error("Storage object exceeds the encryption segment limit.");
return HEADER_BYTES + plaintextBytes + segments * TAG_BYTES;
}
function nonce(prefix, index, last) {
if (index > 4294967295) throw new Error("Storage object exceeds the encryption segment limit.");
const value = Buffer.alloc(12);
prefix.copy(value);
value.writeUInt32BE(index, 7);
value[11] = last ? 1 : 0;
return value;
}
async function* encryptStorageObject(body, masterKey) {
const salt = randomBytes(32);
const prefix = randomBytes(7);
const header = headerBytes(salt, prefix);
const key = Buffer.from(hkdfSync("sha256", masterKey, salt, "openclaw-storage v1 object", 32));
const reader = new StreamReader(body);
try {
yield header;
for (let index = 0;; index++) {
const plaintext = await reader.read(STORAGE_SEGMENT_BYTES);
const last = plaintext.length < STORAGE_SEGMENT_BYTES;
const cipher = createCipheriv("aes-256-gcm", key, nonce(prefix, index, last));
cipher.setAAD(header);
yield Buffer.concat([
cipher.update(plaintext),
cipher.final(),
cipher.getAuthTag()
]);
if (last) break;
}
} finally {
key.fill(0);
await reader.close();
}
}
function decodeBase64(value, size) {
if (typeof value !== "string") throw new Error("Invalid storage encryption header.");
const decoded = Buffer.from(value, "base64");
if (decoded.length !== size || decoded.toString("base64") !== value) throw new Error("Invalid storage encryption header.");
return decoded;
}
async function readHeader(reader) {
const preamble = await reader.read(MAGIC.length + 4);
if (preamble.length !== MAGIC.length + 4 || !preamble.subarray(0, MAGIC.length).equals(MAGIC)) throw new Error("Invalid storage encryption magic.");
const length = preamble.readUInt32BE(MAGIC.length);
if (length === 0 || length > MAX_HEADER_BYTES) throw new Error("Invalid storage encryption header length.");
const json = await reader.read(length);
if (json.length !== length) throw new Error("Truncated storage encryption header.");
const value = JSON.parse(json.toString("utf8"));
if (!value || typeof value !== "object" || !("v" in value) || value.v !== 1 || !("segmentBytes" in value) || value.segmentBytes !== STORAGE_SEGMENT_BYTES || !("objSalt" in value) || !("noncePrefix" in value)) throw new Error("Unsupported storage encryption header.");
return {
header: Buffer.concat([preamble, json]),
salt: decodeBase64(value.objSalt, 32),
prefix: decodeBase64(value.noncePrefix, 7)
};
}
async function* decryptStorageObject(body, masterKey) {
const reader = new StreamReader(body);
let key;
try {
const { header, salt, prefix } = await readHeader(reader);
key = Buffer.from(hkdfSync("sha256", masterKey, salt, "openclaw-storage v1 object", 32));
for (let index = 0;; index++) {
const segment = await reader.read(1048592);
if (segment.length < TAG_BYTES) throw new Error("Truncated storage object: missing final segment.");
const last = segment.length < 1048592;
const decipher = createDecipheriv("aes-256-gcm", key, nonce(prefix, index, last));
decipher.setAAD(header);
decipher.setAuthTag(segment.subarray(-16));
const plaintext = Buffer.concat([decipher.update(segment.subarray(0, -16)), decipher.final()]);
if (plaintext.length > 0) yield plaintext;
if (last) return;
}
} finally {
key?.fill(0);
await reader.close();
}
}
/** The fixed v1 header and segment tags make plaintext size a metadata-only calculation. */
function plaintextStorageSize(storedBytes) {
if (!Number.isSafeInteger(storedBytes) || storedBytes < 0) throw new Error("Storage size must be a nonnegative safe integer.");
const payloadBytes = storedBytes - HEADER_BYTES;
const complete = Math.floor(payloadBytes / 1048592);
const finalBytes = payloadBytes % 1048592;
if (payloadBytes < TAG_BYTES || finalBytes < TAG_BYTES) throw new Error("Truncated storage object: missing final segment.");
if (complete + 1 > 4294967296) throw new Error("Storage object exceeds the encryption segment limit.");
return payloadBytes - (complete + 1) * TAG_BYTES;
}
//#endregion
//#region src/storage/keys.ts
/** Object keys stay portable across local filesystems and object stores. */
function validateStorageKey(key) {
if (Buffer.byteLength(key) > 512 || !key.split("/").every((segment) => /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== "..")) throw new Error("Storage keys must contain nonempty /-separated letters, digits, dots, underscores or hyphens, without . or .. segments, and be at most 512 bytes.");
}
function validateStoragePrefix(prefix) {
if (prefix !== "") {
validateStorageKey(prefix.endsWith("/") ? prefix.slice(0, -1) : prefix);
if (Buffer.byteLength(prefix) > 512) throw new Error("Storage prefixes must be at most 512 bytes.");
}
}
//#endregion
//#region src/storage/filesystem.ts
function validateSettings(settings) {
if (typeof settings.path !== "string" || !path.isAbsolute(settings.path)) return "Filesystem storage settings.path must be an absolute directory path.";
if (Object.keys(settings).some((key) => key !== "path")) return "Filesystem storage only accepts the path setting.";
}
function ignoreMissingPath(error) {
if (!isMissingPathError(error)) throw error;
}
const filesystemStorageProvider = {
id: "filesystem",
label: "Filesystem",
validateSettings,
describeTarget: (settings) => typeof settings.path === "string" ? settings.path : void 0,
async open({ settings, signal }) {
const settingsError = validateSettings(settings);
if (settingsError || typeof settings.path !== "string") throw new Error(settingsError ?? "Filesystem storage requires a path.");
signal?.throwIfAborted();
const directory = await pinDirectory(settings.path, { label: "Storage root directory" });
try {
const files = await root(directory.receipt.realPath, {
symlinks: "reject",
mutationSymlinks: "reject",
hardlinks: "reject",
mkdir: false
});
await directory.assertCurrent();
const readers = /* @__PURE__ */ new Set();
async function assertCurrent(operationSignal) {
operationSignal?.throwIfAborted();
await directory.assertCurrent();
}
const backend = {
displayTarget: settings.path,
async probe(opts) {
await assertCurrent(opts?.signal);
const stats = await fs.statfs(directory.receipt.realPath);
await assertCurrent(opts?.signal);
return {
freeBytes: stats.bavail * stats.bsize,
totalBytes: stats.blocks * stats.bsize
};
},
async putObject(key, body, opts) {
await assertCurrent(opts.signal);
const parentKey = path.posix.dirname(key);
const parentPath = path.join(files.rootReal, parentKey);
if (parentKey !== ".") {
const created = await ensureDurableDirectory({
directoryPath: parentPath,
label: "Storage object directory",
create: async () => {
await assertCurrent(opts.signal);
await files.mkdir(parentKey);
}
});
requireDirectorySync(created.parentSync, "Storage object directory");
}
await assertCurrent(opts.signal);
const parent = await pinDirectory(parentPath, { label: "Storage object directory" });
const temporaryKey = path.posix.join(parentKey, `.openclaw-put-${randomUUID()}.tmp~`);
let temporaryCreated = false;
let sizeBytes = 0;
async function* measuredBody() {
for await (const chunk of body) {
opts.signal?.throwIfAborted();
sizeBytes += chunk.byteLength;
if (opts.sizeBytes !== void 0 && sizeBytes > opts.sizeBytes) throw new Error(`Storage object ${key} exceeds its declared size.`);
yield chunk;
}
if (opts.sizeBytes !== void 0 && sizeBytes !== opts.sizeBytes) throw new Error(`Storage object ${key} does not match its declared size.`);
}
try {
await files.create(temporaryKey, measuredBody(), {
durable: "file",
mkdir: false,
mode: 384,
maxBytes: opts.sizeBytes ?? Number.MAX_SAFE_INTEGER,
signal: opts.signal
});
temporaryCreated = true;
await parent.assertCurrent();
await assertCurrent(opts.signal);
await publishFileNoClobber(path.join(files.rootReal, temporaryKey), path.join(files.rootReal, key), {
strategy: "link-required",
moveSource: true,
durability: "fail-closed"
});
temporaryCreated = false;
requireDirectorySync(await parent.sync(), "Storage object directory");
await assertCurrent(opts.signal);
return { sizeBytes };
} finally {
try {
if (temporaryCreated) {
await directory.assertCurrent();
await parent.assertCurrent();
await files.remove(temporaryKey, { force: true });
requireDirectorySync(await parent.sync(), "Storage object directory");
}
} finally {
await parent.close();
}
}
},
async getObject(key, opts) {
await assertCurrent(opts?.signal);
const opened = await files.open(key).catch(ignoreMissingPath);
if (!opened) return;
const handle = opened.handle;
readers.add(handle);
return (async function* () {
try {
await assertCurrent(opts?.signal);
while (true) {
opts?.signal?.throwIfAborted();
const buffer = Buffer.allocUnsafe(65536);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
if (bytesRead === 0) break;
yield buffer.subarray(0, bytesRead);
}
} finally {
readers.delete(handle);
await handle.close();
}
})();
},
async statObject(key, opts) {
await assertCurrent(opts?.signal);
const stat = await files.stat(key).catch(ignoreMissingPath);
await assertCurrent(opts?.signal);
return stat?.isFile ? {
key,
sizeBytes: stat.size,
modifiedAt: stat.mtimeMs
} : void 0;
},
async *listObjects(prefix, opts) {
await assertCurrent(opts?.signal);
const parentKey = prefix.slice(0, prefix.lastIndexOf("/") + 1).replace(/\/$/u, "");
if (!(await files.stat(parentKey || ".").catch(ignoreMissingPath))?.isDirectory) return;
for await (const entry of files.walk(parentKey, {
symlinkPolicy: "skip",
signal: opts?.signal,
order: "filesystem",
entryFilter(candidate) {
try {
validateStorageKey(candidate.relativePath);
} catch {
return "skip-subtree";
}
return "include";
}
})) {
if (entry.kind !== "file" || !entry.relativePath.startsWith(prefix)) continue;
const stat = await backend.statObject(entry.relativePath, opts);
if (stat) yield stat;
}
},
async deleteObject(key, opts) {
await assertCurrent(opts?.signal);
if (!await backend.statObject(key, opts)) return;
const parent = await pinDirectory(path.dirname(path.join(files.rootReal, key)));
try {
await assertCurrent(opts?.signal);
await files.remove(key, {
force: true,
signal: opts?.signal
});
requireDirectorySync(await parent.sync(), "Storage object directory");
} finally {
await parent.close();
}
},
async close() {
try {
await Promise.all([...readers].map((handle) => handle.close()));
} finally {
readers.clear();
await directory.close();
}
}
};
return backend;
} catch (error) {
await directory.close();
throw error;
}
}
};
//#endregion
//#region src/storage/marker.ts
const STORAGE_MARKER_KEY = "openclaw-storage.json";
const KEY_CHECK = "openclaw-storage key check v1";
const base64 = (bytes) => string().refine((value) => {
const decoded = Buffer.from(value, "base64");
return decoded.length === bytes && decoded.toString("base64") === value;
});
const markerSchema = object({
version: literal(1),
locationId: string().uuid(),
createdAt: number().int().nonnegative(),
encryption: union([literal("none"), object({
kdf: literal("scrypt"),
salt: base64(16),
N: literal(32768),
r: literal(8),
p: literal(1),
keyCheck: base64(32)
}).strict()])
}).strict();
var StorageLocationError = class extends Error {
constructor(state, message) {
super(message);
this.name = "StorageLocationError";
this.state = state;
}
};
const cacheFingerprintKey = randomBytes(32);
const masterKeys = /* @__PURE__ */ new Map();
function deriveMasterKey(locationName, passphrase, salt) {
const fingerprint = createHmac("sha256", cacheFingerprintKey).update(salt).update("\0").update(passphrase).digest("hex");
const cached = masterKeys.get(locationName);
if (cached?.fingerprint === fingerprint) return cached.key;
const key = new Promise((resolve, reject) => {
scrypt(passphrase, Buffer.from(salt, "base64"), 32, {
N: 32768,
r: 8,
p: 1,
maxmem: 67108864
}, (error, derived) => {
if (error) reject(error);
else resolve(derived);
});
});
masterKeys.set(locationName, {
fingerprint,
key
});
key.catch(() => {
if (masterKeys.get(locationName)?.key === key) masterKeys.delete(locationName);
});
return key;
}
async function readStorageMarker(backend, signal) {
const body = await backend.getObject(STORAGE_MARKER_KEY, { signal });
if (!body) return;
const chunks = [];
let size = 0;
for await (const chunk of body) {
size += chunk.byteLength;
if (size > 4096) throw new StorageLocationError("error", "Storage marker exceeds 4096 bytes.");
chunks.push(Buffer.from(chunk));
}
try {
return markerSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
} catch {
throw new StorageLocationError("error", "Invalid storage location marker.");
}
}
async function verifyStorageMarker(name, marker, passphrase) {
if (marker.encryption === "none") {
if (passphrase !== void 0) throw new StorageLocationError("wrong-key", `Storage location "${name}" was initialized without encryption; restore its original encryption configuration.`);
return;
}
if (passphrase === void 0) throw new StorageLocationError("wrong-key", `Storage location "${name}" requires its original encryption passphrase.`);
const masterKey = await deriveMasterKey(name, passphrase, marker.encryption.salt);
const expected = Buffer.from(marker.encryption.keyCheck, "base64");
const actual = createHmac("sha256", masterKey).update(KEY_CHECK).digest();
if (!timingSafeEqual(expected, actual)) throw new StorageLocationError("wrong-key", `Wrong encryption passphrase for storage location "${name}".`);
return masterKey;
}
async function createStorageMarker(name, passphrase) {
const marker = {
version: 1,
locationId: randomUUID(),
createdAt: Date.now(),
encryption: "none"
};
if (passphrase !== void 0) {
const salt = randomBytes(16).toString("base64");
const key = await deriveMasterKey(name, passphrase, salt);
marker.encryption = {
kdf: "scrypt",
salt,
N: 32768,
r: 8,
p: 1,
keyCheck: createHmac("sha256", key).update(KEY_CHECK).digest("base64")
};
}
return marker;
}
//#endregion
//#region src/storage/provider.ts
async function acquireStorageProvider(params) {
if (params.providerId === "filesystem") return {
provider: filesystemStorageProvider,
release: async () => {}
};
if (params.registry) {
const provider = resolveStorageProvider(params.registry, params.providerId);
if (!provider) throw new Error(`Storage provider "${params.providerId}" is not loaded; check its plugin configuration.`);
return {
provider,
registry: params.registry,
release: async () => {}
};
}
const { applyPluginAutoEnable } = await import("./plugin-auto-enable-Bw5N3luC.mjs");
const enabled = applyPluginAutoEnable({
config: params.config,
env: params.env ?? process.env
});
const { resolveManifestContractRuntimePluginResolution } = await import("./manifest-contract-runtime-ZAATHmU-.mjs");
const resolution = resolveManifestContractRuntimePluginResolution({
cfg: enabled.config,
contract: "storageProviders",
value: params.providerId
});
if (resolution.pluginIds.length === 0) throw new Error(`Storage provider "${params.providerId}" is unavailable; install and enable its plugin.`);
const { acquirePluginRegistryForInspection } = await import("./plugins/loader.js");
const acquisition = await acquirePluginRegistryForInspection({
config: enabled.config,
autoEnabledReasons: enabled.autoEnabledReasons,
onlyPluginIds: resolution.pluginIds
});
try {
const provider = resolveStorageProvider(acquisition.registry, params.providerId);
if (!provider) throw new Error(`Plugin did not register storage provider "${params.providerId}".`);
return {
provider,
registry: acquisition.registry,
release: acquisition.release
};
} catch (error) {
await acquisition.release();
throw error;
}
}
//#endregion
//#region src/storage/locations.ts
function configuredLocation(params) {
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(params.name)) throw new StorageLocationError("error", "Invalid storage location name.");
const location = params.config.storage?.locations?.[params.name];
if (!location) throw new StorageLocationError("error", `Storage location "${params.name}" is not configured.`);
return location;
}
function unavailable(name, displayTarget) {
return new StorageLocationError("unavailable", `Storage location "${name}"${displayTarget ? ` (${displayTarget})` : ""} has no initialization marker. If this is a new location, run \`openclaw storage init ${name}\`; otherwise reconnect the disk or check the bucket and prefix.`);
}
function storageLocationError(error) {
if (error instanceof StorageLocationError) return error;
if (isMissingPathError(error) || hasErrnoCode(error, "path-mismatch")) return new StorageLocationError("unavailable", "Storage directory is unavailable. Reconnect the disk and check the configured path; storage init requires an existing directory.");
return new StorageLocationError("error", "Storage could not be accessed. Check its provider settings, credentials, and connection.");
}
function probeFailure(error) {
const { state, message } = storageLocationError(error);
return {
state,
message
};
}
async function prepare(params) {
params.signal?.throwIfAborted();
const location = configuredLocation(params);
const invalid = validateProviderSettings(location.settings, "Storage location");
if (invalid) throw new StorageLocationError("error", invalid);
const acquired = await acquireStorageProvider({
providerId: location.provider,
config: params.config,
registry: params.registry,
env: params.env
}).catch((error) => {
throw storageLocationError(error);
});
try {
const message = acquired.provider.validateSettings?.(location.settings);
if (message) throw new StorageLocationError("error", message);
const env = params.env ?? process.env;
let passphrase;
if (location.encryption !== "none") {
const { materializeSecretInput } = await import("./resolve-secret-input-string-D72TR0Ze.mjs");
passphrase = await materializeSecretInput({
config: params.config,
value: location.encryption.passphrase,
env,
normalize: (value) => typeof value === "string" && value.length > 0 ? value : void 0
});
}
if (location.encryption !== "none" && passphrase === void 0) throw new StorageLocationError("wrong-key", `Storage location "${params.name}" requires a nonempty encryption passphrase.`);
const backend = await acquired.provider.open({
locationName: params.name,
settings: location.settings,
signal: params.signal,
resolveSecret: async (value) => {
const ref = parseSecretRef(value, params.config.secrets?.defaults);
if (!ref) throw new Error("Storage provider credentials must use SecretRefs.");
const { resolveSecretRefString } = await import("./resolve-DqLr79fS.mjs");
return await resolveSecretRefString(ref, {
config: params.config,
env
});
}
});
let closed = false;
const close = async () => {
if (closed) return;
closed = true;
try {
try {
await backend.close?.();
} finally {
await acquired.release();
}
} catch (error) {
throw storageLocationError(error);
}
};
return {
backend,
passphrase,
provider: location.provider,
close
};
} catch (error) {
await acquired.release().catch((releaseError) => {
throw storageLocationError(releaseError);
});
throw storageLocationError(error);
}
}
async function makeLocation(params, prepared, marker) {
const { backend, passphrase } = prepared;
const masterKey = await verifyStorageMarker(params.name, marker, passphrase);
let closed = false;
const markerIdentity = JSON.stringify(marker);
const assertOpen = (signal) => {
signal?.throwIfAborted();
if (closed) throw new StorageLocationError("unavailable", "Storage location is closed.");
};
const check = async (signal) => {
assertOpen(signal);
const current = await readStorageMarker(backend, signal).catch((error) => {
throw storageLocationError(error);
});
if (!current) throw unavailable(params.name, backend.displayTarget);
if (JSON.stringify(current) !== markerIdentity) throw new StorageLocationError("unavailable", `Storage location "${params.name}" identity changed; reopen the location before using it.`);
assertOpen(signal);
};
const info = (object) => ({
...object,
storedBytes: object.sizeBytes,
sizeBytes: masterKey ? plaintextStorageSize(object.sizeBytes) : object.sizeBytes
});
const scoped = (namespace) => {
const keyFor = (key) => {
validateStorageKey(key);
const fullKey = namespace + key;
validateStorageKey(fullKey);
if (fullKey === "openclaw-storage.json") throw new Error("The storage marker is reserved for the storage owner.");
return fullKey;
};
const options = (opts) => ({ signal: opts?.signal ?? params.signal });
return {
describe: () => ({
name: params.name,
provider: prepared.provider,
displayTarget: backend.displayTarget,
locationId: marker.locationId,
encrypted: masterKey !== void 0
}),
probe: async (opts) => {
try {
const operation = options(opts);
await check(operation.signal);
return {
state: "ok",
...await backend.probe(operation)
};
} catch (error) {
return probeFailure(error);
}
},
putObject: async (key, body, opts) => {
const fullKey = keyFor(key);
const operation = options(opts);
if (opts.sizeBytes !== void 0) encryptedStorageSize(opts.sizeBytes);
await check(operation.signal);
let plaintextBytes = 0;
let preconditionFailure;
async function* counted() {
for await (const chunk of body) {
operation.signal?.throwIfAborted();
plaintextBytes += chunk.byteLength;
if (opts.sizeBytes !== void 0 && plaintextBytes > opts.sizeBytes) throw new Error("Storage object is larger than its declared size.");
yield chunk;
}
if (opts.sizeBytes !== void 0 && plaintextBytes !== opts.sizeBytes) throw new Error("Storage object does not match its declared size.");
await check(operation.signal);
try {
await opts.precondition?.(operation.signal);
assertOpen(operation.signal);
} catch (error) {
preconditionFailure = { error };
throw error;
}
}
const stored = await backend.putObject(fullKey, masterKey ? encryptStorageObject(counted(), masterKey) : counted(), {
...operation,
sizeBytes: opts.sizeBytes === void 0 ? void 0 : masterKey ? encryptedStorageSize(opts.sizeBytes) : opts.sizeBytes
}).catch((error) => {
if (!preconditionFailure || error === preconditionFailure.error) throw error;
throw new Error(`${formatErrorMessage(preconditionFailure.error)}\n${formatErrorMessage(error)}`, { cause: error });
});
return {
sizeBytes: plaintextBytes,
storedBytes: stored.sizeBytes
};
},
getObject: async (key, opts) => {
const fullKey = keyFor(key);
const operation = options(opts);
await check(operation.signal);
const body = await backend.getObject(fullKey, operation);
return body && masterKey ? decryptStorageObject(body, masterKey) : body;
},
stat: async (key, opts) => {
const fullKey = keyFor(key);
const operation = options(opts);
assertOpen(operation.signal);
const object = await backend.statObject(fullKey, operation);
assertOpen(operation.signal);
return object ? {
...info(object),
key
} : void 0;
},
async *list(prefix, opts) {
const requestedPrefix = prefix ?? "";
validateStoragePrefix(requestedPrefix);
const fullPrefix = namespace + requestedPrefix;
validateStoragePrefix(fullPrefix);
const operation = options(opts);
assertOpen(operation.signal);
for await (const object of backend.listObjects(fullPrefix, operation)) {
assertOpen(operation.signal);
if (!object.key.startsWith(fullPrefix)) throw new Error("Storage provider returned an object outside the requested prefix.");
try {
validateStorageKey(object.key);
} catch {
continue;
}
const key = object.key.slice(namespace.length);
if (object.key !== "openclaw-storage.json" && (!opts?.acceptKey || opts.acceptKey(key))) yield {
...info(object),
key
};
}
},
delete: async (key, opts) => {
const fullKey = keyFor(key);
const operation = options(opts);
await check(operation.signal);
await opts?.precondition?.(operation.signal);
assertOpen(operation.signal);
await backend.deleteObject(fullKey, operation);
},
scope: (prefix) => {
const next = keyFor(prefix) + "/";
validateStoragePrefix(next);
return scoped(next);
},
close: async () => {
closed = true;
await prepared.close();
}
};
};
return scoped("");
}
async function openStorageLocation(params) {
const prepared = await prepare(params);
try {
const marker = await readStorageMarker(prepared.backend, params.signal);
if (!marker) throw unavailable(params.name, prepared.backend.displayTarget);
return await makeLocation(params, prepared, marker);
} catch (error) {
await prepared.close();
throw storageLocationError(error);
}
}
async function initStorageLocation(params) {
const prepared = await prepare(params);
try {
let marker = await readStorageMarker(prepared.backend, params.signal);
if (!marker) {
const candidate = await createStorageMarker(params.name, prepared.passphrase);
const bytes = Buffer.from(JSON.stringify(candidate) + "\n");
try {
await prepared.backend.putObject(STORAGE_MARKER_KEY, (async function* () {
yield bytes;
})(), {
sizeBytes: bytes.length,
signal: params.signal
});
marker = candidate;
} catch (error) {
marker = await readStorageMarker(prepared.backend, params.signal);
if (!marker || marker.locationId === candidate.locationId) throw error;
}
}
return await makeLocation(params, prepared, marker);
} catch (error) {
await prepared.close();
throw storageLocationError(error);
}
}
async function probeStorageLocation(params) {
try {
const location = await openStorageLocation(params);
try {
return await location.probe();
} finally {
await location.close();
}
} catch (error) {
return probeFailure(error);
}
}
/** Configuration projection only; no provider activation or destination I/O. */
function listStorageLocations(config, registry) {
return Object.entries(config.storage?.locations ?? {}).toSorted(([a], [b]) => a.localeCompare(b)).map(([name, location]) => {
const displayTarget = (location.provider === "filesystem" ? filesystemStorageProvider : registry ? resolveStorageProvider(registry, location.provider) : void 0)?.describeTarget?.(location.settings);
const description = {
name,
provider: location.provider,
encrypted: location.encryption !== "none"
};
return displayTarget === void 0 ? description : Object.assign(description, { displayTarget });
});
}
//#endregion
export { storageLocationError as a, probeStorageLocation as i, listStorageLocations as n, acquireStorageProvider as o, openStorageLocation as r, initStorageLocation as t };