@paramms/chat-widget 1.0.44 → 1.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","sources":["../src/protocol/entities.ts","../src/protocol/actions.ts","../src/protocol/frames.ts","../src/core.ts"],"sourcesContent":["import type {\n ConversationId, MessageId, ProfileId, SubjectId, TenantId, UserId,\n} from './ids.js'\nimport type { ActionId } from './ids.js'\n\n// ── Message content (discriminated union) ─────────────────────────────────────\n// The runtime stays generic by never hard-coding business content: a message is\n// one of a small, fixed set of shapes. `card`/`form`/`system` are how action\n// results and structured prompts render — they subsume most \"rich messaging\"\n// features without a per-feature content zoo.\n\nexport interface CardField { label: string; value: string }\n\n/** A reference to an action a card/quick-reply can invoke. */\nexport interface InlineActionRef { actionId: ActionId; label: string }\n\nexport type MessageContent =\n | { kind: 'text'; text: string; enc?: boolean; iv?: string }\n | { kind: 'attachment'; url: string; mime: string; name?: string; size?: number }\n | { kind: 'card'; title?: string; body?: string; fields?: CardField[]; actions?: InlineActionRef[] }\n | { kind: 'form'; prompt: string; actionId: ActionId }\n | { kind: 'system'; event: string; data?: Record<string, string | number | boolean> }\n | { kind: 'appointment'; title: string; startIso: string; endIso: string; location?: string; description?: string; googleUrl: string; icalUrl: string; confirmed?: boolean }\n\nexport type SenderRole = 'guest' | 'agent' | 'system' | 'bot'\n\n// ── Message ───────────────────────────────────────────────────────────────────\n// Ordering is by `seq` (server-assigned, monotonic per conversation), never by\n// `ts`. `ts` is wall-clock for display only. This kills the reorder/duplicate/\n// lost-on-reconnect class of bugs that millisecond-timestamp ordering caused.\n\nexport interface Message {\n id: MessageId\n conversationId: ConversationId\n seq: number\n senderId: UserId\n senderRole: SenderRole\n content: MessageContent\n ts: number\n replyToId?: MessageId\n editedAt?: number\n deletedAt?: number\n reactions?: Record<string, UserId[]>\n internal?: boolean // true = internal note, only visible to agents\n}\n\n// ── Conversation (the room; messages partition by conversationId) ─────────────\n// The room is the conversation, NOT the subject. Two guests discussing the same\n// subject get two conversations. `subjectId` is a nullable reference, never part\n// of the room identity — so \"one thread per (guest, subject)\" is enforced as\n// app logic at open-time, and many-threads-per-subject stays possible for free.\n\nexport interface Conversation {\n id: ConversationId\n tenantId: TenantId\n profileId: ProfileId // behavior profile → which actions this room has\n subjectId?: SubjectId // optional: the thing it's about\n guestId: UserId // the end-user\n /** Host-supplied display info for an identified guest (widget `user` option).\n * Display metadata only — identity is still the token/guestId. */\n guestName?: string\n guestEmail?: string\n guestAvatar?: string\n guestMeta?: Record<string, string>\n /** True once the guest's identity has been proven by a signed ES256 JWT\n * against the chatroom's guestPublicKey (secure identity mode). */\n guestVerified?: boolean\n participants: UserId[] // guest + any assigned agents (membership = authz)\n assignedAgentId?: UserId // routing/ownership\n aiActive?: boolean // staff assigned the AI to answer this room\n state: string // conversation state-machine state\n firstResponseAt?: number // first agent reply ts (SLA)\n csat?: number // satisfaction score 1–5 (set on resolution)\n lastSeq: number // highest seq assigned in this conversation\n tags?: string[] // macro/manual tags (e.g. \"refund\", \"vip\")\n /** Live sentiment of the guest's most recent message (best-effort, async). */\n sentiment?: 'positive' | 'neutral' | 'frustrated'\n /** -1 (very frustrated) .. +1 (very positive); paired with `sentiment`. */\n sentimentScore?: number\n /** Set once an SLA-breach escalation macro has fired, so it only fires once. */\n slaEscalatedAt?: number\n /** If set, the conversation is snoozed until this Unix ms timestamp.\n * Hidden from the inbox until the timestamp passes, then resurfaces. */\n snoozedUntil?: number\n /** Page URL where the widget was open when the conversation started. */\n pageUrl?: string\n /** Browser tab title at conversation start — gives agents context. */\n pageTitle?: string\n createdAt: number\n updatedAt: number\n}\n\n// ── Co-browsing / shared annotation ────────────────────────────────────────---\n// A lightweight shared whiteboard layered over a conversation: agent and guest\n// can draw freehand strokes that both sides see live. Strokes are relayed\n// (not stored as messages) and kept per-conversation so late joiners can catch\n// up via the `opened` frame's `annotations` field.\n\nexport interface AnnotationPoint { x: number; y: number }\nexport interface AnnotationStroke {\n id: string\n points: AnnotationPoint[]\n color: string\n width: number\n by: UserId\n}\n\n// ── Subject (the referenced entity — Intercom \"custom object\") ────────────────\n// Carries shared state (available → reserved → sold) and fields (price, vin…)\n// that actions read/write. Many conversations reference one subject. Never a room.\n\nexport interface Subject {\n id: SubjectId\n tenantId: TenantId\n title: string\n state: string\n fields: Record<string, string | number | boolean>\n /** URL of the page where the subject lives (e.g. the listing page URL).\n * Captured automatically by the widget and stored on first open. */\n url?: string\n createdAt: number\n updatedAt: number\n}\n\n// ── Conversation lifecycle ────────────────────────────────────────────────────\n/** States in which a conversation is no longer \"open\": it's done with, so inbox\n * / proactive sweeps skip it and load balancing frees the assigned agent. The\n * single source of truth for \"is this conversation finished?\". */\nexport const TERMINAL_STATES: ReadonlySet<string> = new Set([\n 'resolved', 'closed', 'sold', 'issued', 'checked_out',\n])\nexport function isTerminalState(state: string): boolean {\n return TERMINAL_STATES.has(state)\n}\n\nexport type Channel = 'widget' | 'email' | 'sms' | 'whatsapp' | 'instagram' | 'kakao' | 'messenger' | 'line'\n","import type { ActionId, ProfileId, TenantId } from './ids.js'\n\n// ── Actions: the product primitive ────────────────────────────────────────────\n// An action is data an admin authors in the dashboard; the runtime stays generic\n// and only knows how to execute a small, fixed set of EFFECTS. Adding \"make\n// offer\" or \"schedule meeting\" is a config row, not a code deploy.\n\nexport type ActionAudience = 'guest' | 'agent' | 'both'\nexport type ActionSurface = 'toolbar' | 'inline' | 'quick_reply'\n\nexport interface ActionInputField {\n name: string\n label: string\n type: 'text' | 'number' | 'date' | 'select'\n required?: boolean\n options?: string[] // for type: 'select'\n}\n\n// A terminal effect produces a result and ends the action. Actions are\n// single-shot: structured multi-step lives in the conversation state machine,\n// and conversational multi-step is the `bot` effect — not an action workflow.\nexport type TerminalEffect =\n | { type: 'webhook'; url: string } // signed POST to tenant system\n | { type: 'state_transition'; target: 'conversation' | 'subject'; toState: string }\n | { type: 'bot' } // route to the AI resolver\n | { type: 'builtin'; name: string } // e.g. 'handoff'\n\n// The ONLY composition allowed is \"collect a form, then run one terminal\n// effect\" — exactly one level deep. This covers input-gathering (e.g. an offer\n// amount) without becoming a workflow engine.\nexport type ActionEffect =\n | TerminalEffect\n | { type: 'form'; fields: ActionInputField[]; then: TerminalEffect }\n\nexport type ActionResult =\n | { kind: 'system_message'; template?: string } // post a system line into the chat\n | { kind: 'card' } // render the effect's response as a card\n | { kind: 'state_badge' } // reflect a state change\n | { kind: 'none' }\n\nexport interface ActionDef {\n id: ActionId\n label: string\n icon?: string\n confirm?: boolean\n audience: ActionAudience\n surface: ActionSurface\n availableInStates?: string[] // conversation/subject states; omit = always available\n effect: ActionEffect\n result: ActionResult\n}\n\n// Client-safe projection of an action: enough for the widget to render it and\n// collect inputs, but NONE of the effect internals (webhook URLs, transition\n// targets) — those stay server-side and execute on `invoke`. The client filters\n// by `availableInStates` locally against the current conversation state, so a\n// state change needs no manifest round-trip; the server re-validates on invoke.\nexport interface ManifestAction {\n id: ActionId\n label: string\n icon?: string\n confirm?: boolean\n audience: ActionAudience\n surface: ActionSurface\n availableInStates?: string[]\n input?: ActionInputField[] // present when the action collects input (form effect)\n}\n\n/** Project an internal action to its client-safe manifest form. */\nexport function toManifestAction(a: ActionDef): ManifestAction {\n const input = a.effect.type === 'form' ? a.effect.fields : undefined\n return {\n id: a.id, label: a.label, audience: a.audience, surface: a.surface,\n ...(a.icon ? { icon: a.icon } : {}),\n ...(a.confirm ? { confirm: a.confirm } : {}),\n ...(a.availableInStates ? { availableInStates: a.availableInStates } : {}),\n ...(input ? { input } : {}),\n }\n}\n\n// ── Behavior profile (what \"domain\" becomes) ──────────────────────────────────\n// A reusable, admin-composed bundle of actions + defaults + state machine. Not a\n// built-in taxonomy — the 7 old templates become starter presets of this shape.\n// `version` lets an in-flight invocation validate against a consistent snapshot.\n\n/** Operating hours slot: 0=Sun … 6=Sat, times in \"HH:MM\" 24h local. */\nexport interface OperatingHoursSlot { day: 0|1|2|3|4|5|6; open: string; close: string }\n\nexport interface BehaviorProfile {\n id: ProfileId\n tenantId: TenantId\n name: string\n actions: ActionDef[]\n defaults: {\n greeting?: string\n theme?: { accent: string }\n e2e?: boolean\n persona?: string\n /** Paid-tier flag: when true, hides the \"Powered by Relay\" footer in the widget. */\n whiteLabel?: boolean\n /** White-label: serve/embed the widget from this hostname (e.g.\n * \"chat.acmeco.com\"). Allowed automatically as a CORS origin for the\n * control-plane API so the widget works from the custom domain. */\n customDomain?: string\n }\n states: string[]\n initialState: string\n version: number\n welcomeMessage?: string // first message guests see when opening the widget\n operatingHours?: OperatingHoursSlot[] // empty/absent = always open\n offlineMessage?: string // shown outside operating hours instead of chat\n /** Base64-encoded ECDSA P-256 SPKI public key. When set, guest tokens must be\n * signed JWTs — unsigned opaque tokens are rejected. */\n guestPublicKey?: string\n createdAt: number\n updatedAt: number\n}\n","import type {\n ConnectionId, ConversationId, MessageId, ProfileId, SubjectId, UserId,\n} from './ids.js'\nimport type { Channel, Conversation, Message, MessageContent, Subject, AnnotationStroke } from './entities.js'\nimport type { ManifestAction } from './actions.js'\n\n/** Dashboard-configured pre-chat qualification form, delivered in the manifest. */\nexport interface PreChatConfig {\n enabled: boolean\n showWhen?: 'always' | 'offline'\n fields?: ('name' | 'email' | 'phone')[]\n topics?: string[]\n callbackOption?: boolean\n title?: string\n}\n\n// ── Wire protocol ─────────────────────────────────────────────────────────────\n// One shared contract, imported by server + widget + dashboard. A change here is\n// a compile error in every consumer — which is the whole reason this lives in a\n// shared package instead of being hand-copied three times.\n\nexport type ErrorCode =\n | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'BAD_REQUEST'\n | 'RATE_LIMITED' | 'PAYLOAD_TOO_LARGE' | 'CONFLICT' | 'INTERNAL'\n\nexport type ClientFrame =\n | { type: 'auth'; token: string }\n // Open an existing conversation, or find-or-create one. Find-or-create keys on\n // (guest, subject) when subjectId is given; otherwise a fresh conversation.\n | { type: 'open'; conversationId?: ConversationId; subjectId?: SubjectId; profileId?: ProfileId; pageUrl?: string; pageTitle?: string; subjectTitle?: string; subjectMeta?: string; linkFrom?: UserId;\n /** Host-supplied display info for the guest — persisted onto the\n * conversation server-side so agents see who they're talking to.\n * Display metadata only, never used for authorization. */\n userInfo?: { name?: string; email?: string; avatar?: string; meta?: Record<string, string> } }\n | { type: 'send'; conversationId: ConversationId; clientMsgId: string; content: MessageContent; replyToId?: MessageId; via?: Channel[] }\n | { type: 'sync'; conversationId: ConversationId; sinceSeq: number } // catch-up after cursor\n | { type: 'history'; conversationId: ConversationId; beforeSeq: number; limit?: number } // load older\n | { type: 'read'; conversationId: ConversationId; seq: number } // read up to seq\n | { type: 'typing'; conversationId: ConversationId; isTyping: boolean; preview?: string }\n | { type: 'react'; conversationId: ConversationId; messageId: MessageId; emoji: string; remove?: boolean }\n | { type: 'edit'; conversationId: ConversationId; messageId: MessageId; content: MessageContent }\n | { type: 'delete'; conversationId: ConversationId; messageId: MessageId }\n | { type: 'invoke'; conversationId: ConversationId; actionId: string; clientInvokeId: string; inputs?: Record<string, unknown> }\n | { type: 'assign'; conversationId: ConversationId; agentId: UserId | null } // null = unassign\n | { type: 'tag'; conversationId: ConversationId; tag: string; remove?: boolean }\n | { type: 'note'; conversationId: ConversationId; clientMsgId: string; text: string } // internal note\n | { type: 'agent_status'; status: 'online' | 'away' | 'offline' } // agent sets their availability\n // Co-browsing: a freehand stroke (or \"clear\") on the shared annotation canvas\n // for a subject-anchored conversation. Relayed live to the other participant.\n | { type: 'annotate'; conversationId: ConversationId; stroke: Omit<AnnotationStroke, 'by'> }\n | { type: 'annotate_clear'; conversationId: ConversationId }\n | { type: 'pubkey'; conversationId: ConversationId; key: string }\n // X3DH async E2E: a client uploads a batch of one-time prekeys so peers can\n // encrypt to them while they are offline. The server stores them opaquely and\n // vends one on demand — it never derives or uses the keys.\n | { type: 'uploadPrekeys'; identityKey: string; signedPrekey: string; signedPrekeyId: string; signature: string; oneTimePrekeys: string[] }\n | { type: 'fetchPrekey'; targetUserId: UserId }\n // Inbox stream subscription — used by the agent dashboard, which reuses this\n // ConnectionManager. Typed here so the dashboard doesn't need `as never`.\n | { type: 'subscribe_inbox' }\n | { type: 'unsubscribe_inbox' }\n | { type: 'ping' }\n\nexport type ServerFrame =\n | { type: 'authed'; userId: UserId; connectionId: ConnectionId }\n | { type: 'opened'; conversation: Conversation; subject?: Subject; annotations?: AnnotationStroke[] }\n | { type: 'manifest'; conversationId: ConversationId; actions: ManifestAction[]; version: number; name?: string; theme?: { accent: string }; e2e?: boolean; offline?: boolean; offlineMessage?: string; whiteLabel?: boolean; launcherMessage?: { title: string; subtitle?: string }; preChat?: PreChatConfig }\n | { type: 'message'; message: Message }\n | { type: 'ack'; clientMsgId: string; messageId: MessageId; seq: number; ts: number }\n | { type: 'delivered'; conversationId: ConversationId; seq: number; to: UserId }\n | { type: 'read'; conversationId: ConversationId; seq: number; by: UserId }\n | { type: 'sync'; conversationId: ConversationId; messages: Message[] }\n | { type: 'history'; conversationId: ConversationId; messages: Message[]; hasMore: boolean }\n | { type: 'typing'; conversationId: ConversationId; userId: UserId; isTyping: boolean; preview?: string }\n | { type: 'reaction'; conversationId: ConversationId; messageId: MessageId; emoji: string; by: UserId; removed: boolean }\n | { type: 'edited'; conversationId: ConversationId; messageId: MessageId; content: MessageContent; editedAt: number }\n | { type: 'deleted'; conversationId: ConversationId; messageId: MessageId; ts: number }\n | { type: 'state'; conversationId: ConversationId; state: string }\n | { type: 'assigned'; conversationId: ConversationId; agentId: UserId | null }\n | { type: 'tagged'; conversationId: ConversationId; tag: string; removed: boolean }\n | { type: 'visitor_count'; count: number } // broadcast to agents: guests currently connected\n | { type: 'agent_status_changed'; agentId: UserId; status: 'online' | 'away' | 'offline' }\n // Live sentiment of a guest's most recent message — relayed to agents only so\n // the inbox can flag frustrated conversations as they happen.\n | { type: 'sentiment'; conversationId: ConversationId; label: 'positive' | 'neutral' | 'frustrated'; score: number }\n // Co-browsing: relay of an annotation stroke / clear to everyone in the room.\n | { type: 'annotation'; conversationId: ConversationId; stroke: AnnotationStroke }\n | { type: 'annotation_clear'; conversationId: ConversationId; by: UserId }\n | { type: 'subjectState'; subjectId: SubjectId; state: string }\n | { type: 'presence'; conversationId: ConversationId; userId: UserId; status: 'online' | 'offline'; lastSeen?: number }\n | { type: 'invoked'; clientInvokeId: string; ok: boolean; error?: string }\n | { type: 'error'; code: ErrorCode; message: string }\n | { type: 'peerkey'; conversationId: ConversationId; userId: UserId; key: string }\n // X3DH bundle vended to a requesting client so they can encrypt to an offline peer.\n // Contains null when the target user has no registered prekeys.\n | { type: 'prekeyBundle'; targetUserId: UserId; bundle: { identityKey: string; signedPrekey: string; signedPrekeyId: string; signature: string; oneTimePrekey?: string } | null }\n | { type: 'pong' }\n // Live inbox update for the guest's OWN conversation list (widget list socket\n // subscribes via `subscribe_inbox`). `patch` mirrors the agent inbox patch; the\n // list re-fetches on receipt, so only `kind`/`conversationId` are load-bearing.\n | { type: 'inbox_event'; kind: 'new' | 'update'; conversationId: ConversationId; patch?: Record<string, unknown> }\n\n/** Limits referenced by both ends so validation stays consistent. */\nexport const LIMITS = {\n MAX_TEXT_LEN: 8_000,\n MAX_HISTORY_LIMIT: 100,\n DEFAULT_HISTORY: 50,\n} as const\n","// ── @paramms/chat-widget/core — the headless SDK ─────────────────────────────\n// Everything you need to build your OWN chat UI (an in-app messenger, a\n// marketplace inbox, a full chat app) on the Relay protocol, with zero DOM or\n// React dependencies. This is not a new client: it is the exact transport,\n// store, outbox, and E2E machinery the bundled widget AND the agent dashboard\n// run on — re-exported behind a stable boundary, plus a small convenience\n// client for the common case.\n//\n// import { RelayClient } from '@paramms/chat-widget/core'\n//\n// // ONE url, any scheme — wss/ws/http(s) all work; ws + REST derived from it.\n// const relay = new RelayClient({ url: 'https://api.relay.paramms.com', token, profileId: 'p_x' })\n// const convo = relay.open({ subjectId: 'listing_42' }) // support thread\n// const dm = relay.open({ kind: 'direct', peerId: 'user_bob' }) // user↔user (signed identity required)\n// convo.onChange(() => render(convo.store.messages()))\n// convo.send('hello!')\n//\n// For React, see '@paramms/chat-widget/hooks'.\nexport { ConnectionManager, type SocketLike } from './connection.js'\nexport { ChatStore } from './store.js'\nexport { PersistentOutbox } from './outbox.js'\nexport { E2ESession } from './e2e.js'\nexport { restoreHistory, resolveRelayUrls, httpBaseFromWsUrl } from './history.js'\nexport { mountChatList, type ChatListEntry, type ChatListHandle, type ChatListOptions } from './chatlist.js'\nexport { persistentUid } from './uid.js'\nexport * from './protocol/index.js'\n\nimport { ConnectionManager } from './connection.js'\nimport { ChatStore } from './store.js'\nimport type { ClientFrame, ServerFrame, ConversationId, UserId } from './protocol/index.js'\nimport { asUserId } from './protocol/index.js'\nimport { persistentUid } from './uid.js'\nimport { resolveRelayUrls } from './history.js'\n\nexport interface RelayClientOptions {\n /** Relay URL — ONE url, any scheme. `https://api.relay.paramms.com` is the\n * recommended form; the WebSocket URL (`wss://…/ws`) and REST base are\n * derived from it automatically. `wss://`/`ws://`/`http://` also accepted. */\n url: string\n /** HTTP(S) base for REST calls — only when the REST API lives on a\n * DIFFERENT origin than the socket. Normally omit.\n * @deprecated pass a single `url`; kept for back-compat. */\n apiUrl?: string\n /** Identity: a signed JWT (secure), a stable userId (host-vouched), or omit\n * for an anonymous per-browser guest (browser environments only). */\n token?: string\n /** Chatroom id (from the dashboard). Required to open conversations. */\n profileId: string\n}\n\nexport interface OpenOptions {\n /** Support thread scoped to a subject (listing/order/…): one thread per\n * (user, subject). Omit for the profile's single support thread. */\n subjectId?: string\n subjectTitle?: string\n /** User↔user conversation (requires the chatroom to have signed identity\n * and `token` to be a valid signed JWT). */\n kind?: 'direct'\n peerId?: string\n /** Display info persisted for agents (support threads only). */\n user?: { name?: string; email?: string; avatar?: string; meta?: Record<string, string> }\n}\n\n/** One conversation = one connection + one store. Deliberately thin: the\n * store is the source of truth, `onChange` is the render signal, everything\n * else is the same primitives the first-party UIs use. */\nexport class RelayConversation {\n readonly store: ChatStore\n private readonly conn: ConnectionManager\n private readonly listeners = new Set<() => void>()\n private msgSeq = 0\n private _status = 'connecting'\n private _statusMessage: string | undefined\n\n constructor(opts: RelayClientOptions & OpenOptions & { me: UserId }) {\n this.store = new ChatStore(opts.me)\n const open: Extract<ClientFrame, { type: 'open' }> = {\n type: 'open',\n profileId: opts.profileId as never,\n ...(opts.subjectId ? { subjectId: opts.subjectId as never } : {}),\n ...(opts.subjectTitle ? { subjectTitle: opts.subjectTitle } : {}),\n ...(opts.kind === 'direct' ? { kind: 'direct' as const, peerId: asUserId(opts.peerId ?? '') } : {}),\n ...(opts.user ? { userInfo: opts.user } : {}),\n }\n // Accept any scheme (https/http/wss/ws) — a plain `https://api.…` URL is\n // resolved to the concrete `wss://…/ws` socket endpoint, exactly like the\n // bundled widget's mount(). Before this, RelayClient required a raw\n // WebSocket URL while the React components took `https://` — one URL now\n // works across the entire SDK.\n const { wsUrl } = resolveRelayUrls(opts.url, opts.apiUrl)\n this.conn = new ConnectionManager({\n url: wsUrl,\n token: opts.token ?? opts.me,\n open,\n getCursor: () => this.store.highestSeq(),\n onFrame: (f: ServerFrame) => {\n // The server tells us our CANONICAL id on auth (a signed JWT's sub,\n // not the raw token) — capture it so `mine` checks work under every\n // identity tier.\n if (f.type === 'authed') this._me = f.userId as UserId\n this.store.apply(f); this.emit()\n },\n onStatusChange: (s, msg) => { this._status = s; this._statusMessage = msg; this.emit() },\n })\n this.conn.connect()\n }\n\n /** Subscribe to any change (message, typing, status). Returns unsubscribe. */\n onChange(fn: () => void): () => void {\n this.listeners.add(fn)\n return () => this.listeners.delete(fn)\n }\n private emit(): void { for (const fn of this.listeners) fn() }\n\n private _me: UserId | undefined\n /** Our canonical user id as resolved by the server (JWT sub / userId / anon id). */\n get me(): UserId | undefined { return this._me }\n get conversationId(): ConversationId | undefined { return this.store.conversationId }\n get status(): string { return this._status }\n get statusMessage(): string | undefined { return this._statusMessage }\n\n send(text: string): void {\n const clientMsgId = `c_${Date.now().toString(36)}_${++this.msgSeq}`\n const cid = this.store.conversationId\n if (!cid) return\n this.store.addOptimistic(clientMsgId, { kind: 'text', text })\n this.conn.send({ type: 'send', conversationId: cid, clientMsgId, content: { kind: 'text', text } })\n this.emit()\n }\n\n typing(isTyping: boolean, preview?: string): void {\n const cid = this.store.conversationId\n if (!cid) return\n this.conn.send({ type: 'typing', conversationId: cid, isTyping, ...(preview ? { preview } : {}) })\n }\n\n markRead(): void {\n const cid = this.store.conversationId\n if (!cid) return\n this.conn.send({ type: 'read', conversationId: cid, seq: this.store.highestSeq() })\n }\n\n close(): void { this.conn.close(); this.listeners.clear() }\n}\n\nexport class RelayClient {\n constructor(private readonly opts: RelayClientOptions) {}\n\n /** The identity this client will act as: the token's subject (resolved\n * server-side), the raw userId, or a persistent anonymous browser id. */\n me(): UserId {\n return asUserId(this.opts.token ?? persistentUid())\n }\n\n open(open: OpenOptions = {}): RelayConversation {\n return new RelayConversation({ ...this.opts, ...open, me: this.me() })\n }\n}\n"],"names":["TERMINAL_STATES","isTerminalState","state","toManifestAction","a","input","LIMITS","RelayConversation","opts","__publicField","ChatStore","open","asUserId","wsUrl","resolveRelayUrls","ConnectionManager","f","s","msg","fn","text","clientMsgId","cid","isTyping","preview","RelayClient","persistentUid"],"mappings":"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"}
1
+ {"version":3,"file":"core.js","sources":["../src/protocol/entities.ts","../src/protocol/actions.ts","../src/protocol/frames.ts","../src/core.ts"],"sourcesContent":["import type {\n ConversationId, MessageId, ProfileId, SubjectId, TenantId, UserId,\n} from './ids.js'\nimport type { ActionId } from './ids.js'\n\n// ── Message content (discriminated union) ─────────────────────────────────────\n// The runtime stays generic by never hard-coding business content: a message is\n// one of a small, fixed set of shapes. `card`/`form`/`system` are how action\n// results and structured prompts render — they subsume most \"rich messaging\"\n// features without a per-feature content zoo.\n\nexport interface CardField { label: string; value: string }\n\n/** A reference to an action a card/quick-reply can invoke. */\nexport interface InlineActionRef { actionId: ActionId; label: string }\n\nexport type MessageContent =\n | { kind: 'text'; text: string; enc?: boolean; iv?: string }\n | { kind: 'attachment'; url: string; mime: string; name?: string; size?: number }\n | { kind: 'card'; title?: string; body?: string; fields?: CardField[]; actions?: InlineActionRef[] }\n | { kind: 'form'; prompt: string; actionId: ActionId }\n | { kind: 'system'; event: string; data?: Record<string, string | number | boolean> }\n | { kind: 'appointment'; title: string; startIso: string; endIso: string; location?: string; description?: string; googleUrl: string; icalUrl: string; confirmed?: boolean }\n\nexport type SenderRole = 'guest' | 'agent' | 'system' | 'bot'\n\n// ── Message ───────────────────────────────────────────────────────────────────\n// Ordering is by `seq` (server-assigned, monotonic per conversation), never by\n// `ts`. `ts` is wall-clock for display only. This kills the reorder/duplicate/\n// lost-on-reconnect class of bugs that millisecond-timestamp ordering caused.\n\nexport interface Message {\n id: MessageId\n conversationId: ConversationId\n seq: number\n senderId: UserId\n senderRole: SenderRole\n content: MessageContent\n ts: number\n replyToId?: MessageId\n editedAt?: number\n deletedAt?: number\n reactions?: Record<string, UserId[]>\n internal?: boolean // true = internal note, only visible to agents\n}\n\n// ── Conversation (the room; messages partition by conversationId) ─────────────\n// The room is the conversation, NOT the subject. Two guests discussing the same\n// subject get two conversations. `subjectId` is a nullable reference, never part\n// of the room identity — so \"one thread per (guest, subject)\" is enforced as\n// app logic at open-time, and many-threads-per-subject stays possible for free.\n\nexport interface Conversation {\n id: ConversationId\n tenantId: TenantId\n profileId: ProfileId // behavior profile → which actions this room has\n subjectId?: SubjectId // optional: the thing it's about\n guestId: UserId // the end-user\n /** Host-supplied display info for an identified guest (widget `user` option).\n * Display metadata only — identity is still the token/guestId. */\n guestName?: string\n guestEmail?: string\n guestAvatar?: string\n guestMeta?: Record<string, string>\n /** True once the guest's identity has been proven by a signed ES256 JWT\n * against the chatroom's guestPublicKey (secure identity mode). */\n guestVerified?: boolean\n participants: UserId[] // guest + any assigned agents (membership = authz)\n assignedAgentId?: UserId // routing/ownership\n aiActive?: boolean // staff assigned the AI to answer this room\n state: string // conversation state-machine state\n firstResponseAt?: number // first agent reply ts (SLA)\n csat?: number // satisfaction score 1–5 (set on resolution)\n lastSeq: number // highest seq assigned in this conversation\n tags?: string[] // macro/manual tags (e.g. \"refund\", \"vip\")\n /** Live sentiment of the guest's most recent message (best-effort, async). */\n sentiment?: 'positive' | 'neutral' | 'frustrated'\n /** -1 (very frustrated) .. +1 (very positive); paired with `sentiment`. */\n sentimentScore?: number\n /** Set once an SLA-breach escalation macro has fired, so it only fires once. */\n slaEscalatedAt?: number\n /** If set, the conversation is snoozed until this Unix ms timestamp.\n * Hidden from the inbox until the timestamp passes, then resurfaces. */\n snoozedUntil?: number\n /** Page URL where the widget was open when the conversation started. */\n pageUrl?: string\n /** Browser tab title at conversation start — gives agents context. */\n pageTitle?: string\n createdAt: number\n updatedAt: number\n}\n\n// ── Subject (the referenced entity — Intercom \"custom object\") ────────────────\n// Carries shared state (available → reserved → sold) and fields (price, vin…)\n// that actions read/write. Many conversations reference one subject. Never a room.\n\nexport interface Subject {\n id: SubjectId\n tenantId: TenantId\n title: string\n state: string\n fields: Record<string, string | number | boolean>\n /** URL of the page where the subject lives (e.g. the listing page URL).\n * Captured automatically by the widget and stored on first open. */\n url?: string\n createdAt: number\n updatedAt: number\n}\n\n// ── Conversation lifecycle ────────────────────────────────────────────────────\n/** States in which a conversation is no longer \"open\": it's done with, so inbox\n * / proactive sweeps skip it and load balancing frees the assigned agent. The\n * single source of truth for \"is this conversation finished?\". */\nexport const TERMINAL_STATES: ReadonlySet<string> = new Set([\n 'resolved', 'closed', 'sold', 'issued', 'checked_out',\n])\nexport function isTerminalState(state: string): boolean {\n return TERMINAL_STATES.has(state)\n}\n\nexport type Channel = 'widget' | 'email' | 'sms' | 'whatsapp' | 'instagram' | 'kakao' | 'messenger' | 'line'\n","import type { ActionId, ProfileId, TenantId } from './ids.js'\n\n// ── Actions: the product primitive ────────────────────────────────────────────\n// An action is data an admin authors in the dashboard; the runtime stays generic\n// and only knows how to execute a small, fixed set of EFFECTS. Adding \"make\n// offer\" or \"schedule meeting\" is a config row, not a code deploy.\n\nexport type ActionAudience = 'guest' | 'agent' | 'both'\nexport type ActionSurface = 'toolbar' | 'inline' | 'quick_reply'\n\nexport interface ActionInputField {\n name: string\n label: string\n type: 'text' | 'number' | 'date' | 'select'\n required?: boolean\n options?: string[] // for type: 'select'\n}\n\n// A terminal effect produces a result and ends the action. Actions are\n// single-shot: structured multi-step lives in the conversation state machine,\n// and conversational multi-step is the `bot` effect — not an action workflow.\nexport type TerminalEffect =\n | { type: 'webhook'; url: string } // signed POST to tenant system\n | { type: 'state_transition'; target: 'conversation' | 'subject'; toState: string }\n | { type: 'bot' } // route to the AI resolver\n | { type: 'builtin'; name: string } // e.g. 'handoff'\n\n// The ONLY composition allowed is \"collect a form, then run one terminal\n// effect\" — exactly one level deep. This covers input-gathering (e.g. an offer\n// amount) without becoming a workflow engine.\nexport type ActionEffect =\n | TerminalEffect\n | { type: 'form'; fields: ActionInputField[]; then: TerminalEffect }\n\nexport type ActionResult =\n | { kind: 'system_message'; template?: string } // post a system line into the chat\n | { kind: 'card' } // render the effect's response as a card\n | { kind: 'state_badge' } // reflect a state change\n | { kind: 'none' }\n\nexport interface ActionDef {\n id: ActionId\n label: string\n icon?: string\n confirm?: boolean\n audience: ActionAudience\n surface: ActionSurface\n availableInStates?: string[] // conversation/subject states; omit = always available\n effect: ActionEffect\n result: ActionResult\n}\n\n// Client-safe projection of an action: enough for the widget to render it and\n// collect inputs, but NONE of the effect internals (webhook URLs, transition\n// targets) — those stay server-side and execute on `invoke`. The client filters\n// by `availableInStates` locally against the current conversation state, so a\n// state change needs no manifest round-trip; the server re-validates on invoke.\nexport interface ManifestAction {\n id: ActionId\n label: string\n icon?: string\n confirm?: boolean\n audience: ActionAudience\n surface: ActionSurface\n availableInStates?: string[]\n input?: ActionInputField[] // present when the action collects input (form effect)\n}\n\n/** Project an internal action to its client-safe manifest form. */\nexport function toManifestAction(a: ActionDef): ManifestAction {\n const input = a.effect.type === 'form' ? a.effect.fields : undefined\n return {\n id: a.id, label: a.label, audience: a.audience, surface: a.surface,\n ...(a.icon ? { icon: a.icon } : {}),\n ...(a.confirm ? { confirm: a.confirm } : {}),\n ...(a.availableInStates ? { availableInStates: a.availableInStates } : {}),\n ...(input ? { input } : {}),\n }\n}\n\n// ── Behavior profile (what \"domain\" becomes) ──────────────────────────────────\n// A reusable, admin-composed bundle of actions + defaults + state machine. Not a\n// built-in taxonomy — the 7 old templates become starter presets of this shape.\n// `version` lets an in-flight invocation validate against a consistent snapshot.\n\n/** Operating hours slot: 0=Sun … 6=Sat, times in \"HH:MM\" 24h local. */\nexport interface OperatingHoursSlot { day: 0|1|2|3|4|5|6; open: string; close: string }\n\nexport interface BehaviorProfile {\n id: ProfileId\n tenantId: TenantId\n name: string\n actions: ActionDef[]\n defaults: {\n greeting?: string\n theme?: { accent: string }\n e2e?: boolean\n persona?: string\n /** Paid-tier flag: when true, hides the \"Powered by Relay\" footer in the widget. */\n whiteLabel?: boolean\n /** White-label: serve/embed the widget from this hostname (e.g.\n * \"chat.acmeco.com\"). Allowed automatically as a CORS origin for the\n * control-plane API so the widget works from the custom domain. */\n customDomain?: string\n }\n states: string[]\n initialState: string\n version: number\n welcomeMessage?: string // first message guests see when opening the widget\n operatingHours?: OperatingHoursSlot[] // empty/absent = always open\n offlineMessage?: string // shown outside operating hours instead of chat\n /** Base64-encoded ECDSA P-256 SPKI public key. When set, guest tokens must be\n * signed JWTs — unsigned opaque tokens are rejected. */\n guestPublicKey?: string\n createdAt: number\n updatedAt: number\n}\n","import type {\n ConnectionId, ConversationId, MessageId, ProfileId, SubjectId, UserId,\n} from './ids.js'\nimport type { Channel, Conversation, Message, MessageContent, Subject } from './entities.js'\nimport type { ManifestAction } from './actions.js'\n\n/** Dashboard-configured pre-chat qualification form, delivered in the manifest. */\nexport interface PreChatConfig {\n enabled: boolean\n showWhen?: 'always' | 'offline'\n fields?: ('name' | 'email' | 'phone')[]\n topics?: string[]\n callbackOption?: boolean\n title?: string\n}\n\n// ── Wire protocol ─────────────────────────────────────────────────────────────\n// One shared contract, imported by server + widget + dashboard. A change here is\n// a compile error in every consumer — which is the whole reason this lives in a\n// shared package instead of being hand-copied three times.\n\nexport type ErrorCode =\n | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'BAD_REQUEST'\n | 'RATE_LIMITED' | 'PAYLOAD_TOO_LARGE' | 'CONFLICT' | 'INTERNAL'\n\nexport type ClientFrame =\n | { type: 'auth'; token: string }\n // Open an existing conversation, or find-or-create one. Find-or-create keys on\n // (guest, subject) when subjectId is given; otherwise a fresh conversation.\n | { type: 'open'; conversationId?: ConversationId; subjectId?: SubjectId; profileId?: ProfileId; pageUrl?: string; pageTitle?: string; subjectTitle?: string; subjectMeta?: string; linkFrom?: UserId;\n /** Host-supplied display info for the guest — persisted onto the\n * conversation server-side so agents see who they're talking to.\n * Display metadata only, never used for authorization. */\n userInfo?: { name?: string; email?: string; avatar?: string; meta?: Record<string, string> } }\n | { type: 'send'; conversationId: ConversationId; clientMsgId: string; content: MessageContent; replyToId?: MessageId; via?: Channel[] }\n | { type: 'sync'; conversationId: ConversationId; sinceSeq: number } // catch-up after cursor\n | { type: 'history'; conversationId: ConversationId; beforeSeq: number; limit?: number } // load older\n | { type: 'read'; conversationId: ConversationId; seq: number } // read up to seq\n | { type: 'typing'; conversationId: ConversationId; isTyping: boolean; preview?: string }\n | { type: 'react'; conversationId: ConversationId; messageId: MessageId; emoji: string; remove?: boolean }\n | { type: 'edit'; conversationId: ConversationId; messageId: MessageId; content: MessageContent }\n | { type: 'delete'; conversationId: ConversationId; messageId: MessageId }\n | { type: 'invoke'; conversationId: ConversationId; actionId: string; clientInvokeId: string; inputs?: Record<string, unknown> }\n | { type: 'assign'; conversationId: ConversationId; agentId: UserId | null } // null = unassign\n | { type: 'tag'; conversationId: ConversationId; tag: string; remove?: boolean }\n | { type: 'note'; conversationId: ConversationId; clientMsgId: string; text: string } // internal note\n | { type: 'agent_status'; status: 'online' | 'away' | 'offline' } // agent sets their availability\n | { type: 'pubkey'; conversationId: ConversationId; key: string }\n // X3DH async E2E: a client uploads a batch of one-time prekeys so peers can\n // encrypt to them while they are offline. The server stores them opaquely and\n // vends one on demand — it never derives or uses the keys.\n | { type: 'uploadPrekeys'; identityKey: string; signedPrekey: string; signedPrekeyId: string; signature: string; oneTimePrekeys: string[] }\n | { type: 'fetchPrekey'; targetUserId: UserId }\n // Inbox stream subscription — used by the agent dashboard, which reuses this\n // ConnectionManager. Typed here so the dashboard doesn't need `as never`.\n | { type: 'subscribe_inbox' }\n | { type: 'unsubscribe_inbox' }\n | { type: 'ping' }\n\nexport type ServerFrame =\n | { type: 'authed'; userId: UserId; connectionId: ConnectionId }\n | { type: 'opened'; conversation: Conversation; subject?: Subject }\n | { type: 'manifest'; conversationId: ConversationId; actions: ManifestAction[]; version: number; name?: string; theme?: { accent: string }; e2e?: boolean; offline?: boolean; offlineMessage?: string; whiteLabel?: boolean; launcherMessage?: { title: string; subtitle?: string }; preChat?: PreChatConfig }\n | { type: 'message'; message: Message }\n | { type: 'ack'; clientMsgId: string; messageId: MessageId; seq: number; ts: number }\n | { type: 'delivered'; conversationId: ConversationId; seq: number; to: UserId }\n | { type: 'read'; conversationId: ConversationId; seq: number; by: UserId }\n | { type: 'sync'; conversationId: ConversationId; messages: Message[] }\n | { type: 'history'; conversationId: ConversationId; messages: Message[]; hasMore: boolean }\n | { type: 'typing'; conversationId: ConversationId; userId: UserId; isTyping: boolean; preview?: string }\n | { type: 'reaction'; conversationId: ConversationId; messageId: MessageId; emoji: string; by: UserId; removed: boolean }\n | { type: 'edited'; conversationId: ConversationId; messageId: MessageId; content: MessageContent; editedAt: number }\n | { type: 'deleted'; conversationId: ConversationId; messageId: MessageId; ts: number }\n | { type: 'state'; conversationId: ConversationId; state: string }\n | { type: 'assigned'; conversationId: ConversationId; agentId: UserId | null }\n | { type: 'tagged'; conversationId: ConversationId; tag: string; removed: boolean }\n | { type: 'visitor_count'; count: number } // broadcast to agents: guests currently connected\n | { type: 'agent_status_changed'; agentId: UserId; status: 'online' | 'away' | 'offline' }\n // Live sentiment of a guest's most recent message — relayed to agents only so\n // the inbox can flag frustrated conversations as they happen.\n | { type: 'sentiment'; conversationId: ConversationId; label: 'positive' | 'neutral' | 'frustrated'; score: number }\n | { type: 'subjectState'; subjectId: SubjectId; state: string }\n | { type: 'presence'; conversationId: ConversationId; userId: UserId; status: 'online' | 'offline'; lastSeen?: number }\n | { type: 'invoked'; clientInvokeId: string; ok: boolean; error?: string }\n | { type: 'error'; code: ErrorCode; message: string }\n | { type: 'peerkey'; conversationId: ConversationId; userId: UserId; key: string }\n // X3DH bundle vended to a requesting client so they can encrypt to an offline peer.\n // Contains null when the target user has no registered prekeys.\n | { type: 'prekeyBundle'; targetUserId: UserId; bundle: { identityKey: string; signedPrekey: string; signedPrekeyId: string; signature: string; oneTimePrekey?: string } | null }\n | { type: 'pong' }\n // Live inbox update for the guest's OWN conversation list (widget list socket\n // subscribes via `subscribe_inbox`). `patch` mirrors the agent inbox patch; the\n // list re-fetches on receipt, so only `kind`/`conversationId` are load-bearing.\n | { type: 'inbox_event'; kind: 'new' | 'update'; conversationId: ConversationId; patch?: Record<string, unknown> }\n\n/** Limits referenced by both ends so validation stays consistent. */\nexport const LIMITS = {\n MAX_TEXT_LEN: 8_000,\n MAX_HISTORY_LIMIT: 100,\n DEFAULT_HISTORY: 50,\n} as const\n","// ── @paramms/chat-widget/core — the headless SDK ─────────────────────────────\n// Everything you need to build your OWN chat UI (an in-app messenger, a\n// marketplace inbox, a full chat app) on the Relay protocol, with zero DOM or\n// React dependencies. This is not a new client: it is the exact transport,\n// store, outbox, and E2E machinery the bundled widget AND the agent dashboard\n// run on — re-exported behind a stable boundary, plus a small convenience\n// client for the common case.\n//\n// import { RelayClient } from '@paramms/chat-widget/core'\n//\n// // ONE url, any scheme — wss/ws/http(s) all work; ws + REST derived from it.\n// const relay = new RelayClient({ url: 'https://api.relay.paramms.com', token, profileId: 'p_x' })\n// const convo = relay.open({ subjectId: 'listing_42' }) // support thread\n// const dm = relay.open({ kind: 'direct', peerId: 'user_bob' }) // user↔user (signed identity required)\n// convo.onChange(() => render(convo.store.messages()))\n// convo.send('hello!')\n//\n// For React, see '@paramms/chat-widget/hooks'.\nexport { ConnectionManager, type SocketLike } from './connection.js'\nexport { ChatStore } from './store.js'\nexport { PersistentOutbox } from './outbox.js'\nexport { E2ESession } from './e2e.js'\nexport { restoreHistory, resolveRelayUrls, httpBaseFromWsUrl } from './history.js'\nexport { mountChatList, type ChatListEntry, type ChatListHandle, type ChatListOptions } from './chatlist.js'\nexport { persistentUid } from './uid.js'\nexport * from './protocol/index.js'\n\nimport { ConnectionManager } from './connection.js'\nimport { ChatStore } from './store.js'\nimport type { ClientFrame, ServerFrame, ConversationId, UserId } from './protocol/index.js'\nimport { asUserId } from './protocol/index.js'\nimport { persistentUid } from './uid.js'\nimport { resolveRelayUrls } from './history.js'\n\nexport interface RelayClientOptions {\n /** Relay URL — ONE url, any scheme. `https://api.relay.paramms.com` is the\n * recommended form; the WebSocket URL (`wss://…/ws`) and REST base are\n * derived from it automatically. `wss://`/`ws://`/`http://` also accepted. */\n url: string\n /** HTTP(S) base for REST calls — only when the REST API lives on a\n * DIFFERENT origin than the socket. Normally omit.\n * @deprecated pass a single `url`; kept for back-compat. */\n apiUrl?: string\n /** Identity: a signed JWT (secure), a stable userId (host-vouched), or omit\n * for an anonymous per-browser guest (browser environments only). */\n token?: string\n /** Chatroom id (from the dashboard). Required to open conversations. */\n profileId: string\n}\n\nexport interface OpenOptions {\n /** Support thread scoped to a subject (listing/order/…): one thread per\n * (user, subject). Omit for the profile's single support thread. */\n subjectId?: string\n subjectTitle?: string\n /** User↔user conversation (requires the chatroom to have signed identity\n * and `token` to be a valid signed JWT). */\n kind?: 'direct'\n peerId?: string\n /** Display info persisted for agents (support threads only). */\n user?: { name?: string; email?: string; avatar?: string; meta?: Record<string, string> }\n}\n\n/** One conversation = one connection + one store. Deliberately thin: the\n * store is the source of truth, `onChange` is the render signal, everything\n * else is the same primitives the first-party UIs use. */\nexport class RelayConversation {\n readonly store: ChatStore\n private readonly conn: ConnectionManager\n private readonly listeners = new Set<() => void>()\n private msgSeq = 0\n private _status = 'connecting'\n private _statusMessage: string | undefined\n\n constructor(opts: RelayClientOptions & OpenOptions & { me: UserId }) {\n this.store = new ChatStore(opts.me)\n const open: Extract<ClientFrame, { type: 'open' }> = {\n type: 'open',\n profileId: opts.profileId as never,\n ...(opts.subjectId ? { subjectId: opts.subjectId as never } : {}),\n ...(opts.subjectTitle ? { subjectTitle: opts.subjectTitle } : {}),\n ...(opts.kind === 'direct' ? { kind: 'direct' as const, peerId: asUserId(opts.peerId ?? '') } : {}),\n ...(opts.user ? { userInfo: opts.user } : {}),\n }\n // Accept any scheme (https/http/wss/ws) — a plain `https://api.…` URL is\n // resolved to the concrete `wss://…/ws` socket endpoint, exactly like the\n // bundled widget's mount(). Before this, RelayClient required a raw\n // WebSocket URL while the React components took `https://` — one URL now\n // works across the entire SDK.\n const { wsUrl } = resolveRelayUrls(opts.url, opts.apiUrl)\n this.conn = new ConnectionManager({\n url: wsUrl,\n token: opts.token ?? opts.me,\n open,\n getCursor: () => this.store.highestSeq(),\n onFrame: (f: ServerFrame) => {\n // The server tells us our CANONICAL id on auth (a signed JWT's sub,\n // not the raw token) — capture it so `mine` checks work under every\n // identity tier.\n if (f.type === 'authed') this._me = f.userId as UserId\n this.store.apply(f); this.emit()\n },\n onStatusChange: (s, msg) => { this._status = s; this._statusMessage = msg; this.emit() },\n })\n this.conn.connect()\n }\n\n /** Subscribe to any change (message, typing, status). Returns unsubscribe. */\n onChange(fn: () => void): () => void {\n this.listeners.add(fn)\n return () => this.listeners.delete(fn)\n }\n private emit(): void { for (const fn of this.listeners) fn() }\n\n private _me: UserId | undefined\n /** Our canonical user id as resolved by the server (JWT sub / userId / anon id). */\n get me(): UserId | undefined { return this._me }\n get conversationId(): ConversationId | undefined { return this.store.conversationId }\n get status(): string { return this._status }\n get statusMessage(): string | undefined { return this._statusMessage }\n\n send(text: string): void {\n const clientMsgId = `c_${Date.now().toString(36)}_${++this.msgSeq}`\n const cid = this.store.conversationId\n if (!cid) return\n this.store.addOptimistic(clientMsgId, { kind: 'text', text })\n this.conn.send({ type: 'send', conversationId: cid, clientMsgId, content: { kind: 'text', text } })\n this.emit()\n }\n\n typing(isTyping: boolean, preview?: string): void {\n const cid = this.store.conversationId\n if (!cid) return\n this.conn.send({ type: 'typing', conversationId: cid, isTyping, ...(preview ? { preview } : {}) })\n }\n\n markRead(): void {\n const cid = this.store.conversationId\n if (!cid) return\n this.conn.send({ type: 'read', conversationId: cid, seq: this.store.highestSeq() })\n }\n\n close(): void { this.conn.close(); this.listeners.clear() }\n}\n\nexport class RelayClient {\n constructor(private readonly opts: RelayClientOptions) {}\n\n /** The identity this client will act as: the token's subject (resolved\n * server-side), the raw userId, or a persistent anonymous browser id. */\n me(): UserId {\n return asUserId(this.opts.token ?? persistentUid())\n }\n\n open(open: OpenOptions = {}): RelayConversation {\n return new RelayConversation({ ...this.opts, ...open, me: this.me() })\n }\n}\n"],"names":["TERMINAL_STATES","isTerminalState","state","toManifestAction","a","input","LIMITS","RelayConversation","opts","__publicField","ChatStore","open","asUserId","wsUrl","resolveRelayUrls","ConnectionManager","f","s","msg","fn","text","clientMsgId","cid","isTyping","preview","RelayClient","persistentUid"],"mappings":"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"}
package/dist/e2e.d.ts CHANGED
@@ -47,8 +47,18 @@ export declare class E2ESession {
47
47
  usedOTP: boolean;
48
48
  senderIK: string;
49
49
  }>;
