openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
308 lines (307 loc) • 22.2 kB
JavaScript
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { b as redactToolPayloadText } from "./redact-BV3wTtTv.mjs";
import { r as assertExistingDatabaseIdentity } from "./sqlite-worker-identity-BRNRpRyc.mjs";
import { r as isIncognitoSessionKey } from "./session-key-CmZ4o9-l.mjs";
import "./session-key-DY3MOX4N.mjs";
import { h as parseAgentSessionKey } from "./session-key-utils-CDyBC9Gn.mjs";
import { i as listAgentIds } from "./agent-roster-DH6r_8f4.mjs";
import "./agent-scope-config-BEk6-ThS.mjs";
import { n as resolvePersistedSessionStoreOwnerForKey } from "./session-store-owner-DZX-SiyK.mjs";
import { t as escapeHtml } from "./html-escape-BMD_QFeA.mjs";
import { n as HEARTBEAT_PROMPT } from "./heartbeat-nZ1C5oCa.mjs";
import { f as resolveSessionPublicShare } from "./session-accessor.sqlite-entry-store-CIM2yoGL.mjs";
import "./input-provenance-B5YZS2Do.mjs";
import { t as extractAssistantPhaseText } from "./chat-message-content-C-d0FKNG.mjs";
import { t as stripInternalMetadataForDisplay } from "./display-text-sanitize-BuJdluwh.mjs";
import { l as sanitizeAssistantVisibleTextWithProfile } from "./assistant-visible-text-CWq-s28E.mjs";
import { n as isHeartbeatOkResponse, r as isHeartbeatUserMessage } from "./heartbeat-filter-DruZVPZx.mjs";
import { t as MarkdownItCallable } from "./markdown-it-CTBIQGiA.mjs";
import { n as splitMediaOutput } from "./parse-output-BEoO0_ns.mjs";
import { i as stripSuppressedControlReplyToken } from "./control-reply-text-CdlY-Sik.mjs";
import { t as stripUserEnvelopeForDisplay } from "./user-envelope-display-c4XHAD7a.mjs";
import { u as readSessionMessagesPageWithStatsAsync } from "./session-transcript-readers-Bka5K3vB.mjs";
import { i as withReadySessionRows } from "./session-row-prepared-read-BEDhe5gP.mjs";
//#endregion
//#region src/gateway/control-ui-public-session-render.ts
/** Public-reader lifecycle only; never starts the operator app, socket, or roster. */
const PUBLIC_SESSION_ENTRY_SCRIPT = `(()=>{
const link=document.getElementById("session-login");
function hasClientCredential(){
if(link?.dataset.gatewayPath===undefined)return false;
const scope=(location.protocol==="https:"?"wss:":"ws:")+"//"+location.host+link.dataset.gatewayPath;
let credential=new URLSearchParams(location.hash.slice(1)).get("token")?.trim();
try{credential ||= sessionStorage.getItem(${JSON.stringify("openclaw.control.token.v1:")}+scope)?.trim()}catch{}
// Device scopes retain Gateway query strings; either form is only a navigation hint.
try{
const prefix=${JSON.stringify(`openclaw.device.auth.v1:`)}+scope;
for(let i=0;!credential&&i<localStorage.length;i++){
const key=localStorage.key(i);
if(key===prefix||key?.startsWith(prefix+"?"))try{credential=JSON.parse(localStorage.getItem(key)||"null")?.tokens?.operator?.token?.trim()}catch{}
}
}catch{}
return Boolean(credential)
}
const pending=document.querySelector("main[data-entry-pending]");
function showUnavailable(main){
if(!main?.hasAttribute("data-entry-pending"))return;
main.removeAttribute("data-entry-pending");
document.title=main.querySelector(".entry-result h1").textContent+" · OpenClaw";
}
function probeFailed(){
if(!pending)return;
if(link)link.style.visibility="visible";
pending.querySelector(".entry-status h1").textContent="Could not check access";
pending.querySelector(".entry-status p").textContent="Check your connection, then reload or log in to try again.";
}
if(link)fetch(link.href+"&probe=1",{credentials:"same-origin",redirect:"error",cache:"no-store",signal:AbortSignal.timeout(5000)}).then(response=>{
if(response.status===204||(response.status===401&&hasClientCredential()))location.replace(link.href+location.hash);
else if(response.status===401||response.status===403)showUnavailable(pending);
else probeFailed();
}).catch(probeFailed);
document.addEventListener("click",event=>{const login=event.target.closest?.("#session-login");if(login)login.hash=location.hash});
let timer,etag="",running=false,retryAt=0;
const enabled=()=>document.querySelector("main[data-public-refresh='true']")!==null;
const jitter=()=>Math.floor(Math.random()*3000);
const schedule=(delay=15000+jitter())=>{clearTimeout(timer);if(!document.hidden&&enabled())timer=setTimeout(refresh,Math.max(delay,retryAt-Date.now()))};
async function refresh(){
if(document.hidden||!enabled()||running)return;
running=true;
let delay;
try{
const response=await fetch(location.href,{credentials:"same-origin",redirect:"error",cache:"no-store",headers:etag?{"If-None-Match":etag}:{},signal:AbortSignal.timeout(10000)});
if(response.status===429||response.status===503){const seconds=Number(response.headers.get("Retry-After"));if(Number.isFinite(seconds)&&seconds>0){delay=Math.max(15000,seconds*1000)+jitter();retryAt=Date.now()+delay}return}
if(response.status===304||document.hidden)return;
if(response.status!==200&&response.status!==404)return;
const next=new DOMParser().parseFromString(await response.text(),"text/html");
const main=next.querySelector("main[data-public-session]");
const current=document.querySelector("main[data-public-session]");
if(document.hidden||!main||!current)return;
current.replaceWith(main);
document.title=next.title;
// A revoked public read is already resolved; its inert script will not run here.
showUnavailable(main);
etag=response.headers.get("ETag")||"";
}catch{}finally{running=false;schedule(delay)}
}
document.addEventListener("visibilitychange",()=>{if(document.hidden)clearTimeout(timer);else schedule(0)});
schedule();
})();`;
const MAX_MESSAGES = 100;
const MAX_MESSAGE_CHARS = 32768;
const MAX_DOCUMENT_CHARS = 262144;
const markdown = new MarkdownItCallable({
html: false,
linkify: false,
breaks: true
});
markdown.validateLink = (value) => {
const url = URL.parse(value);
return Boolean(url && (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password);
};
markdown.renderer.rules.image = () => "<span class=\"omitted\">[Image omitted]</span>";
markdown.renderer.rules.link_open = (tokens, index, options, _env, renderer) => {
tokens[index]?.attrSet("rel", "noreferrer noopener nofollow");
return renderer.renderToken(tokens, index, options);
};
function publicMessageText(message) {
const entry = asOptionalRecord(message);
if (!entry || entry.role !== "user" && entry.role !== "assistant" || entry.display === false || entry.customType !== void 0 || entry.senderSession !== void 0 || entry.toolCallId !== void 0 || entry.tool_call_id !== void 0) return;
const provenance = asOptionalRecord(entry.provenance);
if (entry.provenance !== void 0 && provenance?.kind !== "external_user") return;
let text;
if (entry.role === "assistant") {
if (entry.phase !== void 0 && entry.phase !== "final_answer") return;
text = extractAssistantPhaseText(entry);
} else if (typeof entry.content === "string") text = entry.content;
else if (Array.isArray(entry.content)) text = entry.content.flatMap((value) => {
const block = asOptionalRecord(value);
return (block?.type === "text" || block?.type === "input_text") && typeof block.text === "string" ? [block.text] : [];
}).join("\n\n");
else if (typeof entry.text === "string") text = entry.text;
if (!text || text.includes("[Inter-session message]")) return;
text = entry.role === "user" ? stripUserEnvelopeForDisplay(text) : stripInternalMetadataForDisplay(text);
if (entry.role === "assistant") text = sanitizeAssistantVisibleTextWithProfile(text, "history", true);
const roleContent = {
role: entry.role,
content: text
};
if (isHeartbeatUserMessage(roleContent, HEARTBEAT_PROMPT) || isHeartbeatOkResponse(roleContent)) return;
if (entry.role === "assistant") text = stripSuppressedControlReplyToken(text);
text = splitMediaOutput(text, { extractAudioDirectives: false }).text;
text = redactToolPayloadText(text).trim();
return text ? {
role: entry.role,
text
} : void 0;
}
function renderPublicSessionDocument(params) {
const isLatest = params.isLatest !== false && !params.unavailable;
const entryPending = params.unavailable && Boolean(params.entryUrl);
const entryLink = params.entryUrl ? `<a class="login" id="session-login"${params.clientAuthBasePath !== void 0 ? ` data-gateway-path="${escapeHtml(params.clientAuthBasePath)}"` : ""} href="${escapeHtml(params.entryUrl)}">Log in <span aria-hidden="true">→</span></a>` : "";
const title = escapeHtml(truncateUtf16Safe(redactToolPayloadText(stripInternalMetadataForDisplay(params.title)), 200).trim() || "Shared conversation");
let truncated = params.truncated || params.messages.length > MAX_MESSAGES;
let remaining = MAX_DOCUMENT_CHARS;
const rows = [];
for (const value of params.messages.slice(-100).toReversed()) {
const message = publicMessageText(value);
if (!message) continue;
if (remaining <= 0) {
truncated = true;
break;
}
const text = truncateUtf16Safe(message.text, Math.min(MAX_MESSAGE_CHARS, remaining));
const clipped = text.length < message.text.length;
truncated ||= clipped;
remaining -= text.length;
rows.push(`<article class="message ${message.role}" aria-label="${message.role === "user" ? "User" : "Assistant"} message"><h2>${message.role === "user" ? "User" : "OpenClaw"}</h2><div class="content">${markdown.render(text)}${clipped ? "<p class=\"omitted\">Message shortened for this public view.</p>" : ""}</div></article>`);
}
const description = params.unavailable ? "Log in to open conversations you have access to." : "A public, read-only OpenClaw conversation. No login required.";
const canonicalMetadata = params.canonicalUrl ? `<link rel="canonical" href="${escapeHtml(params.canonicalUrl)}">
<meta property="og:url" content="${escapeHtml(params.canonicalUrl)}">` : "";
const navigation = params.olderUrl ? `<nav class="pagination" aria-label="Conversation pages"><a href="${escapeHtml(params.olderUrl)}" rel="prev">← Older messages</a></nav>` : "";
return `<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="no-referrer"><meta name="robots" content="noindex, nofollow">
${isLatest && !params.entryUrl ? "<meta http-equiv=\"refresh\" content=\"15\">" : ""}
<title>${entryPending ? "OpenClaw" : `${title} · OpenClaw`}</title>${canonicalMetadata}
<meta property="og:type" content="website"><meta property="og:site_name" content="OpenClaw">
<meta property="og:title" content="${title}"><meta property="og:description" content="${description}">
<meta property="og:image" content="${escapeHtml(params.cardUrl)}">
<meta name="twitter:card" content="summary_large_image">
<style>
:root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#0b1016;color:#edf1f5;font-synthesis:none}
*{box-sizing:border-box}body{margin:0;border-top:3px solid #ff6257}a{color:#91c9ff;text-underline-offset:3px}a:focus-visible{outline:2px solid #ff8b81;outline-offset:4px}
main{width:min(820px,calc(100% - 40px));margin:0 auto;padding:44px 0 60px}.brand{font-weight:750;letter-spacing:-.03em;font-size:19px;color:#edf1f5}.brand span{color:#ff786b;margin-right:9px}.login{display:inline-flex;align-items:center;gap:14px;border:1px solid #42515f;border-radius:8px;padding:9px 14px;color:#edf1f5;text-decoration:none;font-size:13px;font-weight:650;white-space:nowrap}.login:hover{background:#1a2631}.header-actions{display:flex;align-items:center;gap:14px}.masthead{display:flex;align-items:center;justify-content:space-between;gap:16px}.badge{border:1px solid #345044;background:#14281f;border-radius:99px;padding:6px 11px;color:#a3ddba;font-size:12px;font-weight:650;white-space:nowrap}.badge::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:#91d6ab;margin-right:7px}
h1{font-size:clamp(28px,5vw,42px);line-height:1.15;letter-spacing:-.045em;margin:40px 0 16px;overflow-wrap:anywhere}.intro{color:#a6b3c0;font-size:14px;line-height:1.7;margin:0 0 30px;max-width:660px}.intro strong{color:#d6dee6;font-weight:550}.notice{padding:14px 18px;border:1px solid #554735;background:#261f16;color:#d7c4a7;font-size:13px;line-height:1.6;border-radius:10px;margin-bottom:28px}
.pagination{display:flex;justify-content:space-between;gap:18px;margin:24px 0;font-size:13px}.pagination a{padding:9px 0}.transcript{border-top:1px solid #26313b}.message{padding:27px 0;display:grid;grid-template-columns:90px minmax(0,1fr);gap:18px;border-bottom:1px solid #202b35}.message h2{margin:3px 0 0;font-size:12px;letter-spacing:.02em;font-weight:650;color:#99a9ba}.assistant h2{color:#ff958b}.content{min-width:0;font-size:15px;line-height:1.75;overflow-wrap:anywhere}.content>:first-child{margin-top:0}.content>:last-child{margin-bottom:0}.content p{margin:0 0 16px}.content h1,.content h2,.content h3,.content h4{color:#edf1f5;font-size:18px;line-height:1.4;letter-spacing:-.02em;margin:24px 0 12px}.content li{padding-left:3px;margin:5px 0}.content ul,.content ol{padding-left:24px}.content blockquote{margin:20px 0;border-left:3px solid #465b6d;padding:0 18px;color:#b1bfcb}.content pre{overflow:auto;max-width:100%;padding:16px 18px;border:1px solid #283541;border-radius:10px;background:#080d12;font-size:12px;line-height:1.7}.content code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.88em}.content :not(pre)>code{background:#1a2631;padding:2px 5px;border-radius:4px}.content table{display:block;max-width:100%;overflow:auto;border-collapse:collapse;font-size:13px}.content th,.content td{border:1px solid #34414d;text-align:left;padding:8px 12px}.content hr{border:0;border-top:1px solid #34414d;margin:24px 0}.omitted,.empty{color:#8c9dad;font-size:13px}.empty{padding:36px 0;line-height:1.7}footer{display:flex;justify-content:space-between;gap:20px;color:#80909f;font-size:12px;line-height:1.6;margin-top:30px}footer a{color:#a6b3c0}
.entry-status{display:none}[data-entry-pending] .entry-result{display:none}[data-entry-pending] .entry-status{display:block}[data-entry-pending] .login{visibility:hidden}
{.masthead{flex-wrap:wrap}.header-actions{flex-wrap:wrap}main{width:calc(100% - 32px);padding-top:26px}.message{display:block;padding:23px 0}.message h2{margin:0 0 10px}.content h2{margin:24px 0 12px}.badge{font-size:11px}h1{margin-top:32px}footer{display:block}footer a{display:inline-block;margin-top:10px}}
</style>${entryPending ? "<noscript><style>[data-entry-pending] .entry-result{display:block}[data-entry-pending] .entry-status{display:none}[data-entry-pending] .login{visibility:visible}</style></noscript>" : ""}</head><body><main data-public-session${entryPending ? " data-entry-pending" : ""} data-public-refresh="${params.entryUrl && isLatest ? "true" : "false"}">
<header><div class="masthead"><div class="brand"><span aria-hidden="true">✳</span>OpenClaw</div><div class="header-actions">${params.unavailable ? "" : "<span class=\"badge\">Public · Read-only</span>"}${entryLink}</div></div>
${entryPending ? "<div class=\"entry-status\" role=\"status\"><h1>Loading conversation</h1><p class=\"intro\">Checking access…</p></div><div class=\"entry-result\">" : ""}<h1>${title}</h1><p class="intro">${params.unavailable ? "This conversation is not publicly available. Log in to open conversations you have access to." : `<strong>Shared with everyone, no login required.</strong> This ${isLatest ? "live view" : "page"} includes conversation text. Tool output, files, images, reasoning, and interactive content are omitted.${entryLink ? " Log in to open the full conversation if you have access." : ""}`}</p>${entryPending ? "</div>" : ""}</header>
${!isLatest && !params.unavailable ? `<p class="page-label">Earlier conversation · <a href="${escapeHtml(params.latestUrl)}">Back to latest</a></p>` : ""}
${truncated ? "<aside class=\"notice\">Some messages or long text are omitted to keep this public page within its size limit.</aside>" : ""}
${navigation}
${params.unavailable ? "" : `<section class="transcript" aria-label="Conversation">${rows.length ? rows.toReversed().join("\n") : `<p class="empty">${isLatest ? "No public conversation text yet. New messages will appear here as the conversation continues." : "No public conversation text on this page. Use the page links to continue reading."}</p>`}</section>`}
${navigation}
${params.unavailable ? "" : `<footer><span>${isLatest ? params.entryUrl ? "Live view · Updates while this tab is visible" : "Live view · Refreshes every 15 seconds" : "Earlier conversation · Updates when you reload"}<br>Public access can be revoked by the session owner.</span><a href="${escapeHtml(params.latestUrl)}">${isLatest ? "Refresh now" : "Back to latest"}</a></footer>`}
</main>${params.entryUrl ? `<script>${PUBLIC_SESSION_ENTRY_SCRIPT}<\/script>` : ""}</body></html>`;
}
//#endregion
//#region src/gateway/control-ui-public-session-read.ts
function readAuthorizedTarget(cfg, locator, projection) {
const parsed = parseAgentSessionKey(locator.sessionKey);
const fixedOwner = resolvePersistedSessionStoreOwnerForKey(cfg, locator.sessionKey);
if (locator.sessionKey !== "global" && (!parsed || parsed.agentId !== locator.agentId || locator.sessionKey !== `agent:${parsed.agentId}:${parsed.rest}`) || fixedOwner.kind === "retired" || fixedOwner.kind === "configured" && fixedOwner.agentId !== locator.agentId || !listAgentIds(cfg).includes(locator.agentId) || isIncognitoSessionKey(locator.sessionKey) || projection.sharingRevision === void 0 || projection.state.cfg !== cfg) return;
const query = {
key: locator.sessionKey,
agentId: locator.agentId
};
const state = projection.sharingTargetState(query);
if (state.status !== "ready") return;
const target = state.target;
const share = resolveSessionPublicShare(target.entry);
if (target.canonicalKey !== locator.sessionKey || target.agentId !== locator.agentId || share?.id !== locator.shareId || share.sessionId !== locator.sessionId) return;
const source = projection.readSource({
...query,
storePath: target.storePath
});
if (!source || typeof source.databaseIdentity !== "string") return;
assertExistingDatabaseIdentity(source.path, `file:${source.databaseIdentity}`, source.databaseBirthtime);
return {
target,
source
};
}
/** The resident owner installs committed sharing facts before a response can consume them. */
function isPublicSessionShareActive(cfg, locator, projection) {
return Boolean(readAuthorizedTarget(cfg, locator, projection));
}
/** Only the exact published generation is readable; this grants no Gateway session authority. */
async function readPublicSessionShare(cfg, locator, options) {
const { projection } = options;
if (isIncognitoSessionKey(locator.sessionKey) || !listAgentIds(cfg).includes(locator.agentId)) return null;
const queries = () => [{
key: locator.sessionKey,
agentId: locator.agentId
}];
const initial = await withReadySessionRows(projection, queries, () => readAuthorizedTarget(cfg, locator, projection));
if (!initial) return null;
const history = await readSessionMessagesPageWithStatsAsync({
agentId: initial.source.agentId,
sessionKey: locator.sessionKey,
sessionId: locator.sessionId,
storePath: initial.source.path,
sessionEntry: initial.target.entry
}, {
offset: options.offset ?? 0,
maxMessages: 100,
maxBytes: 1048576,
allowResetArchiveFallback: false
});
return withReadySessionRows(projection, queries, () => {
const current = readAuthorizedTarget(cfg, locator, projection);
if (!current || current.source.path !== initial.source.path || current.source.databaseIdentity !== initial.source.databaseIdentity || current.source.databaseBirthtime !== initial.source.databaseBirthtime) return null;
return {
title: (current.target.entry.label || current.target.entry.displayName || "Shared session").trim() || "Shared session",
messages: history.messages,
totalMessages: history.totalMessages,
truncated: history.omittedOversized === true,
...history.olderOffset !== void 0 ? { olderOffset: history.olderOffset } : {}
};
});
}
//#endregion
//#region src/gateway/control-ui-public-session-response.ts
/** Both public routes recheck publication in the synchronous response frame, including 304s. */
async function servePublicSessionRepresentation(params) {
const { req, res, config, gate, locator, projection, offset, unavailable } = params;
const result = await gate.run({
publicationKey: locator.shareId,
sessionKey: locator.sessionKey,
config,
requestKey: params.requestKey,
work: async () => {
const session = await readPublicSessionShare(config, locator, {
offset,
projection
});
return session ? renderPublicSessionDocument({
...session,
...params.document,
isLatest: offset === 0,
...session.olderOffset !== void 0 ? { olderUrl: params.olderUrl(session.olderOffset) } : {}
}) : null;
}
});
if (result.kind !== "ok") {
unavailable(result.kind === "rate-limited" ? 429 : 503, result.kind === "rate-limited" ? result.retryAfterSeconds : void 0);
return true;
}
return withReadySessionRows(projection, () => [{
key: locator.sessionKey,
agentId: locator.agentId
}], () => {
const representation = result.value;
if (!representation || !isPublicSessionShareActive(config, locator, projection)) unavailable(404);
else if (!representation.isCurrent()) unavailable(503);
else {
const { body, etag } = representation;
res.setHeader("ETag", etag);
if (req.headers["if-none-match"] === etag) {
res.statusCode = 304;
res.end();
} else {
res.statusCode = 200;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Content-Length", Buffer.byteLength(body));
res.end(body);
}
}
return true;
});
}
//#endregion
export { PUBLIC_SESSION_ENTRY_SCRIPT as n, renderPublicSessionDocument as r, servePublicSessionRepresentation as t };