consortium
Version:
Remote control and session sharing CLI for AI coding agents
757 lines (742 loc) • 25.9 kB
JavaScript
;
var fs = require('node:fs');
var path = require('node:path');
var persistence = require('./types-B_i6lpTn.cjs');
var axios = require('axios');
var sodium = require('libsodium-wrappers');
var node_crypto = require('node:crypto');
var fs$1 = require('node:fs/promises');
var os = require('node:os');
function decryptBox(env, encryptedBundle, recipientSecretKey) {
const { sodium } = env;
const ephemeralPublicKey = encryptedBundle.slice(0, sodium.crypto_box_PUBLICKEYBYTES);
const nonce = encryptedBundle.slice(
sodium.crypto_box_PUBLICKEYBYTES,
sodium.crypto_box_PUBLICKEYBYTES + sodium.crypto_box_NONCEBYTES
);
const encrypted = encryptedBundle.slice(sodium.crypto_box_PUBLICKEYBYTES + sodium.crypto_box_NONCEBYTES);
try {
return sodium.crypto_box_open_easy(encrypted, nonce, ephemeralPublicKey, recipientSecretKey);
} catch {
return null;
}
}
function decodeBase64(base64, encoding = "base64") {
let normalizedBase64 = base64;
if (encoding === "base64url") {
normalizedBase64 = base64.replace(/-/g, "+").replace(/_/g, "/");
const padding = normalizedBase64.length % 4;
if (padding) {
normalizedBase64 += "=".repeat(4 - padding);
}
}
const binaryString = atob(normalizedBase64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
function decryptDriveDek(env, encrypted, privateKey) {
if (encrypted[0] !== 0) return null;
return decryptBox(env, encrypted.slice(1), privateKey);
}
function decryptDriveContent(env, encrypted, dek) {
const { sodium } = env;
const nonce = encrypted.slice(0, sodium.crypto_secretbox_NONCEBYTES);
const ciphertext = encrypted.slice(sodium.crypto_secretbox_NONCEBYTES);
try {
return sodium.crypto_secretbox_open_easy(ciphertext, nonce, dek);
} catch {
return null;
}
}
function decryptDriveMetadata(env, encrypted, dek) {
try {
const data = decodeBase64(encrypted, "base64");
const decrypted = decryptDriveContent(env, data, dek);
if (!decrypted) return null;
return JSON.parse(new TextDecoder().decode(decrypted));
} catch {
return null;
}
}
function sniffMimeFromBytes(buf) {
if (buf.length < 4) return void 0;
const b = buf instanceof Buffer ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
if (b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) return "image/png";
if (b[0] === 255 && b[1] === 216 && b[2] === 255) return "image/jpeg";
if (b[0] === 71 && b[1] === 73 && b[2] === 70 && b[3] === 56) return "image/gif";
if (b[0] === 37 && b[1] === 80 && b[2] === 68 && b[3] === 70) return "application/pdf";
if (b.length >= 12 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) return "image/webp";
if (b.length >= 12 && b[4] === 102 && b[5] === 116 && b[6] === 121 && b[7] === 112) {
if (b[8] === 113 && b[9] === 116 && b[10] === 32) return "video/quicktime";
return "video/mp4";
}
if (b[0] === 26 && b[1] === 69 && b[2] === 223 && b[3] === 163) return "video/webm";
if (b[0] === 73 && b[1] === 68 && b[2] === 51) return "audio/mpeg";
if (b[0] === 255 && (b[1] & 224) === 224) return "audio/mpeg";
if (b.length >= 12 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 65 && b[10] === 86 && b[11] === 69) return "audio/wav";
if (b[0] === 79 && b[1] === 103 && b[2] === 103 && b[3] === 83) return "audio/ogg";
if (b[0] === 102 && b[1] === 76 && b[2] === 97 && b[3] === 67) return "audio/flac";
return void 0;
}
const MIME_BY_EXT = {
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
avif: "image/avif",
heic: "image/heic",
svg: "image/svg+xml",
mp4: "video/mp4",
mov: "video/quicktime",
webm: "video/webm",
mkv: "video/x-matroska",
mp3: "audio/mpeg",
wav: "audio/wav",
ogg: "audio/ogg",
m4a: "audio/mp4",
flac: "audio/flac",
pdf: "application/pdf"
};
function mimeFromExtension(path) {
const i = path.lastIndexOf(".");
if (i < 0) return void 0;
const ext = path.slice(i + 1).toLowerCase();
return MIME_BY_EXT[ext];
}
const DEFAULTS = {
maxBytes: 64 * 1024 * 1024,
maxDepth: 6,
maxCandidates: 8,
maxFileAgeMs: 30 * 60 * 1e3
// 30 min — older than a turn, probably stale
};
const BASE64ISH = /^[A-Za-z0-9+/=_-]{800,}$/;
function looksLikePath(s) {
return s.length > 1 && s.length < 4096 && path.isAbsolute(s) && !s.includes("\n");
}
function looksLikeUrl(s) {
return /^https?:\/\/[^\s]+$/i.test(s) && s.length < 2048;
}
function extractPathsFromProse(text) {
if (!text || text.length > 1e5) return [];
const out = [];
const re = /\/[^\s'"`)\]]+\.(?:png|jpe?g|gif|webp|svg|avif|heic|mp4|mov|webm|mkv|mp3|wav|ogg|m4a|flac|pdf)\b/gi;
let m;
while ((m = re.exec(text)) !== null) {
out.push(m[0]);
if (out.length >= 4) break;
}
return out;
}
async function scanToolResultForMedia(content, onMedia, options = {}) {
const opts = { ...DEFAULTS, ...options };
const seenBytes = /* @__PURE__ */ new Set();
let emitted = 0;
const tryEmit = async (cand) => {
if (emitted >= opts.maxCandidates) return;
if (cand.bytes.length === 0 || cand.bytes.length > opts.maxBytes) return;
const key = `${cand.mime}|${cand.bytes.length}`;
if (seenBytes.has(key)) return;
seenBytes.add(key);
emitted++;
try {
await onMedia(cand);
} catch (err) {
persistence.logger.debug("[mediaScan] onMedia callback threw:", err);
}
};
const tryEmitFromPath = async (path, name) => {
try {
const stat = await fs.promises.stat(path);
if (!stat.isFile()) return;
if (stat.size === 0 || stat.size > opts.maxBytes) return;
if (Date.now() - stat.mtimeMs > opts.maxFileAgeMs) return;
const extMime = mimeFromExtension(path);
if (!extMime) return;
const buf = await fs.promises.readFile(path);
const sniffed = sniffMimeFromBytes(buf) ?? extMime;
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
await tryEmit({ bytes, mime: sniffed, name: name ?? basenameOf(path) });
} catch (err) {
persistence.logger.debug(`[mediaScan] path read failed (${path}):`, err instanceof Error ? err.message : err);
}
};
const tryEmitFromUrl = async (url, name) => {
try {
const res = await fetch(url, { method: "GET" });
if (!res.ok) return;
const ct = res.headers.get("content-type") ?? "";
const len = Number(res.headers.get("content-length") ?? "0");
if (len > opts.maxBytes) return;
const ab = await res.arrayBuffer();
const buf = Buffer.from(ab);
if (buf.length === 0 || buf.length > opts.maxBytes) return;
const sniffed = sniffMimeFromBytes(buf);
const isMediaCt = ct.startsWith("image/") || ct.startsWith("video/") || ct.startsWith("audio/") || ct === "application/pdf";
if (!sniffed && !isMediaCt) return;
const mime = sniffed ?? ct.split(";")[0].trim();
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
await tryEmit({ bytes, mime, name: name ?? url.split("/").pop() });
} catch (err) {
persistence.logger.debug(`[mediaScan] url fetch failed (${url}):`, err instanceof Error ? err.message : err);
}
};
const walk = async (node, depth, hintName) => {
if (emitted >= opts.maxCandidates) return;
if (node === null || node === void 0 || depth > opts.maxDepth) return;
if (typeof node === "string") {
const s = node;
if (looksLikePath(s)) {
await tryEmitFromPath(s, hintName);
return;
}
if (looksLikeUrl(s)) {
await tryEmitFromUrl(s, hintName);
return;
}
if (BASE64ISH.test(s)) {
try {
const buf = Buffer.from(s, "base64");
if (buf.length > 0) {
const sniffed = sniffMimeFromBytes(buf);
if (sniffed) {
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
await tryEmit({ bytes, mime: sniffed, name: hintName });
}
}
} catch {
}
return;
}
for (const p of extractPathsFromProse(s)) {
await tryEmitFromPath(p);
if (emitted >= opts.maxCandidates) return;
}
return;
}
if (Array.isArray(node)) {
for (const item of node) {
await walk(item, depth + 1);
if (emitted >= opts.maxCandidates) return;
}
return;
}
if (typeof node === "object") {
const o = node;
if (typeof o.type === "string") {
if (o.type === "image" && typeof o.data === "string") {
const mime = (typeof o.mimeType === "string" ? o.mimeType : void 0) ?? "image/png";
try {
const buf = Buffer.from(o.data, "base64");
if (buf.length > 0) {
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
await tryEmit({ bytes, mime, name: typeof o.name === "string" ? o.name : void 0 });
}
} catch {
}
return;
}
if (o.type === "audio" && typeof o.data === "string") {
const mime = (typeof o.mimeType === "string" ? o.mimeType : void 0) ?? "audio/mpeg";
try {
const buf = Buffer.from(o.data, "base64");
if (buf.length > 0) {
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
await tryEmit({ bytes, mime, name: typeof o.name === "string" ? o.name : void 0 });
}
} catch {
}
return;
}
if (o.type === "resource" && o.resource && typeof o.resource === "object") {
const r = o.resource;
if (typeof r.blob === "string") {
const mime = (typeof r.mimeType === "string" ? r.mimeType : void 0) ?? "application/octet-stream";
try {
const buf = Buffer.from(r.blob, "base64");
if (buf.length > 0) {
const sniffed = sniffMimeFromBytes(buf) ?? mime;
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
await tryEmit({ bytes, mime: sniffed, name: typeof r.uri === "string" ? basenameOf(r.uri) : void 0 });
}
} catch {
}
return;
}
if (typeof r.uri === "string") {
const uri = r.uri;
if (uri.startsWith("file://")) {
await tryEmitFromPath(uri.slice(7));
return;
}
}
}
}
for (const [k, v] of Object.entries(o)) {
const lk = k.toLowerCase();
const hint = (lk.includes("name") || lk.includes("filename")) && typeof o.name === "string" ? o.name : void 0;
await walk(v, depth + 1, hint);
if (emitted >= opts.maxCandidates) return;
}
}
};
await walk(content, 0);
}
function basenameOf(p) {
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
return i >= 0 ? p.slice(i + 1) : p;
}
let ENV = null;
let READY = null;
async function getCryptoEnv() {
if (ENV) return ENV;
if (!READY) {
READY = (async () => {
await sodium.ready;
const env = {
// libsodium-wrappers exposes the required surface.
// Cast: the .d.ts types are broader than SodiumLike but
// structurally compatible.
sodium,
getRandomBytes: (size) => new Uint8Array(node_crypto.randomBytes(size))
};
ENV = env;
return env;
})();
}
return READY;
}
class DriveAccessError extends Error {
constructor(message, status) {
super(message);
this.status = status;
this.name = "DriveAccessError";
}
}
class DriveDecryptError extends Error {
constructor(message, driveId, nodeId) {
super(message);
this.driveId = driveId;
this.nodeId = nodeId;
this.name = "DriveDecryptError";
}
}
function isEncryptedWrapper(parsed) {
return typeof parsed === "object" && parsed !== null && parsed.v === 1 && typeof parsed.enc === "string";
}
class DriveClient {
constructor(token, user, baseUrl = persistence.configuration.serverUrl) {
this.user = user;
this.http = axios.create({
baseURL: baseUrl,
headers: { Authorization: `Bearer ${token}` }
});
}
http;
dekCache = /* @__PURE__ */ new Map();
async getDriveInfo(driveId) {
try {
const { data } = await this.http.get(`/v1/drives/${driveId}`);
return data;
} catch (err) {
throw this.wrapAccessError(err, `getDriveInfo ${driveId}`);
}
}
async getNode(driveId, nodeId) {
try {
const { data } = await this.http.get(
`/v1/drives/${driveId}/nodes/${nodeId}`
);
return data;
} catch (err) {
throw this.wrapAccessError(err, `getNode ${driveId}/${nodeId}`);
}
}
async getDownloadUrl(driveId, nodeId) {
try {
const { data } = await this.http.get(
`/v1/drives/${driveId}/nodes/${nodeId}/download-url`
);
return data.downloadUrl;
} catch (err) {
throw this.wrapAccessError(err, `getDownloadUrl ${driveId}/${nodeId}`);
}
}
async listSessionAttachments(sessionId, messageLocalId) {
try {
const { data } = await this.http.get(
`/v1/sessions/${sessionId}/attachments`,
{ params: messageLocalId ? { messageLocalId } : void 0 }
);
return data.attachments;
} catch (err) {
throw this.wrapAccessError(err, `listSessionAttachments ${sessionId}`);
}
}
/**
* Fetch the node, download + decrypt its blob (if any), and decrypt
* its metadata. Nodes without a storage blob return empty `bytes`.
*
* If decryption fails (metadata or content) with the cached DEK,
* refresh the DEK once and retry; surface DriveDecryptError on a
* second failure.
*/
async fetchAndDecrypt(driveId, nodeId) {
const env = await getCryptoEnv();
const node = await this.getNode(driveId, nodeId);
let parsed;
try {
parsed = JSON.parse(node.encryptedMetadata);
} catch {
parsed = void 0;
}
const downloadBlob = async () => {
if (!node.storageBlobKey) return new Uint8Array(0);
const downloadUrl = await this.getDownloadUrl(driveId, nodeId);
try {
const { data } = await axios.get(downloadUrl, {
responseType: "arraybuffer"
});
return new Uint8Array(data);
} catch (err) {
throw this.wrapAccessError(err, `download ${driveId}/${nodeId}`);
}
};
if (parsed !== void 0 && !isEncryptedWrapper(parsed)) {
const bytes = await downloadBlob();
return { bytes: bytes ?? new Uint8Array(0), metadata: parsed, node };
}
const encMetadata = isEncryptedWrapper(parsed) ? parsed.enc : node.encryptedMetadata;
const attempt = async (dek2) => {
const metadata = decryptDriveMetadata(env, encMetadata, dek2);
if (metadata === null) return null;
if (!node.storageBlobKey) {
return { bytes: new Uint8Array(0), metadata, node };
}
const ciphertext = await downloadBlob();
if (!ciphertext) return null;
const bytes = decryptDriveContent(env, ciphertext, dek2);
if (!bytes) return null;
return { bytes, metadata, node };
};
let dek = await this.getDek(driveId, env);
const first = await attempt(dek);
if (first) return first;
persistence.logger.debug(`[DriveClient] decrypt failed, refreshing DEK for drive ${driveId}`);
this.dekCache.delete(driveId);
dek = await this.getDek(driveId, env);
const second = await attempt(dek);
if (second) return second;
throw new DriveDecryptError(
`Failed to decrypt drive node ${nodeId} in drive ${driveId} after DEK refresh`,
driveId,
nodeId
);
}
/**
* Seed the in-memory DEK cache with a key the caller obtained out
* of band (e.g. from the user-message envelope). The daemon's
* machineKey is a different x25519 keypair from the user's content
* key, so without this seed `getDek()` cannot unwrap the stored
* `encryptedDek` and every `fetchAndDecrypt` returns empty bytes.
*/
setKnownDek(driveId, dek) {
this.dekCache.set(driveId, dek);
}
async getDek(driveId, env) {
const cached = this.dekCache.get(driveId);
if (cached) return cached;
const info = await this.getDriveInfo(driveId);
const wrapped = decodeBase64(info.encryptedDek, "base64");
const dek = decryptDriveDek(env, wrapped, this.user.x25519SecretKey);
if (!dek) {
throw new DriveDecryptError(
`Failed to unwrap DEK for drive ${driveId}`,
driveId
);
}
this.dekCache.set(driveId, dek);
return dek;
}
wrapAccessError(err, context) {
if (axios.isAxiosError(err)) {
const status = err.response?.status;
return new DriveAccessError(
`${context}: HTTP ${status ?? "network"} ${err.message}`,
status
);
}
return err instanceof Error ? err : new Error(String(err));
}
}
class AttachmentError extends Error {
code;
attachmentId;
driveNodeId;
constructor(code, message, opts = {}) {
super(message);
this.name = "AttachmentError";
this.code = code;
this.attachmentId = opts.attachmentId;
this.driveNodeId = opts.driveNodeId;
if (opts.cause !== void 0) {
this.cause = opts.cause;
}
}
}
function pickName(metadata, fallback) {
if (metadata && typeof metadata === "object" && metadata !== null) {
const m = metadata;
if (typeof m.name === "string" && m.name.length > 0) return m.name;
if (typeof m.filename === "string" && m.filename.length > 0) return m.filename;
}
return fallback;
}
function pickMime(metadata) {
if (metadata && typeof metadata === "object" && metadata !== null) {
const m = metadata;
if (typeof m.mimeType === "string") return m.mimeType;
if (typeof m.mime === "string") return m.mime;
if (typeof m.contentType === "string") return m.contentType;
}
return void 0;
}
function classifyError(err) {
if (err instanceof AttachmentError) return err.code;
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
if (msg.includes("dek") || msg.includes("key")) return "dek-missing";
if (msg.includes("decrypt")) return "decrypt-failed";
if (msg.includes("not found") || msg.includes("404")) return "not-found";
if (msg.includes("quota") || msg.includes("storage full")) return "quota-exceeded";
return "drive-fetch-failed";
}
function failed(code, base, err) {
const message = err instanceof Error ? err.message : err === void 0 ? void 0 : String(err);
return {
...base,
status: { code, message }
};
}
class AttachmentResolver {
constructor(drive, scratch) {
this.drive = drive;
this.scratch = scratch;
}
/**
* Resolve every binding currently attached to `messageLocalId` (or
* every binding for the session if `messageLocalId` is omitted).
* Always returns a result per binding — failures are reported via
* `status`, never silently dropped.
*/
async resolveForMessage(sessionId, messageLocalId) {
let bindings;
try {
bindings = await this.drive.listSessionAttachments(sessionId, messageLocalId);
} catch (err) {
persistence.logger.debug("[AttachmentResolver] listSessionAttachments failed:", err);
return [];
}
const out = [];
for (const b of bindings) {
try {
out.push(await this.resolveBinding(b));
} catch (err) {
const code = classifyError(err);
const msg = err instanceof Error ? err.message : String(err);
persistence.logger.debug(
`[AttachmentResolver] resolve failed binding=${b.id} node=${b.driveNodeId} kind=${b.kind} driveId=${b.driveId ?? "null"} code=${code}: ${msg}`
);
out.push(failed(code, {
id: b.id,
driveNodeId: b.driveNodeId,
kind: b.kind,
name: b.linkUri ?? b.driveNodeId,
uri: b.linkUri ?? void 0
}, err));
}
}
return out;
}
/**
* Resolve by raw drive node ids when the caller doesn't have a
* binding row (e.g. ad-hoc references encoded directly in the
* message envelope's `driveNodeIds`).
*/
async resolveByDriveNodeIds(driveId, nodeIds) {
const out = [];
for (const nodeId of nodeIds) {
try {
const fetched = await this.drive.fetchAndDecrypt(driveId, nodeId);
if (!fetched.bytes || fetched.bytes.length === 0) {
out.push(failed("dek-missing", {
id: nodeId,
driveNodeId: nodeId,
kind: "drive_file",
name: pickName(fetched.metadata, nodeId),
mime: pickMime(fetched.metadata)
}));
continue;
}
out.push({
id: nodeId,
driveNodeId: nodeId,
kind: "drive_file",
name: pickName(fetched.metadata, nodeId),
mime: pickMime(fetched.metadata),
bytes: fetched.bytes,
status: { code: "ok" }
});
} catch (err) {
const code = classifyError(err);
const msg = err instanceof Error ? err.message : String(err);
persistence.logger.debug(
`[AttachmentResolver] resolveByDriveNodeIds failed driveId=${driveId} node=${nodeId} code=${code}: ${msg}`
);
out.push(failed(code, {
id: nodeId,
driveNodeId: nodeId,
kind: "drive_file",
name: nodeId
}, err));
}
}
return out;
}
async resolveBinding(b) {
if (b.kind === "link" || b.kind === "github_repo") {
return {
id: b.id,
driveNodeId: b.driveNodeId,
kind: b.kind,
name: b.linkUri ?? b.driveNodeId,
uri: b.linkUri ?? void 0,
status: { code: "ok" }
};
}
if (!b.driveId) {
return failed("not-found", {
id: b.id,
driveNodeId: b.driveNodeId,
kind: b.kind,
name: `drive-node:${b.driveNodeId}`
});
}
const fetched = await this.drive.fetchAndDecrypt(b.driveId, b.driveNodeId);
if (!fetched.bytes || fetched.bytes.length === 0) {
return failed("dek-missing", {
id: b.id,
driveNodeId: b.driveNodeId,
kind: b.kind,
name: pickName(fetched.metadata, b.driveNodeId),
mime: pickMime(fetched.metadata)
});
}
return {
id: b.id,
driveNodeId: b.driveNodeId,
kind: b.kind,
name: pickName(fetched.metadata, b.driveNodeId),
mime: pickMime(fetched.metadata),
bytes: fetched.bytes,
status: { code: "ok" }
};
}
}
const ALL_DIRS = /* @__PURE__ */ new Set();
let HOOKS_REGISTERED = false;
function registerProcessHooks() {
if (HOOKS_REGISTERED) return;
HOOKS_REGISTERED = true;
const cleanupAll = () => {
for (const dir of ALL_DIRS) {
try {
require("node:fs").rmSync(dir, { recursive: true, force: true });
} catch {
}
}
ALL_DIRS.clear();
};
process.on("exit", cleanupAll);
process.on("SIGINT", () => {
cleanupAll();
});
process.on("SIGTERM", () => {
cleanupAll();
});
}
function sanitizeName(name) {
return name.replace(/[\\/\0]/g, "_").replace(/\s+/g, "_").slice(0, 128) || "file";
}
class ScratchDir {
constructor(sessionId, opts = {}) {
this.sessionId = sessionId;
const root = opts.workspaceDir ? path.join(opts.workspaceDir, ".consortium", "scratch") : path.join(os.tmpdir(), "consortium", "sessions");
this.base = path.join(root, sessionId, "attachments");
registerProcessHooks();
}
base;
ensured = false;
async ensure() {
if (this.ensured) return;
await fs$1.mkdir(this.base, { recursive: true, mode: 448 });
ALL_DIRS.add(this.base);
this.ensured = true;
}
async write(name, bytes) {
await this.ensure();
const safe = sanitizeName(name);
const prefix = node_crypto.randomBytes(6).toString("hex");
const filePath = path.join(this.base, `${prefix}-${safe}`);
await fs$1.writeFile(filePath, bytes, { mode: 384 });
return filePath;
}
async cleanup() {
if (!this.ensured) return;
try {
await fs$1.rm(this.base, { recursive: true, force: true });
ALL_DIRS.delete(this.base);
} catch (err) {
persistence.logger.debug("[ScratchDir] cleanup failed:", err);
}
}
}
function pickStringArray(v) {
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
}
function pickString(v) {
return typeof v === "string" && v.length > 0 ? v : null;
}
function normalizeAttachmentEnvelope(message) {
const m = message ?? {};
const c = m.content ?? {};
const driveId = pickString(c.driveId);
const driveDek = pickString(c.driveDek) ?? pickString(m.driveDek);
const messageLocalId = pickString(c.localId) ?? pickString(m.localId);
const inlineIds = pickStringArray(c.driveNodeIds);
const topLevelIds = pickStringArray(m.attachmentIds);
const rawIds = inlineIds.length > 0 ? inlineIds : topLevelIds;
const driveNodeIds = driveId ? rawIds : [];
const bindingOnlyNodeIds = driveId ? [] : rawIds;
return {
messageLocalId,
driveId,
driveNodeIds,
driveDek,
bindingOnlyNodeIds
};
}
function attachmentsKillSwitchOff() {
const v = (process.env.CONSORTIUM_ATTACHMENTS_V1 ?? "").toLowerCase();
return v === "0" || v === "false" || v === "off" || v === "no";
}
exports.AttachmentResolver = AttachmentResolver;
exports.DriveClient = DriveClient;
exports.ScratchDir = ScratchDir;
exports.attachmentsKillSwitchOff = attachmentsKillSwitchOff;
exports.normalizeAttachmentEnvelope = normalizeAttachmentEnvelope;
exports.scanToolResultForMedia = scanToolResultForMedia;