50
- /** X3DH recipient: given an init message's sender IK + EK + SPK ID, derive the shared key. */
51
- x3dhReceiveFrom(senderIKb64: string, ephemeralKeyB64: string, spkId: string): Promise<void>;
50
+ /** X3DH recipient: given an init message's sender IK + EK + SPK ID, derive
51
+ * the shared key. `usedOTP` MUST reflect whether the SENDER actually
52
+ * included a one-time prekey in its DH computation (carried on the wire
53
+ * as `x3dhOTP`, see `X3DHInitFields`) — it must never be inferred from
54
+ * whether we happen to still have OTP keys locally. Popping one
55
+ * unconditionally was the bug here: our OTP pool almost always has spare
56
+ * keys (we upload a batch of 20 and only the sender's own choice consumes
57
+ * one), so we'd derive dh4 against an OTP the sender never included,
58
+ * producing a shared key that doesn't match the sender's — every
59
+ * message would come back "🔒 unable to decrypt" — while also burning a
60
+ * one-time key that was never actually used. */
61
+ x3dhReceiveFrom(senderIKb64: string, ephemeralKeyB64: string, spkId: string, usedOTP: boolean): Promise<void>;
52
62
  /** Flush pending X3DH derivation after initX3DH() completes. */
53
63
  flushPendingX3DH(): Promise<void>;
