relay-companion
Version:
Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.
1,167 lines (1,086 loc) • 46.3 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
import atomicJson from "./atomic-json.cjs";
import stateLock from "./state-lock.cjs";
import e2eeIdentity from "./e2ee-identity.cjs";
const { atomicWriteJsonSync } = atomicJson;
const { withJsonLockStrict } = stateLock;
const { identityPath } = e2eeIdentity;
export const E2EE_TRANSPARENCY_EMPTY_CONTEXT = "relay-key-transparency-empty-v1";
export const E2EE_TRANSPARENCY_ENTRY_CONTEXT = "relay-key-transparency-entry-v1";
export const E2EE_TRANSPARENCY_CHAIN_CONTEXT = "relay-key-transparency-chain-v1";
export const E2EE_KEY_PACKAGE_PROOF_CONTEXT = "relay-e2ee-key-package-v1";
export const E2EE_MLS_CREDENTIAL_CONTEXT = "relay-e2ee-mls-credential-v1";
const E2EE_OUTBOX_COMPLETED_LIMIT = 2_000;
export const E2EE_TRANSPARENCY_EMPTY_HASH = createHash("sha256")
.update(E2EE_TRANSPARENCY_EMPTY_CONTEXT)
.digest("base64url");
function statePath(name, options = {}) {
return path.join(path.dirname(identityPath(options)), name);
}
export function e2eeKeyPackagesPath(options = {}) {
return statePath("e2ee-key-packages.json", options);
}
export function e2eeTransparencyPath(options = {}) {
return statePath("e2ee-transparency.json", options);
}
export function e2eeMessageCachePath(options = {}) {
return statePath("e2ee-message-cache.json", options);
}
export function e2eeOutboxPath(options = {}) {
return statePath("e2ee-outbox.json", options);
}
export function e2eeGroupStatesPath(options = {}) {
return statePath("e2ee-group-states.json", options);
}
export function e2eeImportedHistoryPath(options = {}) {
return statePath("e2ee-imported-history.json", options);
}
export function e2eeDeviceTrustPath(options = {}) {
return statePath("e2ee-device-trust.json", options);
}
function canonicalizeJson(value) {
if (Array.isArray(value)) return value.map((item) => item === undefined ? null : canonicalizeJson(item));
if (value && typeof value === "object") {
const result = {};
for (const key of Object.keys(value).sort()) {
if (value[key] !== undefined) result[key] = canonicalizeJson(value[key]);
}
return result;
}
return value;
}
/** Stable local binding between an idempotency key and the content it means. */
export function e2eeRequestHash(context, value) {
return createHash("sha256")
.update(String(context))
.update(JSON.stringify(canonicalizeJson(value)))
.digest("base64url");
}
function sha256Base64Url(value) {
return createHash("sha256").update(value, "utf8").digest("base64url");
}
export function canonicalTransparencyEntry(input) {
return JSON.stringify([
E2EE_TRANSPARENCY_ENTRY_CONTEXT,
input.sequence,
input.type,
input.relayUserId,
input.deviceId,
input.protocol,
input.cipherSuite,
input.signaturePublicKey,
input.fingerprint,
input.deviceExpiresAt,
input.createdAt,
]);
}
export function canonicalTransparencyChain(priorHeadHash, entryHash) {
return JSON.stringify([E2EE_TRANSPARENCY_CHAIN_CONTEXT, priorHeadHash, entryHash]);
}
export function canonicalKeyPackageProof(input) {
return JSON.stringify([
E2EE_KEY_PACKAGE_PROOF_CONTEXT,
input.deviceId,
input.packageRef,
input.keyPackage,
input.expiresAt,
]);
}
export function canonicalMlsCredential(input) {
return JSON.stringify({
version: 1,
context: E2EE_MLS_CREDENTIAL_CONTEXT,
relayUserId: input.relayUserId,
deviceId: input.deviceId,
fingerprint: input.fingerprint,
});
}
export function parseMlsCredential(value) {
try {
const parsed = JSON.parse(String(value || ""));
if (
parsed?.version !== 1 ||
parsed?.context !== E2EE_MLS_CREDENTIAL_CONTEXT ||
!parsed.relayUserId ||
!parsed.deviceId ||
!/^[A-Za-z0-9_-]{43}$/.test(String(parsed.fingerprint || ""))
) return null;
return parsed;
} catch {
return null;
}
}
function freshTransparencyState(identity) {
return {
version: 1,
userId: identity.userId,
deviceId: identity.deviceId,
checkpoint: { size: "0", headHash: E2EE_TRANSPARENCY_EMPTY_HASH },
heads: { "0": E2EE_TRANSPARENCY_EMPTY_HASH },
users: {},
highestE2eeMode: "off",
};
}
const E2EE_MODE_RANK = { off: 0, optional: 1, required: 2 };
export function readTransparencyState(identity, options = {}) {
try {
const state = JSON.parse(fs.readFileSync(e2eeTransparencyPath(options), "utf8"));
if (
state?.version === 1 &&
state.userId === identity.userId &&
state.deviceId === identity.deviceId &&
/^(0|[1-9][0-9]*)$/.test(String(state.checkpoint?.size || "")) &&
/^[A-Za-z0-9_-]{43}$/.test(String(state.checkpoint?.headHash || "")) &&
state.heads &&
state.users
) {
if (!Object.hasOwn(E2EE_MODE_RANK, state.highestE2eeMode)) state.highestE2eeMode = "off";
return state;
}
} catch {}
return freshTransparencyState(identity);
}
function applyTransparencyEntry(state, entry) {
const expectedSequence = (BigInt(state.checkpoint.size) + 1n).toString();
if (entry.sequence !== expectedSequence || entry.priorHeadHash !== state.checkpoint.headHash) {
throw new Error("Relay key transparency history is inconsistent with this device's saved checkpoint.");
}
const entryHash = sha256Base64Url(canonicalTransparencyEntry(entry));
const headHash = sha256Base64Url(canonicalTransparencyChain(entry.priorHeadHash, entryHash));
if (entry.entryHash !== entryHash || entry.headHash !== headHash) {
throw new Error("Relay key transparency history failed cryptographic verification.");
}
const user = state.users[entry.relayUserId] || { devices: {} };
if (entry.type === "device_enrolled") {
const existing = user.devices[entry.deviceId];
if (existing && (
existing.signaturePublicKey !== entry.signaturePublicKey ||
existing.fingerprint !== entry.fingerprint ||
existing.protocol !== entry.protocol ||
existing.cipherSuite !== entry.cipherSuite ||
String(existing.expiresAt || "") !== String(entry.deviceExpiresAt || "")
)) {
throw new Error("Relay key transparency history attempted to replace an enrolled device identity.");
}
user.devices[entry.deviceId] = {
protocol: entry.protocol,
cipherSuite: entry.cipherSuite,
signaturePublicKey: entry.signaturePublicKey,
fingerprint: entry.fingerprint,
enrollmentSequence: entry.sequence,
enrolledAt: entry.createdAt,
expiresAt: entry.deviceExpiresAt,
revocationSequence: null,
revokedAt: null,
};
} else {
const existing = user.devices[entry.deviceId];
if (
!existing ||
existing.signaturePublicKey !== entry.signaturePublicKey ||
existing.fingerprint !== entry.fingerprint ||
existing.protocol !== entry.protocol ||
existing.cipherSuite !== entry.cipherSuite ||
String(existing.expiresAt || "") !== String(entry.deviceExpiresAt || "")
) {
throw new Error("Relay key transparency history revoked an unknown device identity.");
}
existing.revocationSequence = entry.sequence;
existing.revokedAt = entry.createdAt;
}
state.users[entry.relayUserId] = user;
state.checkpoint = { size: entry.sequence, headHash: entry.headHash };
state.heads[entry.sequence] = entry.headHash;
}
function persistTransparencyAdvance(identity, before, after, options = {}) {
const file = e2eeTransparencyPath(options);
const locked = withJsonLockStrict(file, () => {
const current = readTransparencyState(identity, options);
if (
current.checkpoint.size !== before.size ||
current.checkpoint.headHash !== before.headHash
) return false;
if (E2EE_MODE_RANK[current.highestE2eeMode] > E2EE_MODE_RANK[after.highestE2eeMode]) {
after.highestE2eeMode = current.highestE2eeMode;
}
atomicWriteJsonSync(file, after, { mode: 0o600 });
return true;
});
if (!locked.ok) throw new Error("Relay could not lock its local key transparency state; retry safely.");
return locked.value;
}
/** Once an enrolled device has used E2EE, a server response cannot silently move it back to plaintext. */
export function assertAndPinE2eeMode(identity, mode, options = {}) {
if (!Object.hasOwn(E2EE_MODE_RANK, mode)) throw new Error("Relay returned an unknown E2EE mode.");
const file = e2eeTransparencyPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readTransparencyState(identity, options);
if (E2EE_MODE_RANK[mode] < E2EE_MODE_RANK[state.highestE2eeMode]) {
throw new Error(
`Relay attempted to downgrade this device from ${state.highestE2eeMode} E2EE mode to ${mode}. Nothing was sent or opened.`,
);
}
if (E2EE_MODE_RANK[mode] > E2EE_MODE_RANK[state.highestE2eeMode]) {
state.highestE2eeMode = mode;
atomicWriteJsonSync(file, state, { mode: 0o600 });
}
return mode;
});
if (!locked.ok) throw new Error("Relay could not lock its local E2EE downgrade policy; retry safely.");
return locked.value;
}
/** Replay and pin the append-only device directory. The raw directory endpoint is never trusted here. */
export async function syncTransparency(client, identity, options = {}) {
for (;;) {
const state = readTransparencyState(identity, options);
const before = { ...state.checkpoint };
const page = await client.e2eeTransparencySync({ afterSize: before.size, limit: 500 });
if (page.from.size !== before.size || page.from.headHash !== before.headHash) {
throw new Error("Relay presented a different key transparency history to this device.");
}
for (const entry of page.entries || []) applyTransparencyEntry(state, entry);
if (
state.checkpoint.size !== page.checkpoint.size ||
state.checkpoint.headHash !== page.checkpoint.headHash
) throw new Error("Relay key transparency page ended at an unverified checkpoint.");
if (!persistTransparencyAdvance(identity, before, state, options)) continue;
if (page.complete) return readTransparencyState(identity, options);
}
}
export function assertKnownCheckpoint(state, checkpoint) {
const known = state.heads[String(checkpoint?.size || "")];
if (!known || known !== checkpoint?.headHash) {
throw new Error("Relay presented conflicting key transparency checkpoints. Nothing was sent or opened.");
}
}
export function activeDeviceFromTransparency(state, relayUserId, deviceId) {
const device = state.users?.[relayUserId]?.devices?.[deviceId];
const unexpired = device && (!device.expiresAt || Date.parse(device.expiresAt) > Date.now());
return unexpired && !device.revokedAt ? device : null;
}
/** Resolve a credential at the checkpoint authenticated inside its message. */
export function deviceAtTransparencyCheckpoint(state, relayUserId, deviceId, checkpointSize) {
const device = state.users?.[relayUserId]?.devices?.[deviceId];
if (!device) return null;
try {
const at = BigInt(checkpointSize);
const enrolled = BigInt(device.enrollmentSequence || "0");
const revoked = device.revocationSequence ? BigInt(device.revocationSequence) : null;
return enrolled > 0n && enrolled <= at && (revoked === null || revoked > at) ? device : null;
} catch {
return null;
}
}
function freshDeviceTrustState(identity) {
return { version: 1, userId: identity.userId, deviceId: identity.deviceId, users: {} };
}
/** Local TOFU/cross-signing pins. The service can route public proofs, but it cannot rewrite this trust anchor. */
export function readE2eeDeviceTrust(identity, options = {}) {
try {
const state = JSON.parse(fs.readFileSync(e2eeDeviceTrustPath(options), "utf8"));
if (
state?.version === 1 &&
state.userId === identity.userId &&
state.deviceId === identity.deviceId &&
state.users &&
typeof state.users === "object"
) return state;
} catch {}
return freshDeviceTrustState(identity);
}
export function mutateE2eeDeviceTrust(identity, mutate, options = {}) {
const file = e2eeDeviceTrustPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readE2eeDeviceTrust(identity, options);
const value = mutate(state);
atomicWriteJsonSync(file, state, { mode: 0o600 });
return value;
});
if (!locked.ok) throw new Error("Relay could not lock its device trust state; retry safely.");
return locked.value;
}
function freshKeyPackageState(identity) {
return { version: 1, userId: identity.userId, deviceId: identity.deviceId, packages: {} };
}
export function readKeyPackageState(identity, options = {}) {
try {
const state = JSON.parse(fs.readFileSync(e2eeKeyPackagesPath(options), "utf8"));
if (state?.version === 1 && state.userId === identity.userId && state.deviceId === identity.deviceId && state.packages) {
return state;
}
} catch {}
return freshKeyPackageState(identity);
}
export function mutateKeyPackageState(identity, mutate, options = {}) {
const file = e2eeKeyPackagesPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readKeyPackageState(identity, options);
const value = mutate(state);
atomicWriteJsonSync(file, state, { mode: 0o600 });
return value;
});
if (!locked.ok) throw new Error("Relay could not lock its local MLS KeyPackages; retry safely.");
return locked.value;
}
function messageCacheKey(identity) {
return createHash("sha256")
.update("relay-e2ee-local-message-cache-v1")
.update(Buffer.from(String(identity.privateKeyJwk?.d || ""), "base64url"))
.digest();
}
function outboxKey(identity) {
return createHash("sha256")
.update("relay-e2ee-local-outbox-v1")
.update(Buffer.from(String(identity.privateKeyJwk?.d || ""), "base64url"))
.digest();
}
function groupStateKey(identity) {
return createHash("sha256")
.update("relay-e2ee-local-group-state-v1")
.update(Buffer.from(String(identity.privateKeyJwk?.d || ""), "base64url"))
.digest();
}
function groupStateAad(identity, groupId) {
return Buffer.from(JSON.stringify([
"relay-e2ee-local-group-state-v1",
identity.userId,
identity.deviceId,
groupId,
]), "utf8");
}
function groupEventAad(identity, groupId, eventId) {
return Buffer.from(JSON.stringify([
"relay-e2ee-local-group-event-v1",
identity.userId,
identity.deviceId,
groupId,
eventId,
]), "utf8");
}
function groupOutboxAad(identity, groupId, eventId) {
return Buffer.from(JSON.stringify([
"relay-e2ee-local-group-outbox-v1",
identity.userId,
identity.deviceId,
groupId,
eventId,
]), "utf8");
}
function encryptGroupEvent(identity, groupId, eventId, plaintext) {
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", groupStateKey(identity), iv);
cipher.setAAD(groupEventAad(identity, groupId, eventId));
const ciphertext = Buffer.concat([
cipher.update(Buffer.from(JSON.stringify(plaintext), "utf8")),
cipher.final(),
]);
return {
iv: iv.toString("base64url"),
tag: cipher.getAuthTag().toString("base64url"),
ciphertext: ciphertext.toString("base64url"),
};
}
function encryptGroupOutbox(identity, groupId, eventId, plaintext) {
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", groupStateKey(identity), iv);
cipher.setAAD(groupOutboxAad(identity, groupId, eventId));
const ciphertext = Buffer.concat([
cipher.update(Buffer.from(JSON.stringify(plaintext), "utf8")),
cipher.final(),
]);
return {
updatedAt: new Date().toISOString(),
iv: iv.toString("base64url"),
tag: cipher.getAuthTag().toString("base64url"),
ciphertext: ciphertext.toString("base64url"),
};
}
function decryptGroupOutbox(identity, groupId, eventId, item) {
try {
const decipher = createDecipheriv("aes-256-gcm", groupStateKey(identity), Buffer.from(item.iv, "base64url"));
decipher.setAAD(groupOutboxAad(identity, groupId, eventId));
decipher.setAuthTag(Buffer.from(item.tag, "base64url"));
const value = JSON.parse(Buffer.concat([
decipher.update(Buffer.from(item.ciphertext, "base64url")),
decipher.final(),
]).toString("utf8"));
if (
!value?.requestHash ||
!["pending", "completed"].includes(value.status) ||
(value.status === "pending" && !value.wire) ||
(value.status === "completed" && !value.response)
) throw new Error("invalid outbox value");
return value;
} catch {
throw new Error("Relay's encrypted local group outbox could not be authenticated.");
}
}
function freshGroupState(identity) {
return { version: 1, userId: identity.userId, deviceId: identity.deviceId, groups: {} };
}
function readGroupStateFile(identity, options = {}) {
try {
const state = JSON.parse(fs.readFileSync(e2eeGroupStatesPath(options), "utf8"));
if (state?.version === 1 && state.userId === identity.userId && state.deviceId === identity.deviceId && state.groups) {
return state;
}
} catch {}
return freshGroupState(identity);
}
/** Read one MLS GroupState. Its serialized secrets are authenticated and encrypted at rest. */
export function readEncryptedGroupState(identity, groupId, options = {}) {
const item = readGroupStateFile(identity, options).groups[groupId];
if (!item) return null;
try {
const decipher = createDecipheriv("aes-256-gcm", groupStateKey(identity), Buffer.from(item.iv, "base64url"));
decipher.setAAD(groupStateAad(identity, groupId));
decipher.setAuthTag(Buffer.from(item.tag, "base64url"));
const plaintext = Buffer.concat([
decipher.update(Buffer.from(item.ciphertext, "base64url")),
decipher.final(),
]);
return {
bytes: new Uint8Array(plaintext),
epoch: String(item.epoch),
membershipRevision: String(item.membershipRevision),
stateRevision: Number(item.stateRevision || 0),
updatedAt: String(item.updatedAt),
};
} catch {
throw new Error("Relay's encrypted local MLS group state could not be authenticated.");
}
}
/** A processed event is committed in the same atomic JSON replacement as the advanced MLS ratchet. */
export function readProcessedGroupEvent(identity, groupId, eventId, options = {}) {
const item = readGroupStateFile(identity, options).groups[groupId]?.processedEvents?.[eventId];
if (!item) return null;
try {
const decipher = createDecipheriv("aes-256-gcm", groupStateKey(identity), Buffer.from(item.iv, "base64url"));
decipher.setAAD(groupEventAad(identity, groupId, eventId));
decipher.setAuthTag(Buffer.from(item.tag, "base64url"));
const value = JSON.parse(Buffer.concat([
decipher.update(Buffer.from(item.ciphertext, "base64url")),
decipher.final(),
]).toString("utf8"));
// v1 cached the plaintext directly. Product-integrated group messages also
// retain their opaque wire metadata so an acknowledged message can still
// be rendered after a restart without asking the service for ciphertext it
// is free to retire.
return value?.plaintext || value;
} catch {
throw new Error("Relay's encrypted local group-event cache could not be authenticated.");
}
}
/** Every durable group event available to the local product projection. */
export function readProcessedGroupEvents(identity, options = {}) {
const state = readGroupStateFile(identity, options);
const opened = [];
for (const [groupId, group] of Object.entries(state.groups || {})) {
for (const [eventId, item] of Object.entries(group.processedEvents || {})) {
try {
const decipher = createDecipheriv("aes-256-gcm", groupStateKey(identity), Buffer.from(item.iv, "base64url"));
decipher.setAAD(groupEventAad(identity, groupId, eventId));
decipher.setAuthTag(Buffer.from(item.tag, "base64url"));
const value = JSON.parse(Buffer.concat([
decipher.update(Buffer.from(item.ciphertext, "base64url")),
decipher.final(),
]).toString("utf8"));
opened.push({
groupId,
eventId,
plaintext: value?.plaintext || value,
...(value?.wire ? { wire: value.wire } : {}),
});
} catch {
throw new Error("Relay's encrypted local group-event cache could not be authenticated.");
}
}
}
return opened;
}
/** Exact wire bytes waiting for an idempotent group-send acknowledgement. */
export function readPendingGroupOutbox(identity, groupId, eventId, options = {}) {
const item = readGroupStateFile(identity, options).groups[groupId]?.pendingOutbox?.[eventId];
return item ? decryptGroupOutbox(identity, groupId, eventId, item) : null;
}
/** The one MLS epoch transition this group must finish or roll back before sending. */
export function readPendingGroupTransition(identity, groupId, options = {}) {
const item = readGroupStateFile(identity, options).groups[groupId]?.pendingTransition;
if (!item) return null;
if (!item.transitionId) throw new Error("Relay's encrypted local group transition is malformed.");
return {
transitionId: item.transitionId,
...decryptGroupOutbox(identity, groupId, item.transitionId, item),
};
}
/** Atomically replace one MLS GroupState after every ratchet or epoch advance. */
export function writeEncryptedGroupState(identity, groupId, value, options = {}) {
const file = e2eeGroupStatesPath(options);
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", groupStateKey(identity), iv);
cipher.setAAD(groupStateAad(identity, groupId));
const encrypted = Buffer.concat([cipher.update(Buffer.from(value.bytes)), cipher.final()]);
const encryptedItem = {
epoch: String(value.epoch),
membershipRevision: String(value.membershipRevision),
updatedAt: new Date().toISOString(),
iv: iv.toString("base64url"),
tag: cipher.getAuthTag().toString("base64url"),
ciphertext: encrypted.toString("base64url"),
};
const locked = withJsonLockStrict(file, () => {
const state = readGroupStateFile(identity, options);
const current = state.groups[groupId];
if (current && BigInt(current.epoch) > BigInt(encryptedItem.epoch)) {
throw new Error("Relay refused to overwrite a newer local MLS group epoch.");
}
const currentRevision = Number(current?.stateRevision || 0);
if (value.expectedStateRevision !== undefined && currentRevision !== value.expectedStateRevision) {
throw new Error("Relay detected a concurrent MLS group-state advance. Nothing was sent or acknowledged; retry safely.");
}
const stateRevision = currentRevision + 1;
const processedEvents = { ...(current?.processedEvents || {}) };
if (value.processedEvent) {
processedEvents[value.processedEvent.eventId] = encryptGroupEvent(
identity,
groupId,
value.processedEvent.eventId,
value.processedEvent.wire
? { plaintext: value.processedEvent.plaintext, wire: value.processedEvent.wire }
: value.processedEvent.plaintext,
);
const ids = Object.keys(processedEvents);
for (const eventId of ids.slice(0, Math.max(0, ids.length - 2_000))) delete processedEvents[eventId];
}
const pendingOutbox = { ...(current?.pendingOutbox || {}) };
if (value.pendingOutbox) {
const eventId = value.pendingOutbox.eventId;
const existing = pendingOutbox[eventId]
? decryptGroupOutbox(identity, groupId, eventId, pendingOutbox[eventId])
: null;
if (existing && existing.requestHash !== value.pendingOutbox.requestHash) {
throw new Error("That encrypted group idempotency key is already bound to different content.");
}
if (!existing) {
pendingOutbox[eventId] = encryptGroupOutbox(identity, groupId, eventId, {
status: "pending",
requestHash: value.pendingOutbox.requestHash,
wire: value.pendingOutbox.wire,
});
}
}
let pendingTransition = current?.pendingTransition;
if (value.pendingTransition) {
const transitionId = value.pendingTransition.transitionId;
const existing = pendingTransition
? decryptGroupOutbox(identity, groupId, pendingTransition.transitionId, pendingTransition)
: null;
if (existing?.status === "pending") {
if (
pendingTransition.transitionId !== transitionId ||
existing.requestHash !== value.pendingTransition.requestHash
) throw new Error("Another encrypted group membership transition is still pending.");
throw new Error("That encrypted group membership transition is already pending.");
}
pendingTransition = {
transitionId,
...encryptGroupOutbox(identity, groupId, transitionId, {
status: "pending",
requestHash: value.pendingTransition.requestHash,
wire: value.pendingTransition.wire,
rollbackState: value.pendingTransition.rollbackState,
appliedStateRevision: stateRevision,
}),
};
}
state.groups[groupId] = {
...encryptedItem,
stateRevision,
processedEvents,
pendingOutbox,
...(pendingTransition ? { pendingTransition } : {}),
};
atomicWriteJsonSync(file, state, { mode: 0o600 });
return stateRevision;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local MLS group state; retry safely.");
return locked.value;
}
/** Mark the exact epoch transition accepted without touching the already-advanced MLS state. */
export function completeGroupTransition(
identity,
groupId,
transitionId,
expectedRequestHash,
response,
options = {},
) {
const file = e2eeGroupStatesPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readGroupStateFile(identity, options);
const group = state.groups[groupId];
const item = group?.pendingTransition;
if (!item || item.transitionId !== transitionId) {
throw new Error("Relay could not find the encrypted group transition it just delivered.");
}
const value = decryptGroupOutbox(identity, groupId, transitionId, item);
if (value.requestHash !== expectedRequestHash) {
throw new Error("Relay refused to complete a different encrypted group transition.");
}
if (value.status === "completed") return value.response;
state.groups[groupId] = {
...group,
pendingTransition: {
transitionId,
...encryptGroupOutbox(identity, groupId, transitionId, {
status: "completed",
requestHash: value.requestHash,
response,
completedAt: new Date().toISOString(),
}),
},
};
atomicWriteJsonSync(file, state, { mode: 0o600 });
return response;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local MLS group state; retry safely.");
return locked.value;
}
/** Restore the exact pre-Commit state after the server proves another Commit won the epoch CAS. */
export function rollbackPendingGroupTransition(
identity,
groupId,
transitionId,
expectedRequestHash,
options = {},
) {
const file = e2eeGroupStatesPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readGroupStateFile(identity, options);
const group = state.groups[groupId];
const item = group?.pendingTransition;
if (!item || item.transitionId !== transitionId) return false;
const value = decryptGroupOutbox(identity, groupId, transitionId, item);
if (value.requestHash !== expectedRequestHash) {
throw new Error("Relay refused to roll back a different encrypted group transition.");
}
if (value.status !== "pending" || !value.rollbackState?.bytes) {
throw new Error("Relay cannot roll back an acknowledged encrypted group transition.");
}
const currentRevision = Number(group.stateRevision || 0);
if (
currentRevision !== Number(value.appliedStateRevision) ||
String(group.epoch) !== String(value.wire?.epoch)
) throw new Error("Relay refused to roll back because local MLS state advanced after the pending transition.");
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", groupStateKey(identity), iv);
cipher.setAAD(groupStateAad(identity, groupId));
const encrypted = Buffer.concat([
cipher.update(Buffer.from(value.rollbackState.bytes, "base64url")),
cipher.final(),
]);
state.groups[groupId] = {
epoch: String(value.rollbackState.epoch),
membershipRevision: String(value.rollbackState.membershipRevision),
stateRevision: currentRevision + 1,
updatedAt: new Date().toISOString(),
iv: iv.toString("base64url"),
tag: cipher.getAuthTag().toString("base64url"),
ciphertext: encrypted.toString("base64url"),
processedEvents: { ...(group.processedEvents || {}) },
pendingOutbox: { ...(group.pendingOutbox || {}) },
};
atomicWriteJsonSync(file, state, { mode: 0o600 });
return true;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local MLS group state; retry safely.");
return locked.value;
}
/** Record the acknowledged response so later duplicate calls never ratchet again. */
export function completeGroupOutbox(identity, groupId, eventId, expectedRequestHash, response, options = {}) {
const file = e2eeGroupStatesPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readGroupStateFile(identity, options);
const group = state.groups[groupId];
const item = group?.pendingOutbox?.[eventId];
if (!item) throw new Error("Relay could not find the encrypted group outbox entry it just delivered.");
const value = decryptGroupOutbox(identity, groupId, eventId, item);
if (value.requestHash !== expectedRequestHash) {
throw new Error("Relay refused to complete a different encrypted group outbox entry.");
}
if (value.status === "completed") return value.response;
const pendingOutbox = { ...group.pendingOutbox };
pendingOutbox[eventId] = encryptGroupOutbox(identity, groupId, eventId, {
status: "completed",
requestHash: value.requestHash,
response,
completedAt: new Date().toISOString(),
});
const completed = Object.entries(pendingOutbox)
.map(([id, encrypted]) => [id, encrypted, decryptGroupOutbox(identity, groupId, id, encrypted)])
.filter(([, , entry]) => entry.status === "completed")
.sort((a, b) => String(a[2].completedAt).localeCompare(String(b[2].completedAt)));
for (const [id] of completed.slice(0, Math.max(0, completed.length - E2EE_OUTBOX_COMPLETED_LIMIT))) {
delete pendingOutbox[id];
}
state.groups[groupId] = { ...group, pendingOutbox };
atomicWriteJsonSync(file, state, { mode: 0o600 });
return response;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local MLS group state; retry safely.");
return locked.value;
}
/** Remove an acknowledged wire payload without advancing or rolling back MLS. */
export function removePendingGroupOutbox(identity, groupId, eventId, expectedRequestHash, options = {}) {
const file = e2eeGroupStatesPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readGroupStateFile(identity, options);
const group = state.groups[groupId];
const item = group?.pendingOutbox?.[eventId];
if (!item) return false;
const value = decryptGroupOutbox(identity, groupId, eventId, item);
if (expectedRequestHash && value.requestHash !== expectedRequestHash) {
throw new Error("Relay refused to clear a different encrypted group outbox entry.");
}
const pendingOutbox = { ...group.pendingOutbox };
delete pendingOutbox[eventId];
state.groups[groupId] = { ...group, pendingOutbox };
atomicWriteJsonSync(file, state, { mode: 0o600 });
return true;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local MLS group state; retry safely.");
return locked.value;
}
export function removeEncryptedGroupState(identity, groupId, options = {}) {
const file = e2eeGroupStatesPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readGroupStateFile(identity, options);
if (!Object.hasOwn(state.groups, groupId)) return;
delete state.groups[groupId];
atomicWriteJsonSync(file, state, { mode: 0o600 });
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local MLS group state; retry safely.");
}
function readMessageCache(identity, options = {}) {
try {
const state = JSON.parse(fs.readFileSync(e2eeMessageCachePath(options), "utf8"));
if (state?.version === 1 && state.userId === identity.userId && state.deviceId === identity.deviceId && state.items) {
return state;
}
} catch {}
return { version: 1, userId: identity.userId, deviceId: identity.deviceId, items: {} };
}
function outboxAad(identity, entryId) {
return Buffer.from(JSON.stringify([
"relay-e2ee-local-outbox-v1",
identity.userId,
identity.deviceId,
entryId,
]), "utf8");
}
function readOutboxFile(identity, options = {}) {
let raw;
try {
raw = fs.readFileSync(e2eeOutboxPath(options), "utf8");
} catch (error) {
if (error?.code === "ENOENT") return { version: 1, userId: identity.userId, deviceId: identity.deviceId, items: {} };
throw new Error("Relay could not read its encrypted local outbox.");
}
try {
const state = JSON.parse(raw);
if (state?.version === 1 && state.userId === identity.userId && state.deviceId === identity.deviceId && state.items) {
return state;
}
} catch {}
throw new Error("Relay's encrypted local outbox is invalid; nothing was sent.");
}
function encryptOutboxValue(identity, entryId, value) {
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", outboxKey(identity), iv);
cipher.setAAD(outboxAad(identity, entryId));
const encrypted = Buffer.concat([
cipher.update(Buffer.from(JSON.stringify(value), "utf8")),
cipher.final(),
]);
return {
updatedAt: new Date().toISOString(),
iv: iv.toString("base64url"),
tag: cipher.getAuthTag().toString("base64url"),
ciphertext: encrypted.toString("base64url"),
};
}
function decryptOutboxValue(identity, entryId, item) {
try {
const decipher = createDecipheriv("aes-256-gcm", outboxKey(identity), Buffer.from(item.iv, "base64url"));
decipher.setAAD(outboxAad(identity, entryId));
decipher.setAuthTag(Buffer.from(item.tag, "base64url"));
const value = JSON.parse(Buffer.concat([
decipher.update(Buffer.from(item.ciphertext, "base64url")),
decipher.final(),
]).toString("utf8"));
if (
!value?.requestHash ||
!["pending", "completed"].includes(value.status) ||
(value.status === "pending" && !value.wire) ||
(value.status === "completed" && !value.response)
) throw new Error("invalid outbox value");
return value;
} catch {
throw new Error("Relay's encrypted local outbox could not be authenticated.");
}
}
export function readPendingE2eeOutbox(identity, entryId, options = {}) {
const item = readOutboxFile(identity, options).items[entryId];
return item ? decryptOutboxValue(identity, entryId, item) : null;
}
/**
* Persist the exact request before delivery. Concurrent writers with the same
* content converge on the first ciphertext; different content is refused.
*/
export function writePendingE2eeOutbox(identity, entryId, value, options = {}) {
const file = e2eeOutboxPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readOutboxFile(identity, options);
const existing = state.items[entryId]
? decryptOutboxValue(identity, entryId, state.items[entryId])
: null;
if (existing) {
if (existing.requestHash !== value.requestHash) {
throw new Error("That encrypted idempotency key is already bound to different content.");
}
return existing;
}
const pending = {
status: "pending",
requestHash: value.requestHash,
wire: value.wire,
...(value.currentSenderPackageRef ? { currentSenderPackageRef: value.currentSenderPackageRef } : {}),
};
state.items[entryId] = encryptOutboxValue(identity, entryId, pending);
atomicWriteJsonSync(file, state, { mode: 0o600 });
return pending;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local outbox; retry safely.");
return locked.value;
}
export function completeE2eeOutbox(identity, entryId, expectedRequestHash, response, options = {}) {
const file = e2eeOutboxPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readOutboxFile(identity, options);
const item = state.items[entryId];
if (!item) throw new Error("Relay could not find the encrypted outbox entry it just delivered.");
const value = decryptOutboxValue(identity, entryId, item);
if (value.requestHash !== expectedRequestHash) {
throw new Error("Relay refused to complete a different encrypted outbox entry.");
}
if (value.status === "completed") return value.response;
state.items[entryId] = encryptOutboxValue(identity, entryId, {
status: "completed",
requestHash: value.requestHash,
response,
completedAt: new Date().toISOString(),
});
const completed = Object.entries(state.items)
.map(([id, encrypted]) => [id, encrypted, decryptOutboxValue(identity, id, encrypted)])
.filter(([, , entry]) => entry.status === "completed")
.sort((a, b) => String(a[2].completedAt).localeCompare(String(b[2].completedAt)));
for (const [id] of completed.slice(0, Math.max(0, completed.length - E2EE_OUTBOX_COMPLETED_LIMIT))) {
delete state.items[id];
}
atomicWriteJsonSync(file, state, { mode: 0o600 });
return response;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local outbox; retry safely.");
return locked.value;
}
export function removePendingE2eeOutbox(identity, entryId, expectedRequestHash, options = {}) {
const file = e2eeOutboxPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readOutboxFile(identity, options);
const item = state.items[entryId];
if (!item) return false;
const value = decryptOutboxValue(identity, entryId, item);
if (expectedRequestHash && value.requestHash !== expectedRequestHash) {
throw new Error("Relay refused to clear a different encrypted outbox entry.");
}
delete state.items[entryId];
atomicWriteJsonSync(file, state, { mode: 0o600 });
return true;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local outbox; retry safely.");
return locked.value;
}
export function readCachedPlaintext(identity, messageId, options = {}) {
const item = readMessageCache(identity, options).items[messageId];
if (!item) return null;
try {
const decipher = createDecipheriv("aes-256-gcm", messageCacheKey(identity), Buffer.from(item.iv, "base64url"));
decipher.setAuthTag(Buffer.from(item.tag, "base64url"));
const plaintext = Buffer.concat([
decipher.update(Buffer.from(item.ciphertext, "base64url")),
decipher.final(),
]);
return JSON.parse(plaintext.toString("utf8"));
} catch {
throw new Error("Relay's encrypted local message cache could not be authenticated.");
}
}
export function cachePlaintext(identity, messageId, plaintext, options = {}) {
const file = e2eeMessageCachePath(options);
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", messageCacheKey(identity), iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(JSON.stringify(plaintext), "utf8")),
cipher.final(),
]);
const item = {
iv: iv.toString("base64url"),
tag: cipher.getAuthTag().toString("base64url"),
ciphertext: encrypted.toString("base64url"),
};
const locked = withJsonLockStrict(file, () => {
const state = readMessageCache(identity, options);
state.items[messageId] = item;
atomicWriteJsonSync(file, state, { mode: 0o600 });
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local message cache; retry safely.");
}
export function removeCachedPlaintext(identity, messageId, options = {}) {
const file = e2eeMessageCachePath(options);
const locked = withJsonLockStrict(file, () => {
const state = readMessageCache(identity, options);
const existed = Boolean(state.items[messageId]);
delete state.items[messageId];
atomicWriteJsonSync(file, state, { mode: 0o600 });
return existed;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local message cache; retry safely.");
return locked.value;
}
export function removeProcessedGroupEvent(identity, groupId, eventId, options = {}) {
const file = e2eeGroupStatesPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readGroupStateFile(identity, options);
const group = state.groups[groupId];
if (!group) return false;
const existed = Boolean(group.processedEvents?.[eventId]);
if (group.processedEvents) delete group.processedEvents[eventId];
if (group.pendingOutbox) delete group.pendingOutbox[eventId];
atomicWriteJsonSync(file, state, { mode: 0o600 });
return existed;
});
if (!locked.ok) throw new Error("Relay could not lock its encrypted local group cache; retry safely.");
return locked.value;
}
export function removeImportedE2eeHistoryRecord(identity, messageId, options = {}) {
const file = e2eeImportedHistoryPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readImportedHistoryFile(identity, options);
const existed = Boolean(state.items[messageId]);
delete state.items[messageId];
atomicWriteJsonSync(file, state, { mode: 0o600 });
return existed;
});
if (!locked.ok) throw new Error("Relay could not lock its imported E2EE history; retry safely.");
return locked.value;
}
export function removeLocalE2eeAttachmentDirectory(messageId, options = {}) {
const root = path.join(path.dirname(identityPath(options)), "attachments", String(messageId).replace(/[^A-Za-z0-9_-]/g, "_"));
try { fs.rmSync(root, { recursive: true, force: true }); } catch {}
}
function importedHistoryAad(identity, messageId) {
return Buffer.from(JSON.stringify([
"relay-e2ee-imported-history-v1",
identity.userId,
identity.deviceId,
messageId,
]), "utf8");
}
function readImportedHistoryFile(identity, options = {}) {
try {
const state = JSON.parse(fs.readFileSync(e2eeImportedHistoryPath(options), "utf8"));
if (state?.version === 1 && state.userId === identity.userId && state.deviceId === identity.deviceId && state.items) {
return state;
}
} catch {}
return { version: 1, userId: identity.userId, deviceId: identity.deviceId, items: {} };
}
function encryptImportedHistoryRecord(identity, messageId, record) {
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", messageCacheKey(identity), iv);
cipher.setAAD(importedHistoryAad(identity, messageId));
const ciphertext = Buffer.concat([
cipher.update(Buffer.from(JSON.stringify(record), "utf8")),
cipher.final(),
]);
return {
iv: iv.toString("base64url"),
tag: cipher.getAuthTag().toString("base64url"),
ciphertext: ciphertext.toString("base64url"),
};
}
function decryptImportedHistoryRecord(identity, messageId, record) {
try {
const decipher = createDecipheriv(
"aes-256-gcm",
messageCacheKey(identity),
Buffer.from(record.iv, "base64url"),
);
decipher.setAAD(importedHistoryAad(identity, messageId));
decipher.setAuthTag(Buffer.from(record.tag, "base64url"));
const opened = JSON.parse(Buffer.concat([
decipher.update(Buffer.from(record.ciphertext, "base64url")),
decipher.final(),
]).toString("utf8"));
if (opened?.wire?.relayId !== messageId || opened?.plaintext?.eventId !== messageId) {
throw new Error("invalid imported history record");
}
return opened;
} catch {
throw new Error("Relay's imported E2EE history could not be authenticated.");
}
}
export function importE2eeHistoryRecords(identity, records, options = {}) {
const file = e2eeImportedHistoryPath(options);
const locked = withJsonLockStrict(file, () => {
const state = readImportedHistoryFile(identity, options);
for (const record of records) {
const messageId = String(record?.wire?.relayId || "");
if (!messageId || record?.plaintext?.eventId !== messageId) {
throw new Error("Relay refused an invalid imported E2EE history record.");
}
state.items[messageId] = encryptImportedHistoryRecord(identity, messageId, record);
}
atomicWriteJsonSync(file, state, { mode: 0o600 });
return records.length;
});
if (!locked.ok) throw new Error("Relay could not lock its imported E2EE history; retry safely.");
return locked.value;
}
export function readImportedE2eeHistory(identity, options = {}) {
const state = readImportedHistoryFile(identity, options);
return Object.entries(state.items)
.map(([messageId, record]) => decryptImportedHistoryRecord(identity, messageId, record))
.sort((left, right) =>
String(left.plaintext.authoredAt || left.plaintext.createdAt).localeCompare(String(right.plaintext.authoredAt || right.plaintext.createdAt)) ||
String(left.wire.relayId).localeCompare(String(right.wire.relayId)));
}
export function readImportedE2eeHistoryRecord(identity, messageId, options = {}) {
const record = readImportedHistoryFile(identity, options).items[messageId];
return record ? decryptImportedHistoryRecord(identity, messageId, record) : null;
}
export function removeE2eeRuntimeState(options = {}) {
for (const file of [
e2eeKeyPackagesPath(options),
e2eeTransparencyPath(options),
e2eeMessageCachePath(options),
e2eeOutboxPath(options),
e2eeGroupStatesPath(options),
e2eeImportedHistoryPath(options),
e2eeDeviceTrustPath(options),
]) {
try { fs.rmSync(file, { force: true }); } catch {}
}
}