openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
143 lines (142 loc) • 6.97 kB
JavaScript
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as containsAsciiControlCharacter } from "./string-normalization-CECOqf64.mjs";
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs";
import { s as registerSecretValueForRedaction } from "./secret-redaction-registry-DorVHn0l.mjs";
import { c as resolveDeviceIdentityStore } from "./device-identity-store-id-VtwS0.mjs";
import { n as deriveCanonicalEd25519PrivateKeyRaw } from "./ed25519-signature-D_xiObSW.mjs";
import { i as loadOrCreateProcessDeviceIdentity, n as loadDeviceIdentityIfPresent } from "./device-identity-KmxGQO-v.mjs";
import "./public-share-BBu0WQWQ.mjs";
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto";
//#region src/gateway/control-ui-public-session-token.ts
const PUBLIC_SESSION_TOKEN_PREFIX = "v1.";
const PUBLIC_SESSION_TOKEN_CIPHER = "aes-256-gcm";
const PUBLIC_SESSION_TOKEN_NONCE_BYTES = 12;
const PUBLIC_SESSION_TOKEN_AAD = Buffer.from("openclaw.public-session-share-locator.aad.v1", "utf8");
const PUBLIC_SESSION_TOKEN_KEY_SALT = Buffer.from("openclaw.public-session-share-locator.salt.v1", "utf8");
const PUBLIC_SESSION_TOKEN_KEY_INFO = Buffer.from("openclaw.public-session-share-locator.key.v1", "utf8");
const PUBLIC_SESSION_TOKEN_MAX_PLAINTEXT_BYTES = 5e3;
const PUBLIC_SESSION_LOCATOR_KEYS = [
"agentId",
"sessionId",
"sessionKey",
"shareId"
].toSorted();
const PUBLIC_SESSION_CLAIM_KEYS = [...PUBLIC_SESSION_LOCATOR_KEYS, "v"].toSorted();
function hasExactKeys(value, keys) {
return Object.keys(value).toSorted().join("\0") === keys.join("\0");
}
function isValidLocator(value) {
if (!isRecord(value)) return false;
return typeof value.agentId === "string" && /^[a-z0-9][a-z0-9_-]{0,63}$/u.test(value.agentId) && typeof value.sessionKey === "string" && value.sessionKey.length > 0 && value.sessionKey.length <= 4096 && !containsAsciiControlCharacter(value.sessionKey) && typeof value.sessionId === "string" && value.sessionId.length > 0 && value.sessionId.length <= 512 && !/[/\\\s\p{Cc}]/u.test(value.sessionId) && typeof value.shareId === "string" && /^[a-f0-9]{48}$/u.test(value.shareId);
}
function parseClaims(value) {
if (!isRecord(value)) return null;
if (value.v !== 1 || !hasExactKeys(value, PUBLIC_SESSION_CLAIM_KEYS) || !isValidLocator(value)) return null;
return {
agentId: value.agentId,
sessionKey: value.sessionKey,
sessionId: value.sessionId,
shareId: value.shareId
};
}
function derivePublicSessionTokenKey(identity) {
const privateSeed = deriveCanonicalEd25519PrivateKeyRaw(identity.privateKeyPem);
try {
return Buffer.from(hkdfSync("sha256", privateSeed, PUBLIC_SESSION_TOKEN_KEY_SALT, PUBLIC_SESSION_TOKEN_KEY_INFO, 32));
} finally {
privateSeed.fill(0);
}
}
/** Creates a domain-separated opaque-locator codec from one durable Gateway identity. */
function createPublicSessionShareTokenCodec(identity) {
const key = derivePublicSessionTokenKey(identity);
return {
mint(locator) {
if (!isValidLocator(locator) || !hasExactKeys(locator, PUBLIC_SESSION_LOCATOR_KEYS)) throw new Error("invalid public session locator");
const plaintext = Buffer.from(JSON.stringify({
v: 1,
...locator
}), "utf8");
if (plaintext.byteLength > PUBLIC_SESSION_TOKEN_MAX_PLAINTEXT_BYTES) throw new Error("public session locator exceeds the maximum length");
const nonce = randomBytes(PUBLIC_SESSION_TOKEN_NONCE_BYTES);
const cipher = createCipheriv(PUBLIC_SESSION_TOKEN_CIPHER, key, nonce);
cipher.setAAD(PUBLIC_SESSION_TOKEN_AAD);
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const token = `${PUBLIC_SESSION_TOKEN_PREFIX}${Buffer.concat([
nonce,
cipher.getAuthTag(),
ciphertext
]).toString("base64url")}`;
if (token.length > 7e3) throw new Error("public session token exceeds the maximum length");
registerSecretValueForRedaction(locator.shareId);
registerSecretValueForRedaction(token);
return token;
},
resolve(token) {
if (token.length > 7e3 || !token.startsWith(PUBLIC_SESSION_TOKEN_PREFIX)) return null;
const encoded = token.slice(3);
if (!encoded || !/^[A-Za-z0-9_-]+$/u.test(encoded)) return null;
const sealed = Buffer.from(encoded, "base64url");
if (sealed.toString("base64url") !== encoded || sealed.byteLength <= 28) return null;
const nonce = sealed.subarray(0, PUBLIC_SESSION_TOKEN_NONCE_BYTES);
const tag = sealed.subarray(PUBLIC_SESSION_TOKEN_NONCE_BYTES, 28);
const ciphertext = sealed.subarray(28);
if (ciphertext.byteLength > PUBLIC_SESSION_TOKEN_MAX_PLAINTEXT_BYTES) return null;
try {
const decipher = createDecipheriv(PUBLIC_SESSION_TOKEN_CIPHER, key, nonce);
decipher.setAAD(PUBLIC_SESSION_TOKEN_AAD);
decipher.setAuthTag(tag);
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
if (plaintext.byteLength > PUBLIC_SESSION_TOKEN_MAX_PLAINTEXT_BYTES) return null;
const locator = parseClaims(JSON.parse(plaintext.toString("utf8")));
if (!locator) return null;
registerSecretValueForRedaction(token);
registerSecretValueForRedaction(locator.shareId);
return locator;
} catch {
return null;
}
}
};
}
const codecsByDatabasePath = /* @__PURE__ */ new Map();
const missingIdentityDatabasePaths = /* @__PURE__ */ new Map();
function cacheCodec(databasePath, identity) {
const codec = createPublicSessionShareTokenCodec(identity);
pruneMapToMaxSize(codecsByDatabasePath, 31);
codecsByDatabasePath.set(databasePath, codec);
missingIdentityDatabasePaths.delete(databasePath);
return codec;
}
function resolveProcessCodec(params) {
const options = params.env ? { env: params.env } : {};
const { databasePath } = resolveDeviceIdentityStore(options);
const cached = codecsByDatabasePath.get(databasePath);
if (cached) return cached;
if (!params.create && missingIdentityDatabasePaths.has(databasePath)) return null;
const identity = params.create ? loadOrCreateProcessDeviceIdentity(options) : loadDeviceIdentityIfPresent(options);
if (!identity) {
pruneMapToMaxSize(missingIdentityDatabasePaths, 31);
missingIdentityDatabasePaths.set(databasePath, true);
return null;
}
return cacheCodec(databasePath, identity);
}
/** Loads the process codec before a session-store commit that will publish a grant. */
function loadPublicSessionShareTokenCodec(options = {}) {
const codec = resolveProcessCodec({
create: true,
...options.env ? { env: options.env } : {}
});
if (!codec) throw new Error("public session token identity is unavailable");
return codec;
}
/** Resolves an opaque locator without letting anonymous traffic create durable identity state. */
function resolvePublicSessionShareToken(token, options = {}) {
return resolveProcessCodec({
create: false,
...options.env ? { env: options.env } : {}
})?.resolve(token) ?? null;
}
//#endregion
export { resolvePublicSessionShareToken as n, loadPublicSessionShareTokenCodec as t };