54
64
  /** Encrypt outgoing text into a wire content object. For X3DH init messages,
@@ -57,6 +67,7 @@ export declare class E2ESession {
57
67
  ephemeralKey: string;
58
68
  spkId: string;
59
69
  senderIK: string;
70
+ usedOTP: boolean;
60
71
  }): Promise<MessageContent>;
61
72
  /** Decrypt one content object if it is encrypted (otherwise pass through). */
62
73
  private openContent;
@@ -69,6 +80,14 @@ export interface X3DHInitFields {
69
80
  x3dhEK: string;
70
81
  x3dhSPK: string;
71
82
  x3dhIK: string;
83
+ /** Whether the sender's DH computation included a one-time prekey (dh4).
84
+ * The receiver MUST honor this exactly — it decides whether to consume
85
+ * one of its own OTP keys, and doing so when the sender didn't include
86
+ * one derives a mismatched shared key (see x3dhReceiveFrom). Absent on
87
+ * messages from a build predating this field: treated as `false`, which
88
+ * is only correct if that sender also never used an OTP — a fresh E2E
89
+ * session on both sides (the normal case) is unaffected either way. */
90
+ x3dhOTP: boolean;
72
91
  }
73
92
  export declare function extractX3DHInit(content: MessageContent): X3DHInitFields | null;
74
93
  export { type X3DHBundle } from './crypto.js';
package/dist/e2e.js CHANGED
@@ -1,8 +1,8 @@
1
- var f = Object.defineProperty;
2
- var x = (i, e, t) => e in i ? f(i, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : i[e] = t;
1
+ var b = Object.defineProperty;
2
+ var x = (i, e, t) => e in i ? b(i, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : i[e] = t;
3
3
  var y = (i, e, t) => x(i, typeof e != "symbol" ? e + "" : e, t);
4
4
  const r = () => globalThis.crypto.subtle;
5
- function P(i) {
5
+ function l(i) {
6
6
  const e = i instanceof Uint8Array ? i : new Uint8Array(i);
7
7
  let t = "";
8
8
  for (const a of e) t += String.fromCharCode(a);
@@ -13,12 +13,12 @@ function g(i) {
13
13
  for (let n = 0; n < e.length; n++) a[n] = e.charCodeAt(n);
14
14
  return a;
15
15
  }
16
- async function l() {
16
+ async function P() {
17
17
  const i = await r().generateKey({ name: "ECDH", namedCurve: "P-256" }, !0, ["deriveKey", "deriveBits"]);
18
18
  return { publicKey: i.publicKey, privateKey: i.privateKey };
19
19
  }
20
- async function h(i) {
21
- return P(await r().exportKey("raw", i));
20
+ async function K(i) {
21
+ return l(await r().exportKey("raw", i));
22
22
  }
23
23
  async function u(i) {
24
24
  return r().importKey("raw", g(i), { name: "ECDH", namedCurve: "P-256" }, !1, []);
@@ -35,7 +35,7 @@ async function C(i, e) {
35
35
  }
36
36
  async function S(i, e) {
37
37
  const t = globalThis.crypto.getRandomValues(new Uint8Array(12)), a = new TextEncoder().encode(e), n = await r().encrypt({ name: "AES-GCM", iv: t }, i, a);
38
- return { ct: P(n), iv: P(t) };
38
+ return { ct: l(n), iv: l(t) };
39
39
  }
40
40
  async function D(i, e, t) {
41
41
  const a = await r().decrypt({ name: "AES-GCM", iv: g(t) }, i, g(e));
@@ -51,7 +51,7 @@ async function E(i) {
51
51
  }
52
52
  } catch {
53
53
  }
54
- const e = await l();
54
+ const e = await P();
55
55
  try {
56
56
  const n = await r().exportKey("jwk", e.publicKey), s = await r().exportKey("jwk", e.privateKey);
57
57
  (a = globalThis.localStorage) == null || a.setItem(i, JSON.stringify({ pub: n, priv: s }));
@@ -61,15 +61,15 @@ async function E(i) {
61
61
  }
62
62
  async function H(i, e) {
63
63
  const t = await r().exportKey("raw", e), a = await r().sign({ name: "ECDSA", hash: "SHA-256" }, i, t);
64
- return P(a);
64
+ return l(a);
65
65
  }
66
- async function A() {
67
- const i = await l(), e = await r().generateKey({ name: "ECDSA", namedCurve: "P-256" }, !0, ["sign", "verify"]);
66
+ async function T() {
67
+ const i = await P(), e = await r().generateKey({ name: "ECDSA", namedCurve: "P-256" }, !0, ["sign", "verify"]);
68
68
  return {
69
69
  ecdhKP: i,
70
70
  ecdsaKP: { publicKey: e.publicKey, privateKey: e.privateKey },
71
- publicKeyB64: await h(i.publicKey),
72
- sigPublicKeyB64: await h(e.publicKey)
71
+ publicKeyB64: await K(i.publicKey),
72
+ sigPublicKeyB64: await K(e.publicKey)
73
73
  };
74
74
  }
75
75
  async function m(i) {
@@ -81,13 +81,13 @@ async function m(i) {
81
81
  return {
82
82
  ecdhKP: { publicKey: c, privateKey: o },
83
83
  ecdsaKP: { publicKey: d, privateKey: p },
84
- publicKeyB64: await h(c),
85
- sigPublicKeyB64: await h(d)
84
+ publicKeyB64: await K(c),
85
+ sigPublicKeyB64: await K(d)
86
86
  };
87
87
  }
88
88
  } catch {
89
89
  }
90
- const e = await A();
90
+ const e = await T();
91
91
  try {
92
92
  const n = await r().exportKey("jwk", e.ecdhKP.publicKey), s = await r().exportKey("jwk", e.ecdhKP.privateKey), c = await r().exportKey("jwk", e.ecdsaKP.publicKey), o = await r().exportKey("jwk", e.ecdsaKP.privateKey);
93
93
  (a = globalThis.localStorage) == null || a.setItem(`${i}-identity`, JSON.stringify({ ecdhPub: n, ecdhPriv: s, ecdsaPub: c, ecdsaPriv: o }));
@@ -95,15 +95,15 @@ async function m(i) {
95
95
  }
96
96
  return e;
97
97
  }
98
- async function I(i, e) {
99
- const t = await l(), a = await h(t.publicKey), n = await u(e.identityKey), s = await u(e.signedPrekey), c = e.oneTimePrekey ? await u(e.oneTimePrekey) : null, o = await K(i.privateKey, s), d = await K(t.privateKey, n), p = await K(t.privateKey, s), w = c ? await K(t.privateKey, c) : null, v = k(o, d, p, ...w ? [w] : []);
100
- return { sharedKey: await b(v), ephemeralPublicKey: a };
98
+ async function A(i, e) {
99
+ const t = await P(), a = await K(t.publicKey), n = await u(e.identityKey), s = await u(e.signedPrekey), c = e.oneTimePrekey ? await u(e.oneTimePrekey) : null, o = await h(i.privateKey, s), d = await h(t.privateKey, n), p = await h(t.privateKey, s), w = c ? await h(t.privateKey, c) : null, v = k(o, d, p, ...w ? [w] : []);
100
+ return { sharedKey: await f(v), ephemeralPublicKey: a };
101
101
  }
102
- async function T(i, e, t, a, n) {
103
- const s = await u(t), c = await u(a), o = await K(e.privateKey, s), d = await K(i.privateKey, c), p = await K(e.privateKey, c), w = n ? await K(n.privateKey, c) : null, v = k(o, d, p, ...w ? [w] : []);
104
- return b(v);
102
+ async function I(i, e, t, a, n) {
103
+ const s = await u(t), c = await u(a), o = await h(e.privateKey, s), d = await h(i.privateKey, c), p = await h(e.privateKey, c), w = n ? await h(n.privateKey, c) : null, v = k(o, d, p, ...w ? [w] : []);
104
+ return f(v);
105
105
  }
106
- async function K(i, e) {
106
+ async function h(i, e) {
107
107
  return r().deriveBits({ name: "ECDH", public: e }, i, 256);
108
108
  }
109
109
  function k(...i) {
@@ -113,7 +113,7 @@ function k(...i) {
113
113
  t.set(new Uint8Array(n), a), a += n.byteLength;
114
114
  return t.buffer;
115
115
  }
116
- async function b(i) {
116
+ async function f(i) {
117
117
  const e = await r().importKey("raw", i, "HKDF", !1, ["deriveKey"]);
118
118
  return r().deriveKey(
119
119
  { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: new TextEncoder().encode("ObjectChat X3DH v1") },
@@ -124,7 +124,7 @@ async function b(i) {
124
124
  );
125
125
  }
126
126
  const j = 20;
127
- class O {
127
+ class X {
128
128
  constructor(e) {
129
129
  // Live ECDH mode state
130
130
  y(this, "kp");
@@ -145,7 +145,7 @@ class O {
145
145
  }
146
146
  /** Live ECDH mode: Generate/restore our keypair and return our public key to publish. */
147
147
  async begin() {
148
- return this.kp = await E(this.storageKey), h(this.kp.publicKey);
148
+ return this.kp = await E(this.storageKey), K(this.kp.publicKey);
149
149
  }
150
150
  /** Live ECDH mode: A peer published their key — derive the shared secret. */
151
151
  async onPeerKey(e) {
@@ -155,9 +155,9 @@ class O {
155
155
  /** X3DH: Generate identity key, signed prekey, and OTP prekeys.
156
156
  * Returns the upload frame payload the caller should send to the server. */
157
157
  async initX3DH() {
158
- this.identityKP = await m(this.storageKey), this.signedPreKP = await l(), this.signedPrekeyId = `spk-${Date.now()}-${Math.random().toString(36).slice(2)}`;
159
- for (let n = 0; n < j; n++) this.otpKeys.push(await l());
160
- const e = await h(this.signedPreKP.publicKey), t = await H(this.identityKP.ecdsaKP.privateKey, this.signedPreKP.publicKey), a = await Promise.all(this.otpKeys.map((n) => h(n.publicKey)));
158
+ this.identityKP = await m(this.storageKey), this.signedPreKP = await P(), this.signedPrekeyId = `spk-${Date.now()}-${Math.random().toString(36).slice(2)}`;
159
+ for (let n = 0; n < j; n++) this.otpKeys.push(await P());
160
+ const e = await K(this.signedPreKP.publicKey), t = await H(this.identityKP.ecdsaKP.privateKey, this.signedPreKP.publicKey), a = await Promise.all(this.otpKeys.map((n) => K(n.publicKey)));
161
161
  return {
162
162
  identityKey: this.identityKP.publicKeyB64,
163
163
  signedPrekey: e,
@@ -170,23 +170,33 @@ class O {
170
170
  * return the init message fields to embed in the first encrypted message. */
171
171
  async x3dhSendTo(e) {
172
172
  this.identityKP || (this.identityKP = await m(this.storageKey));
173
- const { sharedKey: t, ephemeralPublicKey: a } = await I(this.identityKP.ecdhKP, e);
173
+ const { sharedKey: t, ephemeralPublicKey: a } = await A(this.identityKP.ecdhKP, e);
174
174
  return this.x3dhShared = t, { ephemeralKey: a, spkId: e.signedPrekeyId, usedOTP: !!e.oneTimePrekey, senderIK: this.identityKP.publicKeyB64 };
175
175
  }
176
- /** X3DH recipient: given an init message's sender IK + EK + SPK ID, derive the shared key. */
177
- async x3dhReceiveFrom(e, t, a) {
176
+ /** X3DH recipient: given an init message's sender IK + EK + SPK ID, derive
177
+ * the shared key. `usedOTP` MUST reflect whether the SENDER actually
178
+ * included a one-time prekey in its DH computation (carried on the wire
179
+ * as `x3dhOTP`, see `X3DHInitFields`) — it must never be inferred from
180
+ * whether we happen to still have OTP keys locally. Popping one
181
+ * unconditionally was the bug here: our OTP pool almost always has spare
182
+ * keys (we upload a batch of 20 and only the sender's own choice consumes
183
+ * one), so we'd derive dh4 against an OTP the sender never included,
184
+ * producing a shared key that doesn't match the sender's — every
185
+ * message would come back "🔒 unable to decrypt" — while also burning a
186
+ * one-time key that was never actually used. */
187
+ async x3dhReceiveFrom(e, t, a, n) {
178
188
  if (!this.identityKP || !this.signedPreKP) {
179
- this.pendingX3DH = { senderIK: e, ephemeralKey: t, spkId: a };
189
+ this.pendingX3DH = { senderIK: e, ephemeralKey: t, spkId: a, usedOTP: n };
180
190
  return;
181
191
  }
182
- const n = this.otpKeys.shift();
183
- this.x3dhShared = await T(this.identityKP.ecdhKP, this.signedPreKP, e, t, n);
192
+ const s = n ? this.otpKeys.shift() : void 0;
193
+ this.x3dhShared = await I(this.identityKP.ecdhKP, this.signedPreKP, e, t, s);
184
194
  }
185
195
  /** Flush pending X3DH derivation after initX3DH() completes. */
186
196
  async flushPendingX3DH() {
187
197
  if (!this.pendingX3DH) return;
188
- const { senderIK: e, ephemeralKey: t, spkId: a } = this.pendingX3DH;
189
- this.pendingX3DH = void 0, await this.x3dhReceiveFrom(e, t, a);
198
+ const { senderIK: e, ephemeralKey: t, spkId: a, usedOTP: n } = this.pendingX3DH;
199
+ this.pendingX3DH = void 0, await this.x3dhReceiveFrom(e, t, a, n);
190
200
  }
191
201
  /** Encrypt outgoing text into a wire content object. For X3DH init messages,
192
202
  * the caller should pass x3dhInit fields to embed in the content. */
@@ -199,7 +209,7 @@ class O {
199
209
  text: n,
200
210
  enc: !0,
201
211
  iv: s,
202
- ...t ? { x3dhEK: t.ephemeralKey, x3dhSPK: t.spkId, x3dhIK: t.senderIK } : {}
212
+ ...t ? { x3dhEK: t.ephemeralKey, x3dhSPK: t.spkId, x3dhIK: t.senderIK, x3dhOTP: t.usedOTP } : {}
203
213
  };
204
214
  }
205
215
  /** Decrypt one content object if it is encrypted (otherwise pass through). */
@@ -223,10 +233,10 @@ class O {
223
233
  function U(i) {
224
234
  if (i.kind !== "text" || !i.enc) return null;
225
235
  const e = i;
226
- return !e.x3dhEK || !e.x3dhSPK || !e.x3dhIK ? null : { x3dhEK: e.x3dhEK, x3dhSPK: e.x3dhSPK, x3dhIK: e.x3dhIK };
236
+ return !e.x3dhEK || !e.x3dhSPK || !e.x3dhIK ? null : { x3dhEK: e.x3dhEK, x3dhSPK: e.x3dhSPK, x3dhIK: e.x3dhIK, x3dhOTP: e.x3dhOTP ?? !1 };
227
237
  }
228
238
  export {
229
- O as E2ESession,
239
+ X as E2ESession,
230
240
  U as extractX3DHInit
231
241
  };
232
242
  //# sourceMappingURL=e2e.js.map
package/dist/e2e.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"e2e.js","sources":["../src/crypto.ts","../src/e2e.ts"],"sourcesContent":["/**\n * End-to-end encryption primitives (Web Crypto): ECDH P-256 for key agreement\n * + AES-GCM for message content. The server only ever relays public keys and\n * stores ciphertext — it cannot read messages.\n *\n * Scope/limitations (honest): this secures a *live 1:1* session — the guest and\n * one agent exchange public keys while both are connected, then messages between\n * them are encrypted. True asynchronous E2E (encrypting to an offline party)\n * needs a prekey/X3DH scheme, which is out of scope here. When E2E is on, the\n * AI assistant cannot read the room (by design).\n */\n\nconst subtle = (): SubtleCrypto => globalThis.crypto.subtle\n\nfunction b64encode(buf: ArrayBuffer | Uint8Array): string {\n const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf)\n let s = ''\n for (const b of bytes) s += String.fromCharCode(b)\n return btoa(s)\n}\nfunction b64decode(s: string): Uint8Array<ArrayBuffer> {\n const bin = atob(s)\n const buf = new ArrayBuffer(bin.length)\n const out = new Uint8Array(buf)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\nexport interface KeyPair { publicKey: CryptoKey; privateKey: CryptoKey }\n\nexport async function generateKeyPair(): Promise<KeyPair> {\n const kp = await subtle().generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits'])\n return { publicKey: kp.publicKey, privateKey: kp.privateKey }\n}\n\n/** Export a public key to a compact base64 string (raw, 65 bytes for P-256). */\nexport async function exportPublicKey(key: CryptoKey): Promise<string> {\n return b64encode(await subtle().exportKey('raw', key))\n}\n\nasync function importPeerPublicKey(b64: string): Promise<CryptoKey> {\n return subtle().importKey('raw', b64decode(b64), { name: 'ECDH', namedCurve: 'P-256' }, false, [])\n}\n\n/** Derive the shared AES-GCM key from our private key + the peer's public key. */\nexport async function deriveSharedKey(privateKey: CryptoKey, peerPublicKeyB64: string): Promise<CryptoKey> {\n const peer = await importPeerPublicKey(peerPublicKeyB64)\n return subtle().deriveKey(\n { name: 'ECDH', public: peer },\n privateKey,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\nexport interface Ciphertext { ct: string; iv: string }\n\nexport async function encrypt(key: CryptoKey, plaintext: string): Promise<Ciphertext> {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))\n const data = new TextEncoder().encode(plaintext)\n const ct = await subtle().encrypt({ name: 'AES-GCM', iv }, key, data)\n return { ct: b64encode(ct), iv: b64encode(iv) }\n}\n\nexport async function decrypt(key: CryptoKey, ct: string, iv: string): Promise<string> {\n const plain = await subtle().decrypt({ name: 'AES-GCM', iv: b64decode(iv) }, key, b64decode(ct))\n return new TextDecoder().decode(plain)\n}\n\n/** Persist/restore our keypair across reloads (so prior ciphertext stays readable). */\nexport async function loadOrCreateKeyPair(storageKey: string): Promise<KeyPair> {\n try {\n const raw = globalThis.localStorage?.getItem(storageKey)\n if (raw) {\n const { pub, priv } = JSON.parse(raw) as { pub: JsonWebKey; priv: JsonWebKey }\n const publicKey = await subtle().importKey('jwk', pub, { name: 'ECDH', namedCurve: 'P-256' }, true, [])\n const privateKey = await subtle().importKey('jwk', priv, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits'])\n return { publicKey, privateKey }\n }\n } catch { /* fall through to fresh keys */ }\n const kp = await generateKeyPair()\n try {\n const pub = await subtle().exportKey('jwk', kp.publicKey)\n const priv = await subtle().exportKey('jwk', kp.privateKey)\n globalThis.localStorage?.setItem(storageKey, JSON.stringify({ pub, priv }))\n } catch { /* non-persistent environment is fine */ }\n return kp\n}\n\n// ── X3DH async E2E ────────────────────────────────────────────────────────────\n// Extended Triple Diffie-Hellman (X3DH) allows encrypting to an *offline* peer\n// using their published prekey bundle. This enables asynchronous E2E: the sender\n// can encrypt before the recipient connects.\n//\n// Key roles:\n// IK = long-term identity key (ECDH P-256, persistent in localStorage)\n// SPK = signed prekey (ECDH P-256, rotated periodically, server-stored)\n// OPK = one-time prekey (ECDH P-256, single-use pool, server-stored)\n// EK = ephemeral key (ECDH P-256, generated per-message, discarded after)\n//\n// X3DH shared secret = KDF(DH(IK_s, SPK_r) || DH(EK, IK_r) || DH(EK, SPK_r) || DH(EK, OPK_r))\n// Where _s = sender, _r = recipient.\n\n/** Sign a prekey public key bytes using ECDSA P-256 SHA-256.\n * The signingKey must be an ECDSA P-256 private key (not ECDH).\n * In the full X3DH setup the identity key pair contains both an ECDH key\n * (for DH) and an ECDSA key (for signing). We keep them separate here. */\nexport async function signPrekey(signingPrivateKey: CryptoKey, spkPublicKey: CryptoKey): Promise<string> {\n const spkRaw = await subtle().exportKey('raw', spkPublicKey)\n const sig = await subtle().sign({ name: 'ECDSA', hash: 'SHA-256' }, signingPrivateKey, spkRaw)\n return b64encode(sig)\n}\n\n/** Verify an SPK signature. verifyPublicKey must be an ECDSA P-256 public key. */\nexport async function verifyPrekeySignature(verifyPublicKeyB64: string, spkPublicKeyB64: string, signatureB64: string): Promise<boolean> {\n try {\n const verKey = await subtle().importKey('raw', b64decode(verifyPublicKeyB64), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify'])\n return await subtle().verify({ name: 'ECDSA', hash: 'SHA-256' }, verKey, b64decode(signatureB64), b64decode(spkPublicKeyB64))\n } catch { return false }\n}\n\n/** A full identity keypair for X3DH: ECDH key for DH computations + ECDSA key\n * for signing prekeys. The two key objects share the same P-256 curve but have\n * different usages, so Web Crypto treats them separately. */\nexport interface IdentityKeyPair {\n ecdhKP: KeyPair // for DH in X3DH\n ecdsaKP: { publicKey: CryptoKey; privateKey: CryptoKey } // for signing SPKs\n /** The ECDH public key exported as base64 — used as the X3DH identity key. */\n publicKeyB64: string\n /** The ECDSA public key exported as base64 — used for SPK signature verification. */\n sigPublicKeyB64: string\n}\n\n/** Generate a full X3DH identity keypair (ECDH + ECDSA on the same P-256 curve). */\nexport async function generateIdentityKeyPair(): Promise<IdentityKeyPair> {\n const ecdhKP = await generateKeyPair()\n const ecdsaKP = await subtle().generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])\n return {\n ecdhKP,\n ecdsaKP: { publicKey: ecdsaKP.publicKey, privateKey: ecdsaKP.privateKey },\n publicKeyB64: await exportPublicKey(ecdhKP.publicKey),\n sigPublicKeyB64: await exportPublicKey(ecdsaKP.publicKey),\n }\n}\n\n/** Load or generate an identity keypair, persisting both components. */\nexport async function loadOrCreateIdentityKeyPair(storageKey: string): Promise<IdentityKeyPair> {\n try {\n const raw = globalThis.localStorage?.getItem(`${storageKey}-identity`)\n if (raw) {\n const d = JSON.parse(raw) as { ecdhPub: JsonWebKey; ecdhPriv: JsonWebKey; ecdsaPub: JsonWebKey; ecdsaPriv: JsonWebKey }\n const ecdhPub = await subtle().importKey('jwk', d.ecdhPub, { name: 'ECDH', namedCurve: 'P-256' }, true, [])\n const ecdhPriv = await subtle().importKey('jwk', d.ecdhPriv, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits'])\n const ecdsaPub = await subtle().importKey('jwk', d.ecdsaPub, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify'])\n const ecdsaPriv = await subtle().importKey('jwk', d.ecdsaPriv, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign'])\n return {\n ecdhKP: { publicKey: ecdhPub, privateKey: ecdhPriv },\n ecdsaKP: { publicKey: ecdsaPub, privateKey: ecdsaPriv },\n publicKeyB64: await exportPublicKey(ecdhPub),\n sigPublicKeyB64: await exportPublicKey(ecdsaPub),\n }\n }\n } catch { /* generate fresh */ }\n const ikp = await generateIdentityKeyPair()\n try {\n const ecdhPub = await subtle().exportKey('jwk', ikp.ecdhKP.publicKey)\n const ecdhPriv = await subtle().exportKey('jwk', ikp.ecdhKP.privateKey)\n const ecdsaPub = await subtle().exportKey('jwk', ikp.ecdsaKP.publicKey)\n const ecdsaPriv = await subtle().exportKey('jwk', ikp.ecdsaKP.privateKey)\n globalThis.localStorage?.setItem(`${storageKey}-identity`, JSON.stringify({ ecdhPub, ecdhPriv, ecdsaPub, ecdsaPriv }))\n } catch { /* non-persistent ok */ }\n return ikp\n}\n\nexport interface X3DHBundle {\n identityKey: string // base64 raw P-256 public key\n signedPrekey: string // base64 raw P-256 public key\n signedPrekeyId: string // opaque ID for key rotation tracking\n signature: string // base64 ECDSA signature of SPK by IK\n oneTimePrekey?: string // base64 raw P-256 public key (optional)\n}\n\n/** X3DH sender side: derive a shared key from the recipient's prekey bundle.\n * Returns the shared AES-GCM key and the ephemeral public key to transmit. */\nexport async function x3dhSend(\n senderIK: KeyPair,\n recipientBundle: X3DHBundle,\n): Promise<{ sharedKey: CryptoKey; ephemeralPublicKey: string }> {\n const ek = await generateKeyPair()\n const epkB64 = await exportPublicKey(ek.publicKey)\n\n // Import recipient keys for DH.\n const ik_r = await importPeerPublicKey(recipientBundle.identityKey)\n const spk_r = await importPeerPublicKey(recipientBundle.signedPrekey)\n const opk_r = recipientBundle.oneTimePrekey ? await importPeerPublicKey(recipientBundle.oneTimePrekey) : null\n\n // Four DH computations per spec (three if no OPK).\n const dh1 = await rawDH(senderIK.privateKey, spk_r) // DH(IK_s, SPK_r)\n const dh2 = await rawDH(ek.privateKey, ik_r) // DH(EK, IK_r)\n const dh3 = await rawDH(ek.privateKey, spk_r) // DH(EK, SPK_r)\n const dh4 = opk_r ? await rawDH(ek.privateKey, opk_r) : null // DH(EK, OPK_r)\n\n const ikm = concatBuffers(dh1, dh2, dh3, ...(dh4 ? [dh4] : []))\n const sharedKey = await hkdfDeriveKey(ikm)\n\n return { sharedKey, ephemeralPublicKey: epkB64 }\n}\n\n/** X3DH recipient side: rederive the shared key from an init message.\n * Returns the shared AES-GCM key. */\nexport async function x3dhReceive(\n recipientIK: KeyPair,\n recipientSPK: KeyPair,\n senderIKb64: string,\n ephemeralKeyB64: string,\n recipientOPK?: KeyPair,\n): Promise<CryptoKey> {\n const ik_s = await importPeerPublicKey(senderIKb64)\n const ek_s = await importPeerPublicKey(ephemeralKeyB64)\n\n const dh1 = await rawDH(recipientSPK.privateKey, ik_s) // DH(SPK_r, IK_s)\n const dh2 = await rawDH(recipientIK.privateKey, ek_s) // DH(IK_r, EK)\n const dh3 = await rawDH(recipientSPK.privateKey, ek_s) // DH(SPK_r, EK)\n const dh4 = recipientOPK ? await rawDH(recipientOPK.privateKey, ek_s) : null\n\n const ikm = concatBuffers(dh1, dh2, dh3, ...(dh4 ? [dh4] : []))\n return hkdfDeriveKey(ikm)\n}\n\nasync function rawDH(privateKey: CryptoKey, publicKey: CryptoKey): Promise<ArrayBuffer> {\n return subtle().deriveBits({ name: 'ECDH', public: publicKey }, privateKey, 256)\n}\n\nfunction concatBuffers(...bufs: ArrayBuffer[]): ArrayBuffer {\n const total = bufs.reduce((n, b) => n + b.byteLength, 0)\n const out = new Uint8Array(total)\n let offset = 0\n for (const b of bufs) { out.set(new Uint8Array(b), offset); offset += b.byteLength }\n return out.buffer\n}\n\nasync function hkdfDeriveKey(ikm: ArrayBuffer): Promise<CryptoKey> {\n const ikmKey = await subtle().importKey('raw', ikm, 'HKDF', false, ['deriveKey'])\n return subtle().deriveKey(\n { name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), info: new TextEncoder().encode('ObjectChat X3DH v1') },\n ikmKey,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n","import type { MessageContent, UserId } from './protocol/index.js'\nimport type { ServerFrame } from './protocol/index.js'\nimport {\n type KeyPair, loadOrCreateKeyPair, exportPublicKey, deriveSharedKey, encrypt, decrypt,\n generateKeyPair, signPrekey, x3dhSend, x3dhReceive, type X3DHBundle,\n loadOrCreateIdentityKeyPair, type IdentityKeyPair,\n} from './crypto.js'\n\n// Number of one-time prekeys to generate per upload batch.\nconst OTP_BATCH_SIZE = 20\n\n/**\n * Per-user E2E session. Supports two modes:\n *\n * LIVE (original): Both parties are online. ECDH P-256 key exchange via the\n * `pubkey`/`peerkey` frames. Instant but requires both parties to be connected.\n *\n * ASYNC (X3DH): The sender encrypts to the recipient's prekey bundle while the\n * recipient is offline. Uses X3DH (Extended Triple DH) with identity keys,\n * signed prekeys, and one-time prekeys. The recipient derives the same shared\n * key from the init message when they come online.\n *\n * Both modes produce an AES-GCM 256 shared key for message encryption.\n */\nexport class E2ESession {\n // Live ECDH mode state\n private kp?: KeyPair\n private shared?: CryptoKey\n\n // X3DH async mode state\n private identityKP: IdentityKeyPair | undefined = undefined\n private signedPreKP: KeyPair | undefined = undefined\n private signedPrekeyId: string | undefined = undefined\n private readonly otpKeys: KeyPair[] = [] // one-time prekeys awaiting matching\n private x3dhShared?: CryptoKey\n // Queued init messages arriving before we could derive (shouldn't happen, but safe)\n private pendingX3DH: { senderIK: string; ephemeralKey: string; spkId: string } | undefined = undefined\n\n constructor(private readonly storageKey: string) {}\n\n get ready(): boolean { return !!(this.shared ?? this.x3dhShared) }\n\n /** Live ECDH mode: Generate/restore our keypair and return our public key to publish. */\n async begin(): Promise<string> {\n this.kp = await loadOrCreateKeyPair(this.storageKey)\n return exportPublicKey(this.kp.publicKey)\n }\n\n /** Live ECDH mode: A peer published their key — derive the shared secret. */\n async onPeerKey(peerKeyB64: string): Promise<void> {\n if (!this.kp) return\n this.shared = await deriveSharedKey(this.kp.privateKey, peerKeyB64)\n }\n\n // ── X3DH async mode ────────────────────────────────────────────────────────\n\n /** X3DH: Generate identity key, signed prekey, and OTP prekeys.\n * Returns the upload frame payload the caller should send to the server. */\n async initX3DH(): Promise<{\n identityKey: string; signedPrekey: string; signedPrekeyId: string;\n signature: string; oneTimePrekeys: string[]\n }> {\n // Restore or generate persistent identity keypair (ECDH + ECDSA).\n this.identityKP = await loadOrCreateIdentityKeyPair(this.storageKey)\n // Always generate a fresh signed prekey (rotation).\n this.signedPreKP = await generateKeyPair()\n this.signedPrekeyId = `spk-${Date.now()}-${Math.random().toString(36).slice(2)}`\n // Batch of one-time prekeys.\n for (let i = 0; i < OTP_BATCH_SIZE; i++) this.otpKeys.push(await generateKeyPair())\n\n const signedPrekeyPub = await exportPublicKey(this.signedPreKP.publicKey)\n const signature = await signPrekey(this.identityKP.ecdsaKP.privateKey, this.signedPreKP.publicKey)\n const oneTimePrekeys = await Promise.all(this.otpKeys.map(kp => exportPublicKey(kp.publicKey)))\n\n return {\n identityKey: this.identityKP.publicKeyB64,\n signedPrekey: signedPrekeyPub,\n signedPrekeyId: this.signedPrekeyId,\n signature,\n oneTimePrekeys,\n }\n }\n\n /** X3DH sender: given a recipient's prekey bundle, derive the shared key and\n * return the init message fields to embed in the first encrypted message. */\n async x3dhSendTo(bundle: X3DHBundle): Promise<{ ephemeralKey: string; spkId: string; usedOTP: boolean; senderIK: string }> {\n if (!this.identityKP) this.identityKP = await loadOrCreateIdentityKeyPair(this.storageKey)\n const { sharedKey, ephemeralPublicKey } = await x3dhSend(this.identityKP.ecdhKP, bundle)\n this.x3dhShared = sharedKey\n return { ephemeralKey: ephemeralPublicKey, spkId: bundle.signedPrekeyId, usedOTP: !!bundle.oneTimePrekey, senderIK: this.identityKP.publicKeyB64 }\n }\n\n /** X3DH recipient: given an init message's sender IK + EK + SPK ID, derive the shared key. */\n async x3dhReceiveFrom(senderIKb64: string, ephemeralKeyB64: string, spkId: string): Promise<void> {\n if (!this.identityKP || !this.signedPreKP) {\n // Keys not yet initialised — queue for when initX3DH completes.\n this.pendingX3DH = { senderIK: senderIKb64, ephemeralKey: ephemeralKeyB64, spkId }\n return\n }\n // Find OTP key if used (we pop the first available)\n const otp = this.otpKeys.shift()\n this.x3dhShared = await x3dhReceive(this.identityKP.ecdhKP, this.signedPreKP, senderIKb64, ephemeralKeyB64, otp)\n void spkId // we matched by position; full impl would look up by ID\n }\n\n /** Flush pending X3DH derivation after initX3DH() completes. */\n async flushPendingX3DH(): Promise<void> {\n if (!this.pendingX3DH) return\n const { senderIK, ephemeralKey, spkId } = this.pendingX3DH\n this.pendingX3DH = undefined\n await this.x3dhReceiveFrom(senderIK, ephemeralKey, spkId)\n }\n\n /** Encrypt outgoing text into a wire content object. For X3DH init messages,\n * the caller should pass x3dhInit fields to embed in the content. */\n async sealText(text: string, x3dhInit?: { ephemeralKey: string; spkId: string; senderIK: string }): Promise<MessageContent> {\n const key = this.x3dhShared ?? this.shared\n if (!key) throw new Error('secure channel not ready')\n const { ct, iv } = await encrypt(key, text)\n return {\n kind: 'text', text: ct, enc: true, iv,\n ...(x3dhInit ? { x3dhEK: x3dhInit.ephemeralKey, x3dhSPK: x3dhInit.spkId, x3dhIK: x3dhInit.senderIK } as never : {}),\n }\n }\n\n /** Decrypt one content object if it is encrypted (otherwise pass through). */\n private async openContent(content: MessageContent): Promise<MessageContent> {\n if (content.kind !== 'text' || !content.enc || !content.iv) return content\n const key = this.x3dhShared ?? this.shared\n if (!key) return { kind: 'text', text: '🔒 encrypted' }\n try { return { kind: 'text', text: await decrypt(key, content.text, content.iv) } }\n catch { return { kind: 'text', text: '🔒 unable to decrypt' } }\n }\n\n /** Decrypt any encrypted message content carried by an incoming frame, in place. */\n async openFrame(frame: ServerFrame): Promise<void> {\n if (frame.type === 'message') frame.message.content = await this.openContent(frame.message.content)\n else if (frame.type === 'sync') {\n for (const m of frame.messages) m.content = await this.openContent(m.content)\n }\n }\n}\n\n/** X3DH init fields embedded in a text MessageContent (as extra properties).\n * Present only on the very first message from a sender to an offline peer. */\nexport interface X3DHInitFields {\n x3dhEK: string // sender's ephemeral public key (base64)\n x3dhSPK: string // recipient's signed prekey ID used\n x3dhIK: string // sender's identity public key (base64)\n}\n\nexport function extractX3DHInit(content: MessageContent): X3DHInitFields | null {\n if (content.kind !== 'text' || !content.enc) return null\n const c = content as MessageContent & Partial<X3DHInitFields>\n if (!c.x3dhEK || !c.x3dhSPK || !c.x3dhIK) return null\n return { x3dhEK: c.x3dhEK, x3dhSPK: c.x3dhSPK, x3dhIK: c.x3dhIK }\n}\n\nexport { type X3DHBundle } from './crypto.js'\nexport { type UserId }\n\n"],"names":["subtle","b64encode","buf","bytes","s","b","b64decode","bin","out","i","generateKeyPair","kp","exportPublicKey","key","importPeerPublicKey","b64","deriveSharedKey","privateKey","peerPublicKeyB64","peer","encrypt","plaintext","iv","data","ct","decrypt","plain","loadOrCreateKeyPair","storageKey","_a","_b","raw","pub","priv","publicKey","signPrekey","signingPrivateKey","spkPublicKey","spkRaw","sig","generateIdentityKeyPair","ecdhKP","ecdsaKP","loadOrCreateIdentityKeyPair","d","ecdhPub","ecdhPriv","ecdsaPub","ecdsaPriv","ikp","x3dhSend","senderIK","recipientBundle","ek","epkB64","ik_r","spk_r","opk_r","dh1","rawDH","dh2","dh3","dh4","ikm","concatBuffers","hkdfDeriveKey","x3dhReceive","recipientIK","recipientSPK","senderIKb64","ephemeralKeyB64","recipientOPK","ik_s","ek_s","bufs","total","offset","ikmKey","OTP_BATCH_SIZE","E2ESession","__publicField","peerKeyB64","signedPrekeyPub","signature","oneTimePrekeys","bundle","sharedKey","ephemeralPublicKey","spkId","otp","ephemeralKey","text","x3dhInit","content","frame","m","extractX3DHInit","c"],"mappings":"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"}
1
+ {"version":3,"file":"e2e.js","sources":["../src/crypto.ts","../src/e2e.ts"],"sourcesContent":["/**\n * End-to-end encryption primitives (Web Crypto): ECDH P-256 for key agreement\n * + AES-GCM for message content. The server only ever relays public keys and\n * stores ciphertext — it cannot read messages.\n *\n * Scope/limitations (honest): this secures a *live 1:1* session — the guest and\n * one agent exchange public keys while both are connected, then messages between\n * them are encrypted. True asynchronous E2E (encrypting to an offline party)\n * needs a prekey/X3DH scheme, which is out of scope here. When E2E is on, the\n * AI assistant cannot read the room (by design).\n */\n\nconst subtle = (): SubtleCrypto => globalThis.crypto.subtle\n\nfunction b64encode(buf: ArrayBuffer | Uint8Array): string {\n const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf)\n let s = ''\n for (const b of bytes) s += String.fromCharCode(b)\n return btoa(s)\n}\nfunction b64decode(s: string): Uint8Array<ArrayBuffer> {\n const bin = atob(s)\n const buf = new ArrayBuffer(bin.length)\n const out = new Uint8Array(buf)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n}\n\nexport interface KeyPair { publicKey: CryptoKey; privateKey: CryptoKey }\n\nexport async function generateKeyPair(): Promise<KeyPair> {\n const kp = await subtle().generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits'])\n return { publicKey: kp.publicKey, privateKey: kp.privateKey }\n}\n\n/** Export a public key to a compact base64 string (raw, 65 bytes for P-256). */\nexport async function exportPublicKey(key: CryptoKey): Promise<string> {\n return b64encode(await subtle().exportKey('raw', key))\n}\n\nasync function importPeerPublicKey(b64: string): Promise<CryptoKey> {\n return subtle().importKey('raw', b64decode(b64), { name: 'ECDH', namedCurve: 'P-256' }, false, [])\n}\n\n/** Derive the shared AES-GCM key from our private key + the peer's public key. */\nexport async function deriveSharedKey(privateKey: CryptoKey, peerPublicKeyB64: string): Promise<CryptoKey> {\n const peer = await importPeerPublicKey(peerPublicKeyB64)\n return subtle().deriveKey(\n { name: 'ECDH', public: peer },\n privateKey,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\nexport interface Ciphertext { ct: string; iv: string }\n\nexport async function encrypt(key: CryptoKey, plaintext: string): Promise<Ciphertext> {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))\n const data = new TextEncoder().encode(plaintext)\n const ct = await subtle().encrypt({ name: 'AES-GCM', iv }, key, data)\n return { ct: b64encode(ct), iv: b64encode(iv) }\n}\n\nexport async function decrypt(key: CryptoKey, ct: string, iv: string): Promise<string> {\n const plain = await subtle().decrypt({ name: 'AES-GCM', iv: b64decode(iv) }, key, b64decode(ct))\n return new TextDecoder().decode(plain)\n}\n\n/** Persist/restore our keypair across reloads (so prior ciphertext stays readable). */\nexport async function loadOrCreateKeyPair(storageKey: string): Promise<KeyPair> {\n try {\n const raw = globalThis.localStorage?.getItem(storageKey)\n if (raw) {\n const { pub, priv } = JSON.parse(raw) as { pub: JsonWebKey; priv: JsonWebKey }\n const publicKey = await subtle().importKey('jwk', pub, { name: 'ECDH', namedCurve: 'P-256' }, true, [])\n const privateKey = await subtle().importKey('jwk', priv, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits'])\n return { publicKey, privateKey }\n }\n } catch { /* fall through to fresh keys */ }\n const kp = await generateKeyPair()\n try {\n const pub = await subtle().exportKey('jwk', kp.publicKey)\n const priv = await subtle().exportKey('jwk', kp.privateKey)\n globalThis.localStorage?.setItem(storageKey, JSON.stringify({ pub, priv }))\n } catch { /* non-persistent environment is fine */ }\n return kp\n}\n\n// ── X3DH async E2E ────────────────────────────────────────────────────────────\n// Extended Triple Diffie-Hellman (X3DH) allows encrypting to an *offline* peer\n// using their published prekey bundle. This enables asynchronous E2E: the sender\n// can encrypt before the recipient connects.\n//\n// Key roles:\n// IK = long-term identity key (ECDH P-256, persistent in localStorage)\n// SPK = signed prekey (ECDH P-256, rotated periodically, server-stored)\n// OPK = one-time prekey (ECDH P-256, single-use pool, server-stored)\n// EK = ephemeral key (ECDH P-256, generated per-message, discarded after)\n//\n// X3DH shared secret = KDF(DH(IK_s, SPK_r) || DH(EK, IK_r) || DH(EK, SPK_r) || DH(EK, OPK_r))\n// Where _s = sender, _r = recipient.\n\n/** Sign a prekey public key bytes using ECDSA P-256 SHA-256.\n * The signingKey must be an ECDSA P-256 private key (not ECDH).\n * In the full X3DH setup the identity key pair contains both an ECDH key\n * (for DH) and an ECDSA key (for signing). We keep them separate here. */\nexport async function signPrekey(signingPrivateKey: CryptoKey, spkPublicKey: CryptoKey): Promise<string> {\n const spkRaw = await subtle().exportKey('raw', spkPublicKey)\n const sig = await subtle().sign({ name: 'ECDSA', hash: 'SHA-256' }, signingPrivateKey, spkRaw)\n return b64encode(sig)\n}\n\n/** Verify an SPK signature. verifyPublicKey must be an ECDSA P-256 public key. */\nexport async function verifyPrekeySignature(verifyPublicKeyB64: string, spkPublicKeyB64: string, signatureB64: string): Promise<boolean> {\n try {\n const verKey = await subtle().importKey('raw', b64decode(verifyPublicKeyB64), { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify'])\n return await subtle().verify({ name: 'ECDSA', hash: 'SHA-256' }, verKey, b64decode(signatureB64), b64decode(spkPublicKeyB64))\n } catch { return false }\n}\n\n/** A full identity keypair for X3DH: ECDH key for DH computations + ECDSA key\n * for signing prekeys. The two key objects share the same P-256 curve but have\n * different usages, so Web Crypto treats them separately. */\nexport interface IdentityKeyPair {\n ecdhKP: KeyPair // for DH in X3DH\n ecdsaKP: { publicKey: CryptoKey; privateKey: CryptoKey } // for signing SPKs\n /** The ECDH public key exported as base64 — used as the X3DH identity key. */\n publicKeyB64: string\n /** The ECDSA public key exported as base64 — used for SPK signature verification. */\n sigPublicKeyB64: string\n}\n\n/** Generate a full X3DH identity keypair (ECDH + ECDSA on the same P-256 curve). */\nexport async function generateIdentityKeyPair(): Promise<IdentityKeyPair> {\n const ecdhKP = await generateKeyPair()\n const ecdsaKP = await subtle().generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])\n return {\n ecdhKP,\n ecdsaKP: { publicKey: ecdsaKP.publicKey, privateKey: ecdsaKP.privateKey },\n publicKeyB64: await exportPublicKey(ecdhKP.publicKey),\n sigPublicKeyB64: await exportPublicKey(ecdsaKP.publicKey),\n }\n}\n\n/** Load or generate an identity keypair, persisting both components. */\nexport async function loadOrCreateIdentityKeyPair(storageKey: string): Promise<IdentityKeyPair> {\n try {\n const raw = globalThis.localStorage?.getItem(`${storageKey}-identity`)\n if (raw) {\n const d = JSON.parse(raw) as { ecdhPub: JsonWebKey; ecdhPriv: JsonWebKey; ecdsaPub: JsonWebKey; ecdsaPriv: JsonWebKey }\n const ecdhPub = await subtle().importKey('jwk', d.ecdhPub, { name: 'ECDH', namedCurve: 'P-256' }, true, [])\n const ecdhPriv = await subtle().importKey('jwk', d.ecdhPriv, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits'])\n const ecdsaPub = await subtle().importKey('jwk', d.ecdsaPub, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify'])\n const ecdsaPriv = await subtle().importKey('jwk', d.ecdsaPriv, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign'])\n return {\n ecdhKP: { publicKey: ecdhPub, privateKey: ecdhPriv },\n ecdsaKP: { publicKey: ecdsaPub, privateKey: ecdsaPriv },\n publicKeyB64: await exportPublicKey(ecdhPub),\n sigPublicKeyB64: await exportPublicKey(ecdsaPub),\n }\n }\n } catch { /* generate fresh */ }\n const ikp = await generateIdentityKeyPair()\n try {\n const ecdhPub = await subtle().exportKey('jwk', ikp.ecdhKP.publicKey)\n const ecdhPriv = await subtle().exportKey('jwk', ikp.ecdhKP.privateKey)\n const ecdsaPub = await subtle().exportKey('jwk', ikp.ecdsaKP.publicKey)\n const ecdsaPriv = await subtle().exportKey('jwk', ikp.ecdsaKP.privateKey)\n globalThis.localStorage?.setItem(`${storageKey}-identity`, JSON.stringify({ ecdhPub, ecdhPriv, ecdsaPub, ecdsaPriv }))\n } catch { /* non-persistent ok */ }\n return ikp\n}\n\nexport interface X3DHBundle {\n identityKey: string // base64 raw P-256 public key\n signedPrekey: string // base64 raw P-256 public key\n signedPrekeyId: string // opaque ID for key rotation tracking\n signature: string // base64 ECDSA signature of SPK by IK\n oneTimePrekey?: string // base64 raw P-256 public key (optional)\n}\n\n/** X3DH sender side: derive a shared key from the recipient's prekey bundle.\n * Returns the shared AES-GCM key and the ephemeral public key to transmit. */\nexport async function x3dhSend(\n senderIK: KeyPair,\n recipientBundle: X3DHBundle,\n): Promise<{ sharedKey: CryptoKey; ephemeralPublicKey: string }> {\n const ek = await generateKeyPair()\n const epkB64 = await exportPublicKey(ek.publicKey)\n\n // Import recipient keys for DH.\n const ik_r = await importPeerPublicKey(recipientBundle.identityKey)\n const spk_r = await importPeerPublicKey(recipientBundle.signedPrekey)\n const opk_r = recipientBundle.oneTimePrekey ? await importPeerPublicKey(recipientBundle.oneTimePrekey) : null\n\n // Four DH computations per spec (three if no OPK).\n const dh1 = await rawDH(senderIK.privateKey, spk_r) // DH(IK_s, SPK_r)\n const dh2 = await rawDH(ek.privateKey, ik_r) // DH(EK, IK_r)\n const dh3 = await rawDH(ek.privateKey, spk_r) // DH(EK, SPK_r)\n const dh4 = opk_r ? await rawDH(ek.privateKey, opk_r) : null // DH(EK, OPK_r)\n\n const ikm = concatBuffers(dh1, dh2, dh3, ...(dh4 ? [dh4] : []))\n const sharedKey = await hkdfDeriveKey(ikm)\n\n return { sharedKey, ephemeralPublicKey: epkB64 }\n}\n\n/** X3DH recipient side: rederive the shared key from an init message.\n * Returns the shared AES-GCM key. */\nexport async function x3dhReceive(\n recipientIK: KeyPair,\n recipientSPK: KeyPair,\n senderIKb64: string,\n ephemeralKeyB64: string,\n recipientOPK?: KeyPair,\n): Promise<CryptoKey> {\n const ik_s = await importPeerPublicKey(senderIKb64)\n const ek_s = await importPeerPublicKey(ephemeralKeyB64)\n\n const dh1 = await rawDH(recipientSPK.privateKey, ik_s) // DH(SPK_r, IK_s)\n const dh2 = await rawDH(recipientIK.privateKey, ek_s) // DH(IK_r, EK)\n const dh3 = await rawDH(recipientSPK.privateKey, ek_s) // DH(SPK_r, EK)\n const dh4 = recipientOPK ? await rawDH(recipientOPK.privateKey, ek_s) : null\n\n const ikm = concatBuffers(dh1, dh2, dh3, ...(dh4 ? [dh4] : []))\n return hkdfDeriveKey(ikm)\n}\n\nasync function rawDH(privateKey: CryptoKey, publicKey: CryptoKey): Promise<ArrayBuffer> {\n return subtle().deriveBits({ name: 'ECDH', public: publicKey }, privateKey, 256)\n}\n\nfunction concatBuffers(...bufs: ArrayBuffer[]): ArrayBuffer {\n const total = bufs.reduce((n, b) => n + b.byteLength, 0)\n const out = new Uint8Array(total)\n let offset = 0\n for (const b of bufs) { out.set(new Uint8Array(b), offset); offset += b.byteLength }\n return out.buffer\n}\n\nasync function hkdfDeriveKey(ikm: ArrayBuffer): Promise<CryptoKey> {\n const ikmKey = await subtle().importKey('raw', ikm, 'HKDF', false, ['deriveKey'])\n return subtle().deriveKey(\n { name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), info: new TextEncoder().encode('ObjectChat X3DH v1') },\n ikmKey,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n","import type { MessageContent, UserId } from './protocol/index.js'\nimport type { ServerFrame } from './protocol/index.js'\nimport {\n type KeyPair, loadOrCreateKeyPair, exportPublicKey, deriveSharedKey, encrypt, decrypt,\n generateKeyPair, signPrekey, x3dhSend, x3dhReceive, type X3DHBundle,\n loadOrCreateIdentityKeyPair, type IdentityKeyPair,\n} from './crypto.js'\n\n// Number of one-time prekeys to generate per upload batch.\nconst OTP_BATCH_SIZE = 20\n\n/**\n * Per-user E2E session. Supports two modes:\n *\n * LIVE (original): Both parties are online. ECDH P-256 key exchange via the\n * `pubkey`/`peerkey` frames. Instant but requires both parties to be connected.\n *\n * ASYNC (X3DH): The sender encrypts to the recipient's prekey bundle while the\n * recipient is offline. Uses X3DH (Extended Triple DH) with identity keys,\n * signed prekeys, and one-time prekeys. The recipient derives the same shared\n * key from the init message when they come online.\n *\n * Both modes produce an AES-GCM 256 shared key for message encryption.\n */\nexport class E2ESession {\n // Live ECDH mode state\n private kp?: KeyPair\n private shared?: CryptoKey\n\n // X3DH async mode state\n private identityKP: IdentityKeyPair | undefined = undefined\n private signedPreKP: KeyPair | undefined = undefined\n private signedPrekeyId: string | undefined = undefined\n private readonly otpKeys: KeyPair[] = [] // one-time prekeys awaiting matching\n private x3dhShared?: CryptoKey\n // Queued init messages arriving before we could derive (shouldn't happen, but safe)\n private pendingX3DH: { senderIK: string; ephemeralKey: string; spkId: string; usedOTP: boolean } | undefined = undefined\n\n constructor(private readonly storageKey: string) {}\n\n get ready(): boolean { return !!(this.shared ?? this.x3dhShared) }\n\n /** Live ECDH mode: Generate/restore our keypair and return our public key to publish. */\n async begin(): Promise<string> {\n this.kp = await loadOrCreateKeyPair(this.storageKey)\n return exportPublicKey(this.kp.publicKey)\n }\n\n /** Live ECDH mode: A peer published their key — derive the shared secret. */\n async onPeerKey(peerKeyB64: string): Promise<void> {\n if (!this.kp) return\n this.shared = await deriveSharedKey(this.kp.privateKey, peerKeyB64)\n }\n\n // ── X3DH async mode ────────────────────────────────────────────────────────\n\n /** X3DH: Generate identity key, signed prekey, and OTP prekeys.\n * Returns the upload frame payload the caller should send to the server. */\n async initX3DH(): Promise<{\n identityKey: string; signedPrekey: string; signedPrekeyId: string;\n signature: string; oneTimePrekeys: string[]\n }> {\n // Restore or generate persistent identity keypair (ECDH + ECDSA).\n this.identityKP = await loadOrCreateIdentityKeyPair(this.storageKey)\n // Always generate a fresh signed prekey (rotation).\n this.signedPreKP = await generateKeyPair()\n this.signedPrekeyId = `spk-${Date.now()}-${Math.random().toString(36).slice(2)}`\n // Batch of one-time prekeys.\n for (let i = 0; i < OTP_BATCH_SIZE; i++) this.otpKeys.push(await generateKeyPair())\n\n const signedPrekeyPub = await exportPublicKey(this.signedPreKP.publicKey)\n const signature = await signPrekey(this.identityKP.ecdsaKP.privateKey, this.signedPreKP.publicKey)\n const oneTimePrekeys = await Promise.all(this.otpKeys.map(kp => exportPublicKey(kp.publicKey)))\n\n return {\n identityKey: this.identityKP.publicKeyB64,\n signedPrekey: signedPrekeyPub,\n signedPrekeyId: this.signedPrekeyId,\n signature,\n oneTimePrekeys,\n }\n }\n\n /** X3DH sender: given a recipient's prekey bundle, derive the shared key and\n * return the init message fields to embed in the first encrypted message. */\n async x3dhSendTo(bundle: X3DHBundle): Promise<{ ephemeralKey: string; spkId: string; usedOTP: boolean; senderIK: string }> {\n if (!this.identityKP) this.identityKP = await loadOrCreateIdentityKeyPair(this.storageKey)\n const { sharedKey, ephemeralPublicKey } = await x3dhSend(this.identityKP.ecdhKP, bundle)\n this.x3dhShared = sharedKey\n return { ephemeralKey: ephemeralPublicKey, spkId: bundle.signedPrekeyId, usedOTP: !!bundle.oneTimePrekey, senderIK: this.identityKP.publicKeyB64 }\n }\n\n /** X3DH recipient: given an init message's sender IK + EK + SPK ID, derive\n * the shared key. `usedOTP` MUST reflect whether the SENDER actually\n * included a one-time prekey in its DH computation (carried on the wire\n * as `x3dhOTP`, see `X3DHInitFields`) — it must never be inferred from\n * whether we happen to still have OTP keys locally. Popping one\n * unconditionally was the bug here: our OTP pool almost always has spare\n * keys (we upload a batch of 20 and only the sender's own choice consumes\n * one), so we'd derive dh4 against an OTP the sender never included,\n * producing a shared key that doesn't match the sender's — every\n * message would come back \"🔒 unable to decrypt\" — while also burning a\n * one-time key that was never actually used. */\n async x3dhReceiveFrom(senderIKb64: string, ephemeralKeyB64: string, spkId: string, usedOTP: boolean): Promise<void> {\n if (!this.identityKP || !this.signedPreKP) {\n // Keys not yet initialised — queue for when initX3DH completes.\n this.pendingX3DH = { senderIK: senderIKb64, ephemeralKey: ephemeralKeyB64, spkId, usedOTP }\n return\n }\n // Only consume an OTP when the sender's own message says it used one.\n const otp = usedOTP ? this.otpKeys.shift() : undefined\n this.x3dhShared = await x3dhReceive(this.identityKP.ecdhKP, this.signedPreKP, senderIKb64, ephemeralKeyB64, otp)\n void spkId // we matched by position; full impl would look up by ID\n }\n\n /** Flush pending X3DH derivation after initX3DH() completes. */\n async flushPendingX3DH(): Promise<void> {\n if (!this.pendingX3DH) return\n const { senderIK, ephemeralKey, spkId, usedOTP } = this.pendingX3DH\n this.pendingX3DH = undefined\n await this.x3dhReceiveFrom(senderIK, ephemeralKey, spkId, usedOTP)\n }\n\n /** Encrypt outgoing text into a wire content object. For X3DH init messages,\n * the caller should pass x3dhInit fields to embed in the content. */\n async sealText(text: string, x3dhInit?: { ephemeralKey: string; spkId: string; senderIK: string; usedOTP: boolean }): Promise<MessageContent> {\n const key = this.x3dhShared ?? this.shared\n if (!key) throw new Error('secure channel not ready')\n const { ct, iv } = await encrypt(key, text)\n return {\n kind: 'text', text: ct, enc: true, iv,\n ...(x3dhInit ? { x3dhEK: x3dhInit.ephemeralKey, x3dhSPK: x3dhInit.spkId, x3dhIK: x3dhInit.senderIK, x3dhOTP: x3dhInit.usedOTP } as never : {}),\n }\n }\n\n /** Decrypt one content object if it is encrypted (otherwise pass through). */\n private async openContent(content: MessageContent): Promise<MessageContent> {\n if (content.kind !== 'text' || !content.enc || !content.iv) return content\n const key = this.x3dhShared ?? this.shared\n if (!key) return { kind: 'text', text: '🔒 encrypted' }\n try { return { kind: 'text', text: await decrypt(key, content.text, content.iv) } }\n catch { return { kind: 'text', text: '🔒 unable to decrypt' } }\n }\n\n /** Decrypt any encrypted message content carried by an incoming frame, in place. */\n async openFrame(frame: ServerFrame): Promise<void> {\n if (frame.type === 'message') frame.message.content = await this.openContent(frame.message.content)\n else if (frame.type === 'sync') {\n for (const m of frame.messages) m.content = await this.openContent(m.content)\n }\n }\n}\n\n/** X3DH init fields embedded in a text MessageContent (as extra properties).\n * Present only on the very first message from a sender to an offline peer. */\nexport interface X3DHInitFields {\n x3dhEK: string // sender's ephemeral public key (base64)\n x3dhSPK: string // recipient's signed prekey ID used\n x3dhIK: string // sender's identity public key (base64)\n /** Whether the sender's DH computation included a one-time prekey (dh4).\n * The receiver MUST honor this exactly — it decides whether to consume\n * one of its own OTP keys, and doing so when the sender didn't include\n * one derives a mismatched shared key (see x3dhReceiveFrom). Absent on\n * messages from a build predating this field: treated as `false`, which\n * is only correct if that sender also never used an OTP — a fresh E2E\n * session on both sides (the normal case) is unaffected either way. */\n x3dhOTP: boolean\n}\n\nexport function extractX3DHInit(content: MessageContent): X3DHInitFields | null {\n if (content.kind !== 'text' || !content.enc) return null\n const c = content as MessageContent & Partial<X3DHInitFields>\n if (!c.x3dhEK || !c.x3dhSPK || !c.x3dhIK) return null\n return { x3dhEK: c.x3dhEK, x3dhSPK: c.x3dhSPK, x3dhIK: c.x3dhIK, x3dhOTP: c.x3dhOTP ?? false }\n}\n\nexport { type X3DHBundle } from './crypto.js'\nexport { type UserId }\n\n"],"names":["subtle","b64encode","buf","bytes","s","b","b64decode","bin","out","i","generateKeyPair","kp","exportPublicKey","key","importPeerPublicKey","b64","deriveSharedKey","privateKey","peerPublicKeyB64","peer","encrypt","plaintext","iv","data","ct","decrypt","plain","loadOrCreateKeyPair","storageKey","_a","_b","raw","pub","priv","publicKey","signPrekey","signingPrivateKey","spkPublicKey","spkRaw","sig","generateIdentityKeyPair","ecdhKP","ecdsaKP","loadOrCreateIdentityKeyPair","d","ecdhPub","ecdhPriv","ecdsaPub","ecdsaPriv","ikp","x3dhSend","senderIK","recipientBundle","ek","epkB64","ik_r","spk_r","opk_r","dh1","rawDH","dh2","dh3","dh4","ikm","concatBuffers","hkdfDeriveKey","x3dhReceive","recipientIK","recipientSPK","senderIKb64","ephemeralKeyB64","recipientOPK","ik_s","ek_s","bufs","total","offset","ikmKey","OTP_BATCH_SIZE","E2ESession","__publicField","peerKeyB64","signedPrekeyPub","signature","oneTimePrekeys","bundle","sharedKey","ephemeralPublicKey","spkId","usedOTP","otp","ephemeralKey","text","x3dhInit","content","frame","m","extractX3DHInit","c"],"mappings":"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"}