/** * The tzmesh test double (XTX-14; the contract's Verification section): one * in-memory service answering the transport type the package's client * takes, so a program's tests construct the unchanged client over * `double.transport` and the client's own wire mapping runs in them * (FTR-L0-104). The double answers the twenty-one calls and the stream * (XTX-01), the refusals the contract states, carried as * `TZMESH_DOUBLE_REFUSALS`, delivery at once to a waiting read or an open * stream and the queue for an absent member (XTX-07; XTX-08), an agent's * listeners with each conversation's assignment to one of them (XTX-06), the * reservations, the ceilings as counts in fixed windows (XTX-11), and presence on the * clock the test gives it (XTX-12). It mints each participant secret in the * form XTX-02 states, and a held read or an open stream ends at the caller's * abort signal, the hold dropped and presence following (XTX-14). It * records every call it received with its status and a credential by its * kind alone, and it returns to its constructed state on `reset()`. * * The record (XTX-L0-04): accounts with their handle or none, their two * choices, their reserved names, and a changed-away handle * held for them; participants with their handles and their last call, a * session's and an agent's secrets each one of its listeners'; * channels, each one append-only log with a cursor per member, a direct * conversation naming the listener each of its agents answers it through. * One level admits every call to every credential of the right * kind (XTX-02), but an application's credential, a token minted at an * application's sign-in and the human secret answered to `human` under one, * is refused the owner's acts, `resume`, and `attach` (XTX-02; XTX-03; * XTX-16; XTX-21). * A change of handle renames every record the account owns, so a read names * each sender and subject by its current handle (XTX-21). * * Seams beyond the wire serve a test (XTX-14): the `guestDoor` option shuts * the guest door, `guest` then answering 404 with no body as the service's * wire does while its configuration keeps the door shut (XTX-16); the * `address` option is the network address each call comes from, which the * per-address bound on guests reads, as the `now` option is the clock; * `account` seeds an account * and its public switch, with its handle or, `adopted` false, with none; * `mintToken` answers a token for an account, accepting and ignoring a * level, and with `application_sign_in` true marks it an application's * sign-in, the signature the service would verify left to the service; * `signIn` stands in for the site's sign-in leg, minting the account's human * participant the marked secret a change of handle takes; `dropConnections` * ends every open stream and waiting read, and forgets what reads answered, * as a restart would; `calls` is * the record; `reset` the return. * * What the double leaves to the operated service is the contract's * Verification section's list, one sentence per limit: no signature and no * revocation list, no journal and no memory window, no image re-encode, * no image served and no copy of one, rates counted in one process, no keep-alive and no TLS, no * erasure, and no ranking of the directory's matches. */ import { AUTHORIZATION_HEADER, BEARER_SCHEME, TZMESH_GLOBAL_NAME_PATTERN, TZMESH_CALLS, TZMESH_CALL_CREDENTIALS, TZMESH_CHANNEL_NAME_PATTERN, TZMESH_CONSTANTS, TZMESH_CREDENTIAL_PREFIXES, TZMESH_GUEST_CLIENT_ID, TZMESH_NAME_PATTERN, TZMESH_PATH_PREFIX, TZMESH_REFUSALS, TZMESH_REPORT_CLIENT_ID_PREFIX, TZMESH_SECRET_PREFIX, TZMESH_STREAM_PATH, TZMESH_SYSTEM_EVENTS, TZMESH_WEBHOOK_DELIVERY_HEADER, TZMESH_WEBHOOK_EVENT_HEADER, TZMESH_WEBHOOK_SECRET_PREFIX, TZMESH_WEBHOOK_SIGNATURE_HEADER, signTzmeshWebhook, type TzmeshAccount, type TzmeshAdmits, type TzmeshCall, type TzmeshChannel, type TzmeshChannelMode, type TzmeshConstants, type TzmeshConversation, type TzmeshEvent, type TzmeshImageReference, type TzmeshLevel, type TzmeshListenerRow, type TzmeshMessage, type TzmeshNameCheck, type TzmeshNameVerdict, type TzmeshParticipant, type TzmeshPresence, type TzmeshRefusalName, type TzmeshStreamAddress, type TzmeshSystemEvent, type TzmeshTransport, type TzmeshTransportInit, type TzmeshTransportResponse, type TzmeshVisibility, } from './tzmesh_client.js'; import { tzmeshChallengeOf, TZMESH_APPLICATION_SCOPE, type TzmeshSignInFetch } from './sign_in.js'; /** The double's refusal table: the contract's names at their statuses * (XTX-01), one spelling with the client's. */ export const TZMESH_DOUBLE_REFUSALS = TZMESH_REFUSALS; export interface TzmeshDoubleOptions { /** The clock, milliseconds since the epoch; `Date.now` where absent. */ now?: () => number; /** Constants overriding the contract's (XTX-11), so a test scales a rate. */ constants?: Partial; /** The company's account handle, whose names every other account is * refused (XTX-03); `turnzero` where absent. */ company?: string; /** The names the company reserves beside its own handle, each refused to * every other account with its look-alikes (XTX-09). */ companyNames?: string[]; /** The fetch-shaped function the double's webhook deliveries POST through * (XTX-18), so a test hands it a receiver; where absent, every delivery * fails as an unreachable address would. */ webhookFetch?: TzmeshWebhookFetch; /** The guest door (XTX-16): open, where absent, mints a guest for any * caller; false answers `guest` 404 with no body, the answer the * service's wire gives while its configuration keeps the door shut and * the gate stands. */ guestDoor?: boolean; /** The network address each call comes from, read at the call (XTX-16): * one address mints at most `guestsPerAddressPerDay` guests in a UTC * day, an IPv6 address counted by its /64 prefix. Where absent, no * address is counted. */ address?: () => string; /** The name check's checker (#7522's decision 6), which the test gives: * asked, after every other refusal of a public channel's `create` and of * a public channel's `channel.set` naming a description, for its * verdict, an approval letting the call stand and a refusal answering * `name_refused` with its reason. Where absent, every such call stands * unchecked. The double approves or refuses alone and never answers * `unavailable` (XTX-13), which the service's own suite proves. */ channelNameChecker?: (check: TzmeshNameCheck) => TzmeshNameVerdict | Promise; } /** What the double's webhook deliveries call (XTX-18): the address, and a * POST with its headers and its JSON body; the answer's status alone is * read, a 2xx being a delivery. */ export type TzmeshWebhookFetch = (url: string, init: { method: 'POST'; headers: Record; body: string }) => Promise<{ status: number }>; /** One recorded call (XTX-14): the path, the credential's kind, the status * answered, and the refusal name where one was. */ export interface TzmeshDoubleCall { path: string; credential: 'token' | 'secret' | 'none' | 'unknown'; status: number; refusal?: TzmeshRefusalName; } export interface TzmeshDouble { /** The transport the unchanged client is handed. */ readonly transport: TzmeshTransport; /** Seed an account with its handle and its public switch, or set the * switch of one that stands. With `adopted` false the account has adopted * no handle: `name` is its id, its messaging calls are refused * `handle_required`, and `account.set` gives it its first handle (XTX-21); * its handle reads null. A name finds an account by its id first, then by * its current handle. */ account(name: string, options?: { public?: boolean; unlisted?: boolean; adopted?: boolean }): { handle: string | null; public: boolean; unlisted: boolean }; /** Mint a token for an account (XTX-02), answered once. A level named is * accepted and ignored, since one level admits every call; the account * is named by its id or its current handle, and a name no account holds * seeds one that has adopted it. `application_sign_in` true marks the * token as the plane marks one minted at an application's sign-in: the * owner's acts and `resume` refuse it, and the human secret `human` * answers under it, which the owner's acts refuse too (XTX-14; XTX-17). */ mintToken(request: { account: string; level?: TzmeshLevel; label?: string; application_sign_in?: true }): string; /** The site's sign-in leg stood in (XTX-16; XTX-21): the account's human * participant, minted where none stands, and a fresh secret marked as the * service's own sign-in marks it, the one credential that changes or * releases the account's handle. */ signIn(account: string): { participant: TzmeshParticipant; secret: string }; /** End every open stream and waiting read, and forget what reads * answered, as a restart would (XTX-07). */ dropConnections(): number; /** Run the webhook deliveries that are due at the double's clock (XTX-18): * for each participant whose webhook stands and whose next attempt has * come, POST each entry past its cursor in order through `webhookFetch`, * acknowledging each one a 2xx answered and stopping at the first that * failed, its next attempt after the backoff. Answers the count * delivered. The double delivers at this seam alone, so a test owns * when a delivery happens. */ deliverWebhooks(): Promise; /** The calls received, in order. */ readonly calls: TzmeshDoubleCall[]; /** The constants the double runs under. */ readonly constants: TzmeshConstants; /** The plane's sign-in stood in (XTX-17). */ readonly plane: TzmeshPlaneDouble; /** Return to the constructed state. */ reset(): void; } /** The plane's sign-in stood in for the sign-in module (XTX-17): `fetch` * answers the authorization-server metadata, the client registration, and * the token endpoint at any origin, and `consent` stands in for the person * at the authorization address, answering the callback address the plane * would return their browser to. The token the exchange answers is one * `mintToken` minted, so the unchanged client spends it on the double. */ export interface TzmeshPlaneDouble { readonly fetch: TzmeshSignInFetch; /** The person at the authorization address: signs in as `account` and * allows, or cancels. An application's request, one naming the scope * `tzmesh.application`, is given `chat` under ` at `, its * token carrying `application_sign_in`; a tool's is given the one level * the plane writes, `admin`, under the client's registered name, with no * such mark, repeatable (XTX-17; cloud:MCP-02). A `level` * and a `label` are accepted and ignored, since the person chooses * neither. */ consent(authorizationUrl: string, person: { account: string; level?: 'chat' | 'admin'; label?: string; cancel?: boolean }): string; /** The clients registered, in order. */ readonly clients: ReadonlyArray<{ clientId: string; name: string; redirectUris: string[] }>; } // --------------------------------------------------------------------------- // The record. /** An account (XTX-21). `handle` is the key every record's owner names: the * account's handle once it has adopted one, its id until then. */ interface Account { id: string; handle: string; adopted: boolean; public: boolean; unlisted: boolean; reserved: Set; /** A changed-away handle still the account's, and the instant its hold * ends, judged by the clock of the call that reads it (XTX-21). */ held?: { handle: string; until: number }; /** The id of the account's own agent a message to its handle reaches in * place of its person (XTX-21). */ answeredBy?: string; /** The id of the account's default channel, `/#all`, once made * (XTX-05). */ all?: string; } interface Participant { id: string; kind: 'session' | 'standing' | 'human'; handle: string; global: boolean; description: string; owner: string; owner_visibility: 'public' | 'private'; host?: string; host_session?: string; website?: string; social?: string[]; image?: TzmeshImageReference; tags?: string[]; /** A session's or an agent's free text (XTX-04). */ title?: string; /** A session's or an agent's own visibility (XTX-03), private unless its * owner set it public; never set on a person. */ visibility?: TzmeshVisibility; /** The id of the private channel its owner named as its team at its * register (XTX-03; XTX-L0-09). */ team?: string; /** Who may start a conversation with a public agent (XTX-04; XTX-06): * `signed_in` or `team`, or `everyone` where absent; never set on a * session. */ admits?: Exclude; /** An agent's capacity, unbounded where absent, and whether its queue is * off, on where absent (XTX-04; XTX-06). */ capacity?: number; queueOff?: boolean; /** A one-way agent's sentence: every message to it is refused `one_way` * with it; absent where the agent takes messages (XTX-04; XTX-06). */ oneWay?: string; /** A guest's or a human's latest secret; a session's and an agent's * secrets are its listeners'. */ secret: string; /** A session's or an agent's listeners (XTX-02; XTX-L0-02), and the * number the next one takes. */ listeners: Listener[]; nextListener: number; lastCall: number; /** The instants of the participant's last `read` that answered or * `ack`, and of its last taken `send`, where one stands (XTX-04; * #7314): an agent's listeners each set the agent's own. */ lastRead?: number; lastSend?: number; ended: boolean; endedAt?: number; /** An agent its owner's end deleted (XTX-03): no attach or owner's end * reaches it again. */ deleted?: boolean; holds: number; /** A guest of the site (XTX-16): a human participant of no account. */ guest: boolean; } /** One listener of a session or an agent (XTX-02; XTX-L0-02; the * simplified plan's decision 6): a program's, under a program secret, which * may hold a webhook and revives at its next call after its end, or a * session's, under a session secret, which its end removes; the account * whose token attached it; its own last call and holds. */ interface Listener { id: string; kind: 'program' | 'session'; secret: string; account: string; lastCall: number; ended: boolean; holds: number; /** The listener's name to its owner (XTX-09; the route A plan's decision * 20): the `label` its register or attach carried, or the host's name * where it carried none; and the instant that call made it. */ label?: string; createdAt?: string; /** The listener an agent's register made, until the first call under its * own secret: the owner's register is no call under it, so the agent * reads away and the listener is live to no assignment (XTX-12; XTX-09). */ unheard?: true; /** A program listener an owner's attach made, until the first call under * its own secret: its owner's row reads no `last_call`, the agent reads * no call of it, and it is live to no assignment, as `unheard` (XTX-09; * XTX-12; the route A plan's decision 20; #6706). The attach still sets * the agent's own last call, so an ended agent it revives reads away. */ fresh?: true; /** A program's webhook (XTX-18): the address, the signing secret, the * failed attempts in a row, and the next attempt's instant. */ webhook?: { url: string; secret: string; failures: number; nextAttemptAt: number }; } /** One live secret of a human participant (XTX-16): one per browser, ended * by its own `end` or by `humanSecretIdleMs` unused. */ interface HumanSecret { lastUsed: number; ended: boolean; /** Minted by the site's sign-in leg (the `signIn` seam), never by a * `human` call: the one secret that changes or releases a handle. */ marked: boolean; /** Answered to `human` under a token carrying `application_sign_in`: an * application's credential, which `account.set` refuses in every form. * Distinct from `marked` and never set with it (XTX-16; XTX-21). */ application: boolean; } interface Channel { id: string; kind: 'direct' | 'private' | 'public'; name?: string; description?: string; owner: string; /** The participant whose `create` or first `send` made the channel, absent * for an account's default channel: where its owner_visibility is private, * the channel answers the creator's handle as its owner (XTX-04; XTX-05). */ creator?: string; public_join?: boolean; created_at: string; members: Map; invited: Set; log: TzmeshMessage[]; /** In a direct conversation, the listener each member an agent answers it * through, by the member's id (XTX-06): an entry naming no attached * listener reads as none, the conversation then read by every listener. */ listeners?: Map; lastMessageAt: number; company: boolean; /** An account's default channel, `/#all` (XTX-05): its joinings * and leavings post no system message, so a sibling's register or end * wakes no read. */ isDefault?: boolean; /** `announce` where set, open where absent, so a channel that stands keeps * its behaviour; and the members `channel.set` named, by id, in its order, * a member that leaves dropped (XTX-05). */ mode?: 'announce'; posters?: string[]; /** A public channel's named moderators, by id, in its order (#7522): each * may remove a message or a member; a member that leaves drops. */ moderators?: string[]; /** A public channel its owner took out of the directory by channel.set's * `listed` false (#7904): the list and a query leave it out, and the * exact read still answers it; its members and a join by its name or id * stand. Absent is listed. */ unlisted?: true; /** The accounts remove_member put out of the channel, by the account's * handle (#7522): the end, as an instant and in milliseconds, 0 for good, * the reason, and when it was set. */ bars?: Map; } /** A token names its account by the account's id, so it follows the account * through a change of handle. */ interface Token { accountId: string; label: string; /** Minted at an application's sign-in (XTX-17; cloud:MCP-02): the * owner's acts, `resume`, and `attach` refuse it. */ applicationSignIn: boolean; } interface Waiter { participant: string; /** The listener the read holds for, absent for a person and a guest. */ listener?: string; channels: Set | null; wake: () => void; } interface OpenStream { participant: string; /** The listener the stream is, absent for a person and a guest. */ listener?: string; push: (event: TzmeshEvent) => void; end: () => void; } class Refusal extends Error { constructor(readonly refusal: TzmeshRefusalName, readonly detail: string) { super(detail); } } function refuse(refusal: TzmeshRefusalName, detail: string): never { throw new Refusal(refusal, detail); } /** The sentences a session listener another session's attach replaced is * refused with at every later call and its held read (XTX-02; XTX-03), the * successor named by its attaching account and the way on, a session of * its own, as the service words them (#6000). */ const replacedSentence = (account: string): string => `This agent is now served by another session (a session of ${account}). You no longer serve it; do not run the hold line again. To message again, register a session of your own.`; const encoder = new TextEncoder(); const byteLength = (text: string): number => encoder.encode(text).length; /** A participant secret in the contract's form (XTX-02): the prefix and 43 * base64url characters from 32 random bytes. */ const mintSecret = (prefix: string = TZMESH_SECRET_PREFIX): string => { const bytes = new Uint8Array(32); globalThis.crypto.getRandomValues(bytes); const base64 = btoa(String.fromCharCode(...bytes)); return `${prefix}${base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')}`; }; /** The origin of the wake URL the double answers (XTX-09): a reserved name * that never resolves (RFC 2606), since the double serves no wait, so a * test that runs its hold line reaches nothing. */ const DOUBLE_WAKE_ORIGIN = 'https://tzmesh.invalid'; /** The wait's path the wake URL names, before the participant's id and the * listener's (XTX-09; the tzmesh member's TZT-L0-05). */ const DOUBLE_WAIT_PATH = '/wait-for-messages/'; /** The hold's bound in the line the double answers, production's four * hours, the curl's own limit two minutes past it, as the service's. */ const DOUBLE_HOLD_SECONDS = 14400; /** The origin of the socket address the double answers in `stream` * (XTX-09): the wake URL's reserved origin with the `wss` scheme, which * never resolves, since the double serves no socket; the path before the * participant's id and the listener's; and the one protocol the address is * offered with (the tzmesh member's TZT-L0-05). */ const DOUBLE_STREAM_ORIGIN = 'wss://tzmesh.invalid'; const DOUBLE_LISTEN_PATH = '/listen/'; const DOUBLE_STREAM_PROTOCOL = 'tzmesh.v1'; /** The bound on the owner's list of one participant's direct conversations * (XTX-16), the service's own: the rest stands in the account's export * (XTX-10). */ const CONVERSATIONS_MAX = 200; /** A stored image (XTX-19): its reference, the account whose store it * counts against, and its bytes as uploaded, which the double keeps * without re-encoding. */ interface StoredImage { id: string; account: string; bytes: number; width: number; height: number; data: Uint8Array; } /** An upload's format and size read from its header, as the service's * decoder reads them (XTX-19): PNG, JPEG, or WebP, and whether it is an * animation; undefined where the bytes are none of the three or do not * read. */ const readImageHeader = (b: Uint8Array): { width: number; height: number; animated: boolean } | undefined => { const u16be = (at: number): number => (b[at] << 8) | b[at + 1]; const u32be = (at: number): number => ((b[at] << 24) >>> 0) + ((b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3]); const u32le = (at: number): number => ((b[at + 3] << 24) >>> 0) + ((b[at + 2] << 16) | (b[at + 1] << 8) | b[at]); const ascii = (at: number, n: number): string => String.fromCharCode(...b.subarray(at, at + n)); if (b.length >= 24 && b[0] === 0x89 && ascii(1, 3) === 'PNG') { let animated = false; for (let at = 8; at + 8 <= b.length; ) { const kind = ascii(at + 4, 4); if (kind === 'acTL') animated = true; if (kind === 'IDAT') break; at += 12 + u32be(at); } return { width: u32be(16), height: u32be(20), animated }; } if (b.length >= 4 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) { for (let at = 2; at + 9 <= b.length; ) { if (b[at] !== 0xff) return undefined; const marker = b[at + 1]; if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) return { width: u16be(at + 7), height: u16be(at + 5), animated: false }; at += 2 + u16be(at + 2); } return undefined; } if (b.length >= 30 && ascii(0, 4) === 'RIFF' && ascii(8, 4) === 'WEBP') { let animated = false; for (let at = 12; at + 8 <= b.length; ) { const kind = ascii(at, 4); if (kind === 'ANIM' || kind === 'ANMF') animated = true; const length = u32le(at + 4); at += 8 + length + (length % 2); } const first = ascii(12, 4); if (first === 'VP8X') return { width: 1 + (b[24] | (b[25] << 8) | (b[26] << 16)), height: 1 + (b[27] | (b[28] << 8) | (b[29] << 16)), animated: animated || (b[20] & 0x02) !== 0 }; if (first === 'VP8L') { const bits = u32le(21); return { width: (bits & 0x3fff) + 1, height: ((bits >>> 14) & 0x3fff) + 1, animated }; } if (first === 'VP8 ') return { width: (b[26] | (b[27] << 8)) & 0x3fff, height: (b[28] | (b[29] << 8)) & 0x3fff, animated }; } return undefined; }; /** The size an image takes once its long side is at most `longSide`, the * short side rounded and at least one, as the service scales it (XTX-19). */ const scaledSize = (width: number, height: number, longSide: number): { width: number; height: number } => { const long = Math.max(width, height); if (long <= longSide) return { width, height }; return { width: Math.max(1, Math.floor((width * longSide + Math.floor(long / 2)) / long)), height: Math.max(1, Math.floor((height * longSide + Math.floor(long / 2)) / long)) }; }; /** Whether a webhook's address is an `https` address the service would * POST to (XTX-18): a host, no user name or password, no fragment. */ const webhookAddressAdmitted = (value: string, max: number): boolean => { if (value.length > max) return false; let parsed: URL; try { parsed = new URL(value); } catch { return false; } return parsed.protocol === 'https:' && parsed.hostname !== '' && parsed.username === '' && parsed.password === '' && parsed.hash === '' && !value.includes('#'); }; /** A name's look-alike form (XTX-09): lower case, hyphens dropped, and the * digits 0, 1, 3, and 5 read as the letters o, l, e, and s. */ const lookAlike = (name: string): string => name.toLowerCase().replace(/-/g, '').replace(/0/g, 'o').replace(/1/g, 'l').replace(/3/g, 'e').replace(/5/g, 's'); const isPlainObject = (value: unknown): value is Record => value !== null && typeof value === 'object' && !Array.isArray(value); const jsonResponse = (status: number, body: unknown): TzmeshTransportResponse => ({ status, ok: status >= 200 && status < 300, headers: { get: (name: string) => (name.toLowerCase() === 'content-type' ? 'application/json' : null) }, text: async () => JSON.stringify(body), body: null, }); /** The SSE frame of one event (XTX-08). */ export const eventFrame = (event: TzmeshEvent): string => `event: ${event.event}\nid: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`; /** An async queue of text chunks that a stream's body iterates (XTX-08). */ class ChunkQueue implements AsyncIterable { private readonly chunks: string[] = []; private waiting: (() => void) | null = null; private ended = false; push(chunk: string): void { if (this.ended) return; this.chunks.push(chunk); this.waiting?.(); } end(): void { this.ended = true; this.waiting?.(); } async *[Symbol.asyncIterator](): AsyncIterator { for (;;) { if (this.chunks.length > 0) { yield this.chunks.shift() as string; continue; } if (this.ended) return; await new Promise((resolve) => { this.waiting = () => { this.waiting = null; resolve(); }; }); } } } /** Construct the double (XTX-14). */ export function createTzmeshDouble(options: TzmeshDoubleOptions = {}): TzmeshDouble { const now = options.now ?? (() => Date.now()); const constants: TzmeshConstants = { ...TZMESH_CONSTANTS, ...(options.constants ?? {}) }; const company = options.company ?? 'turnzero'; const companyNames = new Set([company, ...(options.companyNames ?? [])]); const companyLookAlikes = new Set([...companyNames].map(lookAlike)); let accounts = new Map(); let tokens = new Map(); let participants = new Map(); let secrets = new Map(); // Each session listener another session's attach replaced, by its secret, // to the handle of the account that attached its successor (XTX-02; XTX-03). let replaced = new Map(); let channels = new Map(); let waiters = new Set(); // For each reader, the participant or an agent's listener, and each // channel, the newest sequence number a read answered it (XTX-07): a held // read answers at once only where it carries a message past that mark. let answered = new Map>(); let streams = new Set(); let rates = new Map(); let humanSecrets = new Map(); let storedImages = new Map(); let counter = 0; const calls: TzmeshDoubleCall[] = []; // The plane's sign-in stood in (XTX-17), its tokens this double's own. const plane = createPlaneStandIn((request) => mintToken(request)); const nextId = (prefix: string): string => `${prefix}_${(++counter).toString(36).padStart(6, '0')}`; const instant = (): string => new Date(now()).toISOString(); const reset = (): void => { for (const stream of streams) stream.end(); for (const waiter of waiters) waiter.wake(); accounts = new Map(); tokens = new Map(); participants = new Map(); secrets = new Map(); replaced = new Map(); channels = new Map(); waiters = new Set(); answered = new Map(); streams = new Set(); rates = new Map(); humanSecrets = new Map(); storedImages = new Map(); counter = 0; calls.length = 0; plane.reset(); }; // A level is accepted and ignored: one level admits every call (XTX-02). // `application_sign_in` is written only as true, and its absence reads // as false (XTX-17). function mintToken({ account, label, application_sign_in }: { account: string; level?: TzmeshLevel; label?: string; application_sign_in?: true }): string { const held = accountNamed(account); const value = nextId('tok'); tokens.set(value, { accountId: held.id, label: label ?? 'test', applicationSignIn: application_sign_in === true }); return value; } /** The account whose handle, or whose id until it adopts one, is `key`: * the owner every record names. */ const accountByKey = (key: string): Account | undefined => [...accounts.values()].find((a) => a.handle === key); /** The account a seam names, by its id first and its current handle then, * seeded as an account that has adopted the name where none is. */ const accountNamed = (name: string): Account => accounts.get(name) ?? accountByKey(name) ?? accountOf(name); const accountOf = (handle: string): Account => { let account = accountByKey(handle); if (!account) { account = { id: handle, handle, adopted: true, public: false, unlisted: false, reserved: new Set() }; accounts.set(account.id, account); } return account; }; /** The account a token names (XTX-02). */ const tokenAccount = (token: Token): Account => accounts.get(token.accountId) as Account; /** A held handle's account while the hold stands at this call's clock * (XTX-21); a read deletes nothing. */ const heldBy = (name: string): Account | undefined => { const lower = name.toLowerCase(); return [...accounts.values()].find((a) => a.held !== undefined && a.held.until > now() && a.held.handle.toLowerCase() === lower); }; // --- presence (XTX-12) --------------------------------------------------- const listenerWebhookHealthy = (l: Listener): boolean => l.webhook !== undefined && !l.ended && l.webhook.failures < constants.webhookFailingAfter; const webhookHealthy = (p: Participant): boolean => p.listeners.some(listenerWebhookHealthy); /** Whether a listener's key has made no call of its own yet: the one an * agent's register made or a program's attach made, until the first call * under its secret (XTX-12; XTX-09; #6706). */ const silentListener = (l: Listener): boolean => l.unheard === true || l.fresh === true; /** The instant of a participant's last call under any of its listeners * (XTX-12): an agent's is the latest among the listeners it holds, a * program listener its own end detached among them, and 0 where none * stands, so an agent whose listeners the owner's end removed reads away, * never active; a listener whose key has made no call yet, the one its * register or a program's attach made, counts none (#6706); every other * participant's is its own. */ const lastListenerCall = (p: Participant): number => { if (p.kind !== 'standing') return p.lastCall; return p.listeners.reduce((last, l) => (!silentListener(l) && l.lastCall > last ? l.lastCall : last), 0); }; /** Whether a participant holds a stream or a waiting read (XTX-12): an * agent by its listeners' holds, so a listener the owner's end removed * counts no more, though its held read has yet to return; every other * participant by its own count. */ const heldConnection = (p: Participant): boolean => (p.kind === 'standing' ? p.listeners.some((l) => l.holds > 0) : p.holds > 0); const presenceOf = (p: Participant): TzmeshPresence => { const at = now(); if (p.ended) return 'ended'; // A registered webhook not failing past its bound is a held connection, // whatever the silence, an agent's when any listener's is (XTX-12; // XTX-18; XTX-L0-06). // A stream or a waiting read is a held connection the same way, so a // participant that only holds one past the day's silence reads // connected, as the service checks it (XTX-12; #6271). if (webhookHealthy(p) || heldConnection(p)) return 'connected'; if (p.kind !== 'human' && at - p.lastCall >= constants.endedAfterMs) return 'ended'; // A guest ends after `guestIdleMs` with no call; an account's human // participant never ends by silence (XTX-12; XTX-16). if (p.guest && at - p.lastCall >= constants.guestIdleMs) return 'ended'; if (at - lastListenerCall(p) < constants.activeWindowMs) return 'active'; return 'away'; }; const participantRecord = (p: Participant, viewer?: Participant): TzmeshParticipant => { const record: TzmeshParticipant = { id: p.id, kind: p.kind, handle: p.handle, description: p.description, owner: p.owner, owner_visibility: p.owner_visibility, presence: presenceOf(p), }; if (p.title !== undefined) record.title = p.title; if (p.kind === 'standing' || p.kind === 'session') record.visibility = visibilityOf(p); if (p.kind === 'standing') { record.admits = p.admits ?? 'everyone'; // An agent's capacity, where set, and its queue (XTX-04; XTX-06). record.queue = p.queueOff !== true; if (p.capacity !== undefined) record.capacity = p.capacity; // A one-way agent's sentence, where set (XTX-04). if (p.oneWay !== undefined) record.one_way = p.oneWay; } const own = viewer !== undefined && viewer.owner === p.owner; // The team is answered to the owner's own participants and token alone // (XTX-04; XTX-L0-09). if (p.team !== undefined && own) record.team = p.team; // An independent agent stands to the public as its own entity: a // private owner is answered to its own account alone (XTX-04). if (p.owner_visibility === 'private' && !own) delete record.owner; if (p.host !== undefined) record.host = p.host; if (p.host_session !== undefined && viewer !== undefined && viewer.owner === p.owner) record.host_session = p.host_session; // The last read and the last send, where one stands, to the owner's own // participants and token alone, as host_session (XTX-04; #7314). if (viewer !== undefined && viewer.owner === p.owner) { if (p.lastRead !== undefined) record.last_read = new Date(p.lastRead).toISOString(); if (p.lastSend !== undefined) record.last_send = new Date(p.lastSend).toISOString(); } if (p.website !== undefined) record.website = p.website; if (p.social !== undefined) record.social = [...p.social]; if (p.image !== undefined) record.image = { ...p.image }; if (p.tags !== undefined) record.tags = [...p.tags]; if (p.guest) record.guest = true; const hooked = p.listeners.find((l) => l.webhook !== undefined); if (hooked?.webhook !== undefined && viewer !== undefined && viewer.owner === p.owner) record.webhook = { url: hooked.webhook.url, failures: hooked.webhook.failures }; return record; }; /** The owner a channel shows every reader: the account's handle, or the * creating participant's where it keeps its owner private (XTX-04). */ const channelOwner = (c: Channel): string => { const creator = c.creator === undefined ? undefined : participants.get(c.creator); return creator !== undefined && creator.owner_visibility === 'private' ? creator.handle : c.owner; }; const channelRecord = (c: Channel): TzmeshChannel => { const record: TzmeshChannel = { id: c.id, kind: c.kind, owner: channelOwner(c), members: c.members.size, created_at: c.created_at, // The mode alone, open where none was set; members and channel.set // answer the posters (XTX-05). mode: c.mode ?? 'open', }; if (c.name !== undefined) record.name = c.name; if (c.description !== undefined) record.description = c.description; if (c.kind === 'public') { record.public_join = c.public_join ?? true; // Whether the directory lists it, its owner's choice (#7904). record.listed = c.unlisted !== true; } return record; }; // --- delivery (XTX-07; XTX-08) ------------------------------------------- const eventId = (channel: string, seq: number): string => `${channel}:${seq}`; // A participant's own message is never delivered back to it as new // (XTX-07; XTX-08): the stream carries no message of its own sending, and // a waiting read is not woken by one. const ownMessage = (event: TzmeshEvent, participant: string): boolean => event.event === 'message' && event.data.message.sender?.id === participant; // A message in the log, a system message among them, wakes a waiting read // of its channel; the other events wake no read, since a read answers // messages alone (XTX-07). const wakesRead = (event: TzmeshEvent): boolean => event.event === 'message' || (TZMESH_SYSTEM_EVENTS as readonly string[]).includes(event.event); // A member's streams and waiting reads, of an agent's the listener its // conversation sticks to, or each of them where it sticks to none // (reachesListener; XTX-06). const notify = (channel: Channel, event: TzmeshEvent, memberIds: Iterable): void => { const members = new Set(memberIds); for (const stream of streams) if (members.has(stream.participant) && !ownMessage(event, stream.participant) && reachesListener(channel, stream.participant, stream.listener)) stream.push(event); for (const waiter of waiters) { if (!members.has(waiter.participant)) continue; if (waiter.channels !== null && !waiter.channels.has(channel.id)) continue; if (!wakesRead(event) || ownMessage(event, waiter.participant)) continue; if (!reachesListener(channel, waiter.participant, waiter.listener)) continue; waiter.wake(); } }; /** Store one message in the channel's log without telling anyone. */ const store = (channel: Channel, message: Omit): TzmeshMessage => { const seq = channel.log.length + 1; const stored: TzmeshMessage = { id: nextId('msg'), channel: channel.id, seq, at: instant(), ...message }; channel.log.push(stored); channel.lastMessageAt = now(); return stored; }; const append = (channel: Channel, message: Omit): TzmeshMessage => { const stored = store(channel, message); notify(channel, { event: 'message', id: eventId(channel.id, stored.seq), data: { message: stored, channel: channelRecord(channel) } }, channel.members.keys()); return stored; }; /** The event a system message streams as (XTX-08): its own, `joined`, * `left`, `ended`, or `answering`, carrying the message, once, and never * as `message`. */ const systemEvent = (channel: Channel, message: TzmeshMessage): TzmeshEvent => ({ event: message.event as TzmeshSystemEvent, id: eventId(channel.id, message.seq), data: { channel: channel.id, participant: message.subject as { id: string; handle: string }, message }, }); const systemMessage = (channel: Channel, event: TzmeshSystemEvent, subject: Pick, extra: Partial> = {}, toSubject = false): void => { const stored = store(channel, { kind: 'system', mentions: [], event, subject: { id: subject.id, handle: subject.handle }, ...extra }); // One event per system message, and none to its own subject, whose // joining, leaving, end, or answering is no news to it (XTX-05; XTX-08); // a message_removed notice reaches its subject, the sender it tells why // (#7522). notify(channel, systemEvent(channel, stored), [...channel.members.keys()].filter((id) => toSubject || id !== subject.id)); }; /** Drop a member from a channel's named posters and moderators, as a * member that leaves or is removed drops (XTX-05; #7522). */ const dropNamed = (channel: Channel, id: string): void => { if (channel.posters?.includes(id)) { const kept = channel.posters.filter((held) => held !== id); if (kept.length > 0) channel.posters = kept; else delete channel.posters; } if (channel.moderators?.includes(id)) { const kept = channel.moderators.filter((held) => held !== id); if (kept.length > 0) channel.moderators = kept; else delete channel.moderators; } }; const addMember = (channel: Channel, p: Participant): void => { if (channel.members.has(p.id)) return; channel.members.set(p.id, channel.log.length); // An account's default channel posts no joining (XTX-05). if (!channel.isDefault) systemMessage(channel, 'joined', p); // The cursor starts at the channel's newest, the member's own joining // included: its joining is no news to it (XTX-05). channel.members.set(p.id, channel.log.length); }; const removeMember = (channel: Channel, p: Participant): void => { if (!channel.members.has(p.id)) return; if (!channel.isDefault) systemMessage(channel, 'left', p); channel.members.delete(p.id); // A member that leaves drops from the named posters and moderators // (XTX-05; #7522). dropNamed(channel, p.id); }; // --- credentials (XTX-02) ------------------------------------------------ // A caller under a token, under a participant secret, or under no // credential at all, which `guest` and the public readings of `directory` // and `read` admit (XTX-02; XTX-16). type Caller = { kind: 'token'; token: Token; value: string } | { kind: 'secret'; participant: Participant; value: string; listener?: Listener } | { kind: 'anonymous' }; type Credentialed = Exclude; const readCredential = (init: TzmeshTransportInit): { caller: Caller | null; recorded: TzmeshDoubleCall['credential']; replacedBy?: string } => { const header = init.headers?.[AUTHORIZATION_HEADER] ?? init.headers?.Authorization; if (header === undefined || header === '') return { caller: null, recorded: 'none' }; const value = header.startsWith(`${BEARER_SCHEME} `) ? header.slice(BEARER_SCHEME.length + 1) : header; const token = tokens.get(value); if (token) return { caller: { kind: 'token', token, value }, recorded: 'token' }; const participantId = secrets.get(value); const participant = participantId === undefined ? undefined : participants.get(participantId); if (participant) { // A session's and an agent's secret is one of its listeners' (XTX-02). if (participant.kind === 'human' || participant.guest) return { caller: { kind: 'secret', participant, value }, recorded: 'secret' }; const listener = participant.listeners.find((l) => l.secret === value); if (listener !== undefined) return { caller: { kind: 'secret', participant, value, listener }, recorded: 'secret' }; } // A session listener another session's attach replaced is answered with // the replacement's sentence (XTX-02). const successor = replaced.get(value); if (successor !== undefined) return { caller: null, recorded: 'unknown', replacedBy: successor }; return { caller: null, recorded: 'unknown' }; }; const admit = (call: TzmeshCall | 'stream', caller: Caller | null, recorded: TzmeshDoubleCall['credential'], replacedBy?: string): Caller => { const takes = TZMESH_CALL_CREDENTIALS[call]; // `guest` reads no credential at all (XTX-16). if (takes === 'none') return { kind: 'anonymous' }; if (recorded === 'none') { if (takes === 'open') return { kind: 'anonymous' }; refuse('credential_required', `${call} needs a credential, and this call carried none: send an authorization header of Bearer followed by a token or the participant secret register answered`); } if ((caller === null || caller.kind === 'anonymous') && replacedBy !== undefined) refuse('credential_refused', replacedSentence(replacedBy)); if (caller === null || caller.kind === 'anonymous') refuse('credential_refused', `the service does not accept this credential as a live token or a live participant secret: sign in again for a fresh token, or call register for a new participant secret`); if (caller.kind === 'secret') { const p = caller.participant; // An account's human participant holds one secret per browser, each // ended by its own `end` or by `humanSecretIdleMs` unused (XTX-16). const held = humanSecrets.get(caller.value); if (held !== undefined && (held.ended || now() - held.lastUsed >= constants.humanSecretIdleMs)) refuse('credential_refused', 'this sign-in secret has ended, closed by its end or unused too long: sign in again for a new one'); if ((p.ended || presenceOf(p) === 'ended') && call !== 'end') { // A session ended by its silence alone carries on at its own resume, // so the detail names it (#7345); one its end call ended is told to // register anew. if (p.kind === 'session' && !p.ended) refuse('credential_refused', `this session has ended after a long silence, so its secret no longer works: call register with resume set to ${p.id} to carry on as it, or call register again for a new session`); if (p.kind === 'session') refuse('credential_refused', 'this session has ended by its own end call, so its secret no longer works: call register again for a new session'); if (p.guest) refuse('credential_refused', 'this guest has ended, so its secret no longer works: call guest again to start as a new guest'); p.ended = false; } const l = caller.listener; if (l !== undefined) { // A session listener of an agent ends by its silence as a session // does; a program listener its end detached revives at its next call // (XTX-03). if (p.kind === 'standing' && listenerLapsed(l) && call !== 'end') refuse('credential_refused', "this listener of the agent ended after a long silence, so its secret no longer works: call register with attach set to the agent's handle to serve it again"); if (l.ended && call !== 'end') l.ended = false; } if (held !== undefined) held.lastUsed = now(); } // One level admits every call to every credential of the right kind // (XTX-02); the owner's acts read the credential's kind at the call. if (takes === 'token' && caller.kind !== 'token') refuse('credential_refused', `${call} needs a Turn Zero token, and this call carried a participant secret: call it with a token of your account`); if (takes === 'secret' && caller.kind !== 'secret') refuse('credential_refused', `${call} needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first`); if (caller.kind === 'secret') { caller.participant.lastCall = now(); if (caller.listener !== undefined) { caller.listener.lastCall = now(); // The listener's first call under its own secret (XTX-12), a // program attach's key reading used from now (XTX-09). delete caller.listener.unheard; delete caller.listener.fresh; } } return caller; }; /** The calls an account that has adopted no handle is refused at * (XTX-03; XTX-06; XTX-16): every messaging call; the others, `human` and * `account.set` among them, answer as before. */ const HANDLE_REQUIRED_CALLS: ReadonlySet = new Set(['register', 'send', 'create', 'join', 'invite', 'image', 'report_message']); /** `handle_required` (XTX-03): a caller of an account with no handle, a * guest and the company excepted, is told to ask its person; under an * application's credential, the person sets the handle at tzmesh.ai/me * or in their own tool, since `account.set` refuses it (XTX-21). */ const requireHandle = (call: string, caller: Caller): void => { if (!HANDLE_REQUIRED_CALLS.has(call) || caller.kind === 'anonymous') return; if (caller.kind === 'secret' && caller.participant.guest) return; const account = caller.kind === 'token' ? tokenAccount(caller.token) : accountByKey(caller.participant.owner); if (account === undefined || account.adopted) return; if (applicationCredential(caller)) { refuse('handle_required', 'the account has no handle yet: the person chooses one, and whether to stay unlisted and join public channels, at tzmesh.ai/me or from their own tool; an application never chooses them'); } refuse( 'handle_required', 'the account has no handle yet: ask the person for one (3 to 32 letters, digits, and hyphens, a letter or a digit at each end), whether to stay unlisted, and whether to join public channels, then call account.set (the account_set tool over MCP) with their answers; never choose them yourself', ); }; /** An application's credential (XTX-02): a token minted at an * application's sign-in, or the human secret `human` answered under one. */ const applicationCredential = (caller: Caller): boolean => { if (caller.kind === 'token') return caller.token.applicationSignIn; if (caller.kind === 'secret') return humanSecrets.get(caller.value)?.application === true; return false; }; /** The owner's acts, `resume`, and `attach` refuse an application's credential, each * by name, the detail naming where the person takes the act (XTX-02; * XTX-03; XTX-16; XTX-21). */ const refuseApplication = (what: string): never => refuse('credential_refused', `${what} is the person's own act, which an application's sign-in cannot make: the person makes it at tzmesh.ai/me or from their own tool`); const credentialed = (caller: Caller, call: string): Credentialed => { if (caller.kind === 'anonymous') refuse('credential_required', `${call} needs a credential, and this call carried none: send an authorization header of Bearer followed by a token or the participant secret register answered`); return caller; }; /** The key of the caller's account: its handle, or its id until it has * adopted one. */ const callerAccount = (caller: Credentialed): string => (caller.kind === 'token' ? tokenAccount(caller.token).handle : caller.participant.owner); /** A guest may not take the acts XTX-16 withholds from it. */ const noGuest = (caller: Caller, what: string): void => { if (caller.kind === 'secret' && caller.participant.guest) refuse('guest_not_allowed', `a guest cannot ${what}: sign in with a Turn Zero account to do it`); }; /** A guest's new conversation reaches a public standing participant that * admits everyone, and nothing else (XTX-06): a session, a person, an * account's handle, a guest, and an agent that admits signed-in people or * its team alone are refused `guest_not_allowed`, a private agent refused * before as if nothing stood (admitted). */ const guestMayReach = (p: Participant, target: Participant | undefined, accountNamed = false): void => { if (!p.guest) return; if (accountNamed || target === undefined || target.kind !== 'standing') refuse('guest_not_allowed', 'a guest can message only a public agent, not a session, a person, an account, or a channel: sign in with a Turn Zero account to message them'); if (target.admits === 'signed_in') refuse('guest_not_allowed', `${target.handle} talks only with signed-in people: sign in with a Turn Zero account to message it`); if (target.admits === 'team') refuse('guest_not_allowed', `${target.handle} talks only with its owner's account and its team: sign in with a Turn Zero account of that team to message it`); }; /** A body's `admits` as the record keeps it (XTX-04): `everyone` kept as * none, `signed_in` or `team` as itself. */ const readAdmits = (value: unknown): Exclude | undefined => { if (value === 'everyone') return undefined; if (value === 'signed_in' || value === 'team') return value; refuse('invalid_request', 'admits must be everyone, signed_in, or team: use everyone for anyone, signed_in for signed-in people, or team for your account and its team'); }; // --- visibility and admission (XTX-06; XTX-L0-09) ------------------------ /** A session's or an agent's visibility, private where none is set. */ const visibilityOf = (p: Participant): TzmeshVisibility => (p.visibility === 'public' ? 'public' : 'private'); /** Whether an account is a member of a team channel (XTX-L0-09): its * person, or one of its live agents, is a member of the channel. */ const teamMember = (team: string | undefined, account: string): boolean => { if (team === undefined) return false; const c = channels.get(team); if (c === undefined || c.kind !== 'private') return false; for (const id of c.members.keys()) { const q = participants.get(id); if (q === undefined || q.owner !== account || q.guest) continue; if (q.kind === 'human' || (q.kind === 'standing' && !q.ended && presenceOf(q) !== 'ended')) return true; } return false; }; /** Whether a sender may start a new conversation with, or invite, a * participant (XTX-06; XTX-L0-09): a person and a public session or agent * by their own rules after; a private one by its owner's participants, * its team's accounts, and, as an account's `answered_by`, a sender * messaging the account's handle. */ const admitted = (sender: Participant, target: Participant, viaAccount: boolean): boolean => { if (target.kind !== 'session' && target.kind !== 'standing') return true; if (visibilityOf(target) === 'public') return true; if (sender.guest) return false; if (sender.owner === target.owner || viaAccount) return true; return teamMember(target.team, sender.owner); }; /** Whether a sender is one of the participant's owner's own participants * or of an account that is a member of its team; a guest is neither. */ const ownOrTeam = (sender: Participant, target: Participant): boolean => !sender.guest && (sender.owner === target.owner || teamMember(target.team, sender.owner)); // --- listeners (XTX-02; XTX-06; XTX-L0-02; the simplified plan's // decision 6) --------------------------------------------------------------- /** Whether a participant's secrets are listeners': a session's and an * agent's. */ const servedByListeners = (p: Participant): boolean => (p.kind === 'session' || p.kind === 'standing') && !p.guest; /** Whether a participant's direct conversations stick to its listeners: * an agent's. */ const routed = (p: Participant | undefined): p is Participant => p !== undefined && p.kind === 'standing' && !p.guest; /** Make a listener of the participant under a fresh secret, attached by * the account given, its id the participant's next number. */ const addListener = (p: Participant, kind: Listener['kind'], account: string, label?: string): Listener => { p.nextListener += 1; const l: Listener = { id: `l${p.nextListener}`, kind, secret: mintSecret(), account, lastCall: now(), ended: false, holds: 0, createdAt: instant() }; if (label !== undefined && label !== '') l.label = label; p.listeners.push(l); secrets.set(l.secret, p.id); return l; }; /** A listener's `stream` (XTX-09): its socket's address at the double's * reserved origin, naming the participant's id and the listener's, and * the one protocol, `tzmesh.v1`. */ const streamAddress = (p: Participant, l: Listener): TzmeshStreamAddress => ({ url: `${DOUBLE_STREAM_ORIGIN}${DOUBLE_LISTEN_PATH}${p.id}/${l.id}`, protocols: [DOUBLE_STREAM_PROTOCOL], }); /** A session listener ended by its silence, `endedAfterMs` with no call * and nothing held (XTX-12). */ const listenerLapsed = (l: Listener): boolean => l.kind === 'session' && l.holds === 0 && l.webhook === undefined && now() - l.lastCall >= constants.endedAfterMs; /** Whether a listener answers now, for an assignment (XTX-06): attached * and not ended, and holding something, or a healthy webhook, or within * `activeWindowMs` of its last call. */ const listenerLive = (p: Participant, l: Listener): boolean => { if (l.ended || p.ended || listenerLapsed(l)) return false; if (l.holds > 0 || listenerWebhookHealthy(l)) return true; // The listener an agent's register made, and one a program's attach // made, is live by no call until one under its own secret (XTX-09; // XTX-12; #6706). return !silentListener(l) && now() - l.lastCall < constants.activeWindowMs; }; /** The live listeners, the connected ones first where any is. */ const liveListeners = (p: Participant): Listener[] => { const live = p.listeners.filter((l) => listenerLive(p, l)); const connected = live.filter((l) => l.holds > 0 || listenerWebhookHealthy(l)); return connected.length > 0 ? connected : live; }; /** The attached listener a conversation sticks to for a member, or * undefined where it sticks to none. */ const assignedListener = (c: Channel, p: Participant): Listener | undefined => { const id = c.listeners?.get(p.id); const l = id === undefined ? undefined : p.listeners.find((x) => x.id === id); return l === undefined || l.ended ? undefined : l; }; /** Whether a listener of a participant reads a channel: every channel but * a direct conversation, and a direct conversation assigned to it or to * none. */ const visibleTo = (c: Channel, p: Participant, l: Listener | undefined): boolean => { if (l === undefined || c.kind !== 'direct' || !routed(p)) return true; const assigned = assignedListener(c, p); return assigned === undefined || assigned === l; }; const reachesListener = (c: Channel, participant: string, listener: string | undefined): boolean => { if (listener === undefined) return true; const p = participants.get(participant); if (p === undefined) return true; return visibleTo(c, p, p.listeners.find((l) => l.id === listener)); }; /** The other member of a participant's direct conversation. */ const directPeer = (c: Channel, id: string): Participant | undefined => { if (c.kind !== 'direct') return undefined; for (const member of c.members.keys()) if (member !== id) return participants.get(member); return undefined; }; /** An agent's direct conversations with a message in the last * `talkerWindowMs`, those a listener answers where one is given. */ const activeConversations = (p: Participant, l?: Listener): number => [...channels.values()].filter((c) => c.kind === 'direct' && c.members.has(p.id) && now() - c.lastMessageAt < constants.talkerWindowMs && (l === undefined || assignedListener(c, p) === l)).length; /** The live listener with the fewest active conversations, the earliest of * those tied, counting nothing where one is live and no capacity is set. */ const fewestActive = (p: Participant, live: Listener[]): Listener | undefined => { if (live.length === 0) return undefined; if (live.length === 1 && p.capacity === undefined) return live[0]; let best = live[0]; let bestN = -1; for (const l of live) { const n = activeConversations(p, l); if (bestN < 0 || n < bestN) { best = l; bestN = n; } } return best; }; /** Stick a conversation to a listener of the member, an `answering` system * message to the other side where it moves from another listener * (XTX-05). */ const assign = (c: Channel, p: Participant, l: Listener): void => { c.listeners ??= new Map(); const previous = c.listeners.get(p.id); if (previous === l.id) return; c.listeners.set(p.id, l.id); if (previous !== undefined) systemMessage(c, 'answering', p); }; const unassign = (c: Channel, p: Participant): void => { c.listeners?.delete(p.id); }; /** Give an agent's direct conversation another party messaged on one of its * listeners: the one it sticks to while live, or the fewest active * (XTX-06). Answers whether a listener is live. */ const route = (c: Channel, p: Participant): boolean => { const current = assignedListener(c, p); if (current !== undefined && listenerLive(p, current)) return true; const chosen = fewestActive(p, liveListeners(p)); if (chosen === undefined) return false; assign(c, p, chosen); return true; }; /** Give each of an agent's unassigned conversations that holds a message past * its cursor to a live listener, at an attach (XTX-06). */ const assignWaiting = (p: Participant): void => { if (!routed(p)) return; const live = liveListeners(p); if (live.length === 0) return; for (const c of channels.values()) { if (c.kind !== 'direct' || !c.members.has(p.id) || assignedListener(c, p) !== undefined) continue; if ((c.members.get(p.id) ?? 0) >= c.log.length) continue; const chosen = fewestActive(p, live); if (chosen !== undefined) assign(c, p, chosen); } }; /** End a listener's streams and held reads. */ const closeListenerHolds = (p: Participant, l: Listener): void => { for (const stream of [...streams]) if (stream.participant === p.id && stream.listener === l.id) stream.end(); for (const waiter of [...waiters]) if (waiter.participant === p.id && waiter.listener === l.id) waiter.wake(); }; /** Take a listener off its participant with its secret, its conversations * left unassigned. */ const removeListener = (p: Participant, l: Listener): void => { closeListenerHolds(p, l); l.webhook = undefined; if (secrets.get(l.secret) === p.id) secrets.delete(l.secret); for (const c of channels.values()) if (c.listeners?.get(p.id) === l.id) unassign(c, p); p.listeners = p.listeners.filter((x) => x !== l); }; /** Detach a listener (XTX-03): its holds close, its webhook goes, and the * conversations it answered move to another live listener with an * `answering` message, or are left unassigned; a program listener stays, * ended, and a session listener goes. */ const detachListener = (p: Participant, l: Listener): void => { closeListenerHolds(p, l); l.webhook = undefined; if (routed(p)) { const others = liveListeners(p).filter((x) => x !== l); for (const c of channels.values()) { if (c.kind !== 'direct' || c.listeners?.get(p.id) !== l.id) continue; const to = fewestActive(p, others); if (to !== undefined) assign(c, p, to); else unassign(c, p); } } if (l.kind === 'program') { l.ended = true; return; } removeListener(p, l); }; /** An agent's own rules for a message another party sends it (XTX-04; * XTX-06): a public agent admitting its team alone refuses a new * conversation from a signed-in outsider `not_a_member`; a one-way agent * refuses every message `one_way` with its owner's sentence, whatever its * listeners, a new conversation or one that stands; past its capacity a * new conversation is refused `agent_busy`; with its queue off, a message * while no listener is live is refused `nobody_listening`. */ const agentAdmits = (sender: Participant, agent: Participant | undefined, opening: boolean, accountNamed: boolean): void => { if (!routed(agent) || agent.id === sender.id) return; if (opening && visibilityOf(agent) === 'public' && agent.admits === 'team' && !accountNamed && !ownOrTeam(sender, agent)) refuse('not_a_member', `${agent.handle} takes conversations only from its owner's account and its team: ask its owner to add your account to its team, or message another agent`); if (agent.oneWay !== undefined) refuse('one_way', `${agent.handle} takes no messages: ${agent.oneWay}`); if (opening) { if (agent.capacity !== undefined && activeConversations(agent) >= agent.capacity) refuse('agent_busy', `${agent.handle} is already in ${agent.capacity} ${agent.capacity === 1 ? 'conversation' : 'conversations'}, the most it takes at once, so it starts no new one now: try again later, or message another agent`); } if (agent.queueOff === true && liveListeners(agent).length === 0) refuse('nobody_listening', `${agent.handle} has no one serving it now and keeps no queue, so the message would reach no one: try again later, or message another participant`); }; /** The live agent an account names as `answered_by`, or undefined, so a * message to the account's handle reaches its person (XTX-21). */ const answeringAgent = (a: Account): Participant | undefined => { if (a.answeredBy === undefined) return undefined; const p = participants.get(a.answeredBy); if (p === undefined || p.kind !== 'standing' || p.owner !== a.handle || p.ended || presenceOf(p) === 'ended') return undefined; return p; }; /** The account's default channel, made where the account holds a handle * and none stands, its live participants and its person joining it then, * and each participant given joined, every joining posting nothing * (XTX-05). */ const joinAll = (a: Account | undefined, ...joining: Participant[]): void => { if (a === undefined) return; if (a.all === undefined && (a.adopted || a.handle === company)) { const c: Channel = { id: nextId('ch'), kind: 'private', name: 'all', owner: a.handle, created_at: instant(), members: new Map(), invited: new Set(), log: [], lastMessageAt: 0, company: a.handle === company, isDefault: true }; channels.set(c.id, c); a.all = c.id; for (const q of participants.values()) { if (q.owner !== a.handle || q.guest || (q.kind !== 'human' && (q.ended || presenceOf(q) === 'ended'))) continue; addMember(c, q); c.invited.add(q.id); } } const c = a.all === undefined ? undefined : channels.get(a.all); if (c === undefined) return; for (const q of joining) { if (q.guest || c.members.has(q.id)) continue; addMember(c, q); c.invited.add(q.id); } }; // --- bodies -------------------------------------------------------------- const readBody = (init: TzmeshTransportInit): Record => { const text = init.body === undefined ? '{}' : typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body); if (byteLength(text) > constants.bodyBytesMax) refuse('body_too_large', `the request body is over ${constants.bodyBytesMax} bytes, the most a call takes: send less in one call, such as a shorter text or less data`); let parsed: unknown; try { parsed = JSON.parse(text) as unknown; } catch { refuse('invalid_request', 'the request body is not valid JSON: send a JSON object, such as {}'); } if (!isPlainObject(parsed)) refuse('invalid_request', "the request body is JSON but not an object: send a JSON object of the call's members, such as {}"); return parsed; }; const only = (body: Record, members: string[], call: string): void => { // The detail names the members the call takes, as the service's does. const takes = members.length > 0 ? `send only ${[...members].sort().join(', ')}` : 'send no members'; for (const key of Object.keys(body)) if (!members.includes(key)) refuse('invalid_request', `${call} takes no member ${key}: ${takes}`); }; const optionalString = (body: Record, key: string, max?: number): string | undefined => { const value = body[key]; if (value === undefined) return undefined; if (typeof value !== 'string') refuse('invalid_request', `${key} must be a string: send it as text in quotes`); if (max !== undefined && value.length > max) refuse('invalid_request', `${key} is longer than ${max} characters: shorten it`); return value; }; const requiredString = (body: Record, key: string, max?: number): string => { const value = optionalString(body, key, max); if (value === undefined || value === '') refuse('invalid_request', `${key} is missing or empty: give it a value`); return value; }; /** A register's or a profile.set's `one_way` (XTX-03; XTX-04): an agent's * sentence of 1 to `oneWayMax` characters on one line, scanned for a * credential as a description is (XTX-02), or null, which clears it and * answers undefined. A session's or a person's is refused. */ const oneWayOf = (body: Record, kind: string): string | undefined => { if (kind !== 'standing') refuse('invalid_request', 'one_way applies to an agent alone, and a session or a person sets none: leave one_way out'); if (body.one_way === null) return undefined; const sentence = requiredString(body, 'one_way', constants.oneWayMax); if (/[\r\n]/.test(sentence)) refuse('invalid_request', 'one_way is one line, and this one holds a line break: send the sentence on one line'); scanCredentials(sentence, 'one_way'); return sentence; }; const optionalBoolean = (body: Record, key: string): boolean | undefined => { const value = body[key]; if (value === undefined) return undefined; if (typeof value !== 'boolean') refuse('invalid_request', `${key} must be true or false: send it without quotes`); return value; }; const optionalInteger = (body: Record, key: string, min: number, max: number): number | undefined => { const value = body[key]; if (value === undefined) return undefined; if (typeof value !== 'number' || !Number.isInteger(value) || value < min || value > max) refuse('invalid_request', `${key} must be a whole number from ${min} to ${max}: send one in that range`); return value; }; const optionalStringList = (body: Record, key: string): string[] | undefined => { const value = body[key]; if (value === undefined) return undefined; if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) refuse('invalid_request', `${key} must be a list of strings: send it as a JSON array of text values`); return value as string[]; }; /** An image reference's form (XTX-19): its id, and bytes, width, and * height as numbers where given. */ const imageForm = (value: unknown, key: string): string => { if (!isPlainObject(value)) refuse('invalid_request', `${key} must be an image reference: send an object with the id the image call answered`); for (const member of Object.keys(value)) { if (!['id', 'bytes', 'width', 'height'].includes(member)) refuse('invalid_request', `${key} names a member an image reference does not take: send only id, bytes, width, and height`); } if (typeof value.id !== 'string' || value.id === '') refuse('invalid_request', `${key} names no id: give the id the image call answered`); for (const member of ['bytes', 'width', 'height']) { if (value[member] !== undefined && typeof value[member] !== 'number') refuse('invalid_request', `${key}.${member} must be a number: send it without quotes`); } return value.id; }; /** The stored image an id names, refused image_not_found where none * stands (XTX-19). */ const storedImage = (id: string, key: string): TzmeshImageReference => { const stored = storedImages.get(id); if (!stored) refuse('image_not_found', `${key} names no stored image; upload it with the image call first`); return { id: stored.id, bytes: stored.bytes, width: stored.width, height: stored.height }; }; const optionalImage = (value: unknown, key: string): TzmeshImageReference | undefined => { if (value === undefined) return undefined; return storedImage(imageForm(value, key), key); }; // --- lookups ------------------------------------------------------------- const liveParticipants = (): Participant[] => [...participants.values()].filter((p) => !p.ended && presenceOf(p) !== 'ended'); const participantByHandle = (handle: string): Participant | undefined => liveParticipants().find((p) => p.handle === handle); const participantByIdOrHandle = (value: string): Participant | undefined => { const byId = participants.get(value); if (byId && !byId.ended) return byId; return participantByHandle(value); }; /** The private channel `/#` names, whoever reads it: `#all` * the account's default channel, any other name the earliest of the * account's private channels holding it (XTX-05). */ const privateChannelNamed = (value: string): Channel | undefined => { const at = value.indexOf('/#'); if (at <= 0 || at + 2 >= value.length) return undefined; const routed = routeHeld(value.slice(0, at)) ?? value.slice(0, at); const name = value.slice(at + 2); const account = accountByKey(routed); if (account === undefined) return undefined; if (name === 'all' && account.all !== undefined) return channels.get(account.all); return [...channels.values()].find((c) => c.kind === 'private' && !c.isDefault && c.owner === account.handle && c.name === name); }; /** Whether a private channel admits a participant by its account, the one * place the rule stands (join; channelOf): the participant stands invited * itself, or a participant of its own account, no guest, stands invited * or is a member (XTX-05; the simplified plan's decisions 5 and 7, the * owner's ruling (A)). A guest holds no account. The account that owns * the channel is always admitted: its own human participant joins it * whatever the channel's members (XTX-05; XTX-16). */ const invitedAccount = (c: Channel, p: Participant): boolean => { if (p.guest) return false; if (c.invited.has(p.id)) return true; if (p.kind === 'human' && c.kind === 'private' && c.owner === p.owner) return true; const ofAccount = (id: string): boolean => { const q = participants.get(id); return q !== undefined && !q.guest && q.owner === p.owner; }; return [...c.invited].some(ofAccount) || [...c.members.keys()].some(ofAccount); }; /** The channel a call names (XTX-05): by its id, a public channel by * `#`, or a private channel by `/#`, which a viewer * that is no member of it, and whose account it does not admit * (invitedAccount), reads as absent. */ const channelOf = (value: string, viewer?: Participant): Channel | undefined => { const byId = channels.get(value); if (byId) return byId; if (value.startsWith('#')) { const name = value.slice(1); return [...channels.values()].find((c) => c.kind === 'public' && c.name === name); } const named = privateChannelNamed(value); if (named === undefined || viewer === undefined || (!named.members.has(viewer.id) && !invitedAccount(named, viewer))) return undefined; return named; }; /** The public channels whose name reads as the one given ignoring case, * for a claim (XTX-05). */ const publicChannelsFolded = (lower: string): Channel[] => [...channels.values()].filter((c) => c.kind === 'public' && c.name !== undefined && c.name.toLowerCase() === lower); /** The names an account holds in the global pool against its twenty * (XTX-03; XTX-05): its reservations and its public channels' names. */ const reservedCount = (a: Account): number => a.reserved.size + [...channels.values()].filter((c) => c.kind === 'public' && c.owner === a.handle).length; /** Why a new claim of a name collides with one of the claiming account's * own names of another kind, read ignoring case, or undefined (XTX-03; * XTX-05; XTX-21): its live global agents and its public channels, the kind * being claimed passed over. */ /** name_reserved's detail for a name another holder has (XTX-03; XTX-09): * the company's names and their look-alikes, which globalNameHolder * answers as the company's, are said to be Turn Zero's. */ const takenName = (name: string, holder: string): string => holder === company ? `${name} is one of Turn Zero's own names or looks like one: choose another name` : `${name} already belongs to another account: choose another name`; const ownClash = (account: string, name: string, kind: 'standing' | 'channel'): string | undefined => { const lower = name.toLowerCase(); if (kind !== 'standing' && liveParticipants().some((p) => p.owner === account && p.kind === 'standing' && p.global && p.handle.toLowerCase() === lower)) return `${name} is already the name of one of your account's agents: choose another name`; if (kind !== 'channel' && publicChannelsFolded(lower).some((c) => c.owner === account)) return `${name} is already the name of one of your account's public channels: choose another name`; return undefined; }; /** Who holds a global name in the one namespace (XTX-03): an account handle * in any case, a handle held for its account after a change (XTX-21), a * reserved name, a live global handle, or a public channel's name. */ const globalNameHolder = (name: string): string | undefined => { // The company's names and their look-alikes are the company's (XTX-09). if (companyNames.has(name) || companyLookAlikes.has(lookAlike(name))) return company; const lower = name.toLowerCase(); for (const account of accounts.values()) if (account.adopted && account.handle.toLowerCase() === lower) return account.handle; const held = heldBy(name); if (held) return held.handle; for (const account of accounts.values()) if (account.reserved.has(name)) return account.handle; const standing = liveParticipants().find((p) => p.global && p.handle === name); if (standing) return standing.owner; // A public channel's name is in the global pool (XTX-05). const channel = [...channels.values()].find((c) => c.kind === 'public' && c.name === name); if (channel) return channel.owner; return undefined; }; /** Who holds a name read ignoring case, for a claim of it (#5092; XTX-03; * XTX-10): a reservation, a live global handle, and a public channel's name * hold every case variant of themselves, as an account handle does, so a * global register, a public channel's create, and a handle's set refuse a * variant. Another holder answers before the claimer's own (`mine`). A * message reads names as globalNameHolder does. */ const claimHolder = (name: string, mine?: (holder: string) => boolean): string | undefined => { let own: string | undefined; const consider = (holder: string | undefined): string | undefined => { if (holder === undefined) return undefined; if (mine === undefined || !mine(holder)) return holder; own ??= holder; return undefined; }; const exact = consider(globalNameHolder(name)); if (exact !== undefined) return exact; const lower = name.toLowerCase(); for (const account of accounts.values()) { for (const reserved of account.reserved) if (reserved.toLowerCase() === lower && consider(account.handle) !== undefined) return account.handle; } for (const p of liveParticipants()) if (p.global && p.handle.toLowerCase() === lower && consider(p.owner) !== undefined) return p.owner; for (const c of publicChannelsFolded(lower)) if (consider(c.owner) !== undefined) return c.owner; return own; }; /** The service's own name, held by no account in any case (#5387). */ const reservedName = (name: string): boolean => name.toLowerCase() === 'erased'; const directChannel = (a: Participant, b: Participant): Channel => { for (const c of channels.values()) { if (c.kind === 'direct' && c.members.has(a.id) && c.members.has(b.id) && c.members.size === 2) return c; } const c: Channel = { id: nextId('ch'), kind: 'direct', owner: a.owner, creator: a.id, created_at: instant(), members: new Map(), invited: new Set(), log: [], lastMessageAt: 0, company: false }; c.members.set(a.id, 0); c.members.set(b.id, 0); channels.set(c.id, c); return c; }; // --- rates (XTX-11) ------------------------------------------------------ const count = (key: string, ceiling: number, what: string): void => { const window = Math.floor(now() / 60_000); const row = rates.get(key); if (!row || row.window !== window) { rates.set(key, { window, count: 1 }); return; } if (row.count >= ceiling) refuse('rate_limited', `${what} reached the limit of ${ceiling} a minute: wait until the next minute on the clock starts, then try again`); row.count += 1; }; /** A count in fixed one-day windows, a guest's daily ceiling (XTX-16). */ const countDay = (key: string, ceiling: number, what: string): void => { const window = Math.floor(now() / 86_400_000); const row = rates.get(key); if (!row || row.window !== window) { rates.set(key, { window, count: 1 }); return; } if (row.count >= ceiling) refuse('rate_limited', `${what} reached the limit of ${ceiling} a day: wait until midnight UTC, when the day's count starts again, then try again`); row.count += 1; }; // --- the calls ----------------------------------------------------------- /** `register` with `resume` (XTX-03): a session participant's id, under * a token of its account; a fresh session secret answered, replacing its * session listener's; `kind` standing refused `invalid_request`, an agent * being attached to, and every other participant refused * `participant_not_found`. A session ended by its silence alone revives, * refused where another live participant now holds its handle; one its * end call ended is refused (#7345). A resume carrying `host` or * `host_session` sets both, an absent one cleared, as a session's attach * does, and one carrying neither leaves both (#7463); a session listener * it adds is labelled with the host it carries (XTX-09). */ const resumeParticipant = (token: Token, body: Record): unknown => { only(body, ['kind', 'resume', 'session_secret', 'host', 'host_session'], 'register with resume'); const resume = requiredString(body, 'resume'); const kind = body.kind; if (kind !== undefined && kind !== 'session' && kind !== 'standing') refuse('invalid_request', 'kind must be session or standing: use session for one working session, or standing for a lasting agent'); if (kind === 'standing') refuse('invalid_request', 'resume carries on a session, not an agent: to serve an agent, call register with attach set to its handle'); optionalBoolean(body, 'session_secret'); // Read before the session is looked up, as register reads them, so a // refused resume changes nothing; set after the last refusal (#7463). const host = optionalString(body, 'host'); const hostSession = optionalString(body, 'host_session'); const account = tokenAccount(token); const found = [...participants.values()].find((q) => q.owner === account.handle && !q.guest && q.id === resume); const notFound = `${resume} is not a live session of your account, so resume cannot carry it on: call register without resume for a new session, or call register with attach set to an agent's handle to serve the agent`; if (found === undefined || found.kind !== 'session') refuse('participant_not_found', notFound); const p: Participant = found; // An end call is final. A session ended by its silence alone revives at // its own resume, its handle, memberships, and cursors standing, unless // another live participant of its account took its handle meanwhile // (#7345). The service refuses it once its retention has dropped the // session's credential (tzmesh:TZT-05); the double drops no credential. if (p.ended) refuse('participant_not_found', notFound); if (presenceOf(p) === 'ended' && liveParticipants().some((q) => q.id !== p.id && q.handle === p.handle)) refuse('participant_not_found', `${p.handle} now belongs to another live participant of your account, so this session, ended by its silence, cannot take it back: end that participant first, or call register without resume for a new session`); // A resume carrying either sets both, an absent one cleared, as a // session's attach does; one carrying neither leaves both (#7463). if (host !== undefined || hostSession !== undefined) { p.host = host; p.host_session = hostSession; } p.lastCall = now(); // The session's one session listener takes the fresh secret, the one it // held refused from then on (XTX-02). let session: Listener; const held = [...p.listeners].reverse().find((l) => l.kind === 'session'); if (held !== undefined) { if (secrets.get(held.secret) === p.id) secrets.delete(held.secret); held.secret = mintSecret(); held.lastCall = now(); secrets.set(held.secret, p.id); session = held; } else { // The new session listener is labelled with the host the resume // carries, empty where none (XTX-09). session = addListener(p, 'session', account.handle, host); } // A participant that stood before its account's default channel joins // it as it carries on (XTX-05). joinAll(account, p); // The resume answers the session listener's socket beside its fresh // secret (XTX-03; XTX-09). return { participant: participantRecord(p, p), secret: session.secret, stream: streamAddress(p, session) }; }; /** `register` with `attach` (XTX-03; XTX-09; the simplified plan's decision * 6): a new listener of the agent a handle names, global or prefixed, under a * token of an account that owns it or is a member of its team, a * session's unless `session_secret` is false; any other handle refused * `participant_not_found`. An ended agent revives. A session's attach * replaces the agent's session listener, live or not, its conversations * moving to the new listener with `answering`, and answers `replaced`; a * program's replaces none. A session's attach sets the agent's `host` * and `host_session` to the ones it carries, an absent one cleared, since * its one session runs there, and a program's carrying either is refused * `invalid_request` (#5932). */ const attachListener = (token: Token, body: Record): unknown => { only(body, ['attach', 'kind', 'session_secret', 'host', 'host_session', 'label'], 'register with attach'); const name = requiredString(body, 'attach'); if (body.kind !== undefined && body.kind !== 'standing') refuse('invalid_request', 'attach takes an agent, so kind must be standing or left out'); const session = optionalBoolean(body, 'session_secret') ?? true; const host = optionalString(body, 'host'); const hostSession = optionalString(body, 'host_session'); if (!session && (host !== undefined || hostSession !== undefined)) refuse('invalid_request', "host and host_session apply to a session's attach alone, and this attach asks for a program's secret: leave both out, or leave session_secret false out"); const label = listenerLabel(body, host); const account = tokenAccount(token); const handle = routeHeld(name.startsWith('@') ? name.slice(1) : name) ?? name; const named = [...participants.values()].filter((q) => q.kind === 'standing' && !q.deleted && (q.handle === handle || q.id === name)); const agent = named.find((q) => !q.ended && presenceOf(q) !== 'ended') ?? named[named.length - 1]; const notFound = `no agent named ${name} stands that your account may attach to, which takes one of its own agents or one of a team it belongs to: check the handle with directory, or register a new agent with kind standing`; // A name that holds one of the account's sessions says attach takes an agent alone. const held = agent === undefined ? participantByHandle(handle) : undefined; if (held !== undefined && held.kind === 'session' && held.owner === account.handle) refuse('participant_not_found', `${held.handle} is a session of your account, not an agent, and attach takes an agent alone: call register with resume set to the session's id to carry on as it, or register an agent under another name`); if (agent === undefined || (agent.owner !== account.handle && !teamMember(agent.team, account.handle))) refuse('participant_not_found', notFound); if (agent.ended || presenceOf(agent) === 'ended') { if (!agent.global && liveParticipants().some((q) => q.id !== agent.id && q.handle === agent.handle)) refuse('handle_taken', `${agent.handle} now belongs to another live participant of your account, so this ended agent cannot take it back: end that participant first, or register a new agent`); agent.ended = false; agent.endedAt = undefined; } if (session) { agent.host = host; agent.host_session = hostSession; } agent.lastCall = now(); const l = addListener(agent, session ? 'session' : 'program', account.handle, label); // A program's attach is the owner's act, no call under the new key, so // its row reads no `last_call`, the agent reads no call of it, and it // serves no assignment until its program calls (XTX-09; XTX-12; #6706); // the agent's own last call above keeps a revived agent from reading // ended. A session's attach is the session's own call. if (!session) l.fresh = true; // One AI session per agent (XTX-L0-02): the session listener that stood // is replaced, its conversations moving straight to the new one, each // posting `answering`, and its secret refused with the sentence after. let replacedListener: { listener: string; account: string } | null = null; const old = session ? [...agent.listeners].reverse().find((x) => x !== l && x.kind === 'session') : undefined; if (old !== undefined) { for (const c of channels.values()) if (c.kind === 'direct' && c.listeners?.get(agent.id) === old.id) assign(c, agent, l); replaced.set(old.secret, l.account); removeListener(agent, old); replacedListener = { listener: old.id, account: old.account }; } joinAll(accountByKey(agent.owner), agent); assignWaiting(agent); // A session's attach answers its listener's socket; a program's none, // its wake_url answering it (XTX-03; XTX-09). const answer: Record = { participant: participantRecord(agent, agent), secret: l.secret, listener: l.id, replaced: replacedListener }; if (session) answer.stream = streamAddress(agent, l); return answer; }; /** A title's slug, the session name a register with no name takes * (XTX-03): lower case, each run of anything but a letter or a digit one * hyphen, no hyphen at either end, at most `nameMax` characters. */ const slugOf = (title: string): string => { let slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); if (slug.length > constants.nameMax) slug = slug.slice(0, constants.nameMax).replace(/-+$/, ''); return slug; }; /** The slug, or the slug and -2, -3, and on within the name's bound, that * no live participant of the account holds. */ const uniqueSlug = (account: string, slug: string): string | undefined => { if (slug === '') return undefined; const taken = (name: string): boolean => participantByHandle(`${account}/${name}`) !== undefined; if (!taken(slug)) return slug; for (let n = 2; ; n += 1) { const suffix = `-${n}`; const base = slug.length + suffix.length > constants.nameMax ? slug.slice(0, constants.nameMax - suffix.length).replace(/-+$/, '') : slug; if (!taken(`${base}${suffix}`)) return `${base}${suffix}`; } }; /** The listener's label a register or an attach carries (XTX-09; the * route A plan's decision 20): `label`, bounded as a title is and scanned * for a credential, the host's name where the body carries no label, and * none where it carries neither. */ const listenerLabel = (body: Record, host: string | undefined): string | undefined => { const label = optionalString(body, 'label', constants.titleMax); if (label !== undefined) { scanCredentials(label, 'the label'); return label.trim(); } return host; }; const register = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'token') refuse('credential_refused', 'register needs a Turn Zero token, and this call carried a participant secret: call register with a token of your account'); // An application's token registers a session or an agent, never a resume // (XTX-03); its global name is refused below, after the body's own checks, // in the service's order. // The member's name before the global name's rename (#6119) is refused by // name in every form of register, the detail naming the member that // replaced it. const refuseLegacyGlobal = (): void => { if (body.bare !== undefined) refuse('invalid_request', 'register no longer takes bare: send global true for a global name instead'); }; if (caller.token.applicationSignIn && body.attach !== undefined) refuseApplication('attaching to an agent'); if (body.attach !== undefined) { refuseLegacyGlobal(); return attachListener(caller.token, body); } if (caller.token.applicationSignIn && body.resume !== undefined) refuseApplication('resuming a session or an agent'); if (body.resume !== undefined) { refuseLegacyGlobal(); return resumeParticipant(caller.token, body); } refuseLegacyGlobal(); only(body, ['kind', 'name', 'title', 'visibility', 'admits', 'team', 'session_secret', 'global', 'owner_visibility', 'description', 'one_way', 'host', 'host_session', 'website', 'social', 'image', 'tags', 'label'], 'register'); const kind = body.kind; if (kind === 'human') refuse('invalid_request', "register takes no kind human, since a person's participant comes from signing in at the site: send kind session or standing"); if (kind !== 'session' && kind !== 'standing') refuse('invalid_request', 'kind must be session or standing: use session for one working session, or standing for a lasting agent'); const account = tokenAccount(caller.token); const globalName = optionalBoolean(body, 'global') ?? false; if (globalName && kind === 'session') refuse('invalid_request', 'a session cannot take a global name: leave global out, or register with kind standing for a global agent'); if (globalName && caller.token.applicationSignIn) refuseApplication('registering a global name'); // A session or an agent is private unless its register says public, the // person's own word (XTX-03; XTX-L0-09). const visibility = optionalString(body, 'visibility'); if (visibility !== undefined && visibility !== 'public' && visibility !== 'private') refuse('invalid_request', 'visibility must be public or private: leave it out for private'); // Who may start a conversation with an agent, its owner's choice at the // register as at profile.set (XTX-03; XTX-04): everyone where absent; a // session sets none. let admits: Exclude | undefined; if ('admits' in body) { if (kind !== 'standing') refuse('invalid_request', 'admits applies to an agent alone, and a session sets none: leave admits out'); admits = readAdmits(body.admits); } const title = optionalString(body, 'title', constants.titleMax); if (title !== undefined) scanCredentials(title, 'the title'); const sessionSecret = optionalBoolean(body, 'session_secret') ?? false; const description = optionalString(body, 'description', constants.descriptionMax) ?? ''; scanCredentials(description, 'the description'); // An agent's one-way standing, its owner's sentence, at the register as // at profile.set (XTX-03; XTX-04). const oneWay = 'one_way' in body ? oneWayOf(body, kind) : undefined; // The team, one private channel of the account, at the register that // creates the participant alone (XTX-03). const teamName = optionalString(body, 'team'); let team: string | undefined; if (teamName !== undefined) { const named = channels.get(teamName) ?? privateChannelNamed(teamName); if (named === undefined || named.kind !== 'private' || named.owner !== account.handle) refuse('channel_not_found', 'team names no private channel of your account: name one by its id or as /#, or make it first with create'); team = named.id; } const ownerShown = optionalString(body, 'owner_visibility'); if (ownerShown !== undefined && ownerShown !== 'public' && ownerShown !== 'private') refuse('invalid_request', 'owner_visibility must be public or private: leave it out for the default'); if (kind === 'standing' && liveParticipants().filter((p) => p.owner === account.handle && p.kind === 'standing').length >= constants.standingPerAccount) { refuse('cap_reached', `your account already has ${constants.standingPerAccount} agents, the most it can hold: end one you no longer need with end, participant, and delete true first`); } let handle: string; if (globalName) { const name = requiredString(body, 'name'); if (!TZMESH_GLOBAL_NAME_PATTERN.test(name)) refuse('invalid_request', `a global name must be ${constants.globalNameMin} to ${constants.nameMax} letters, digits, and hyphens, beginning and ending with a letter or a digit: choose a name of that form`); if (reservedName(name)) refuse('name_reserved', `${name} is a name the service keeps for itself: choose another name`); const holder = claimHolder(name, (h) => h === account.handle); if (holder !== undefined && holder !== account.handle) refuse('name_reserved', takenName(name, holder)); // The account's own held handle routes to the account while the hold // stands (XTX-21), so a global agent of that name would be hidden by // send's held routing: it is refused until the hold ends or is released. if (heldBy(name)?.handle === account.handle) refuse('handle_taken', `${name} is your account's former handle, held for it after the change: the person releases it at tzmesh.ai/me, or choose another name`); // The account's own handle is its human participant's (XTX-03; XTX-16). if (liveParticipants().some((q) => q.kind === 'human' && q.handle === name)) refuse('handle_taken', `${name} is your account's own handle, which its person holds: choose another name`); // One name, one kind within the pool (XTX-03). const clash = ownClash(account.handle, name, 'standing'); if (clash !== undefined) refuse('handle_taken', clash); if (!account.reserved.has(name) && reservedCount(account) >= constants.reservedNamesPerAccount) refuse('cap_reached', `your account already holds ${constants.reservedNamesPerAccount} global names, counting its reservations, global agents, and public channels, which is the most it can hold: free one first, or use a name under your account's handle instead`); account.reserved.add(name); handle = name; } else { let name = optionalString(body, 'name'); // A session's name is a slug of its title, made unique under the // account (XTX-03). if (name === undefined && kind === 'session' && title !== undefined) name = uniqueSlug(account.handle, slugOf(title)); if (name === undefined) { let n = 1; do { name = `${kind}-${n}`; n += 1; } while (participantByHandle(`${account.handle}/${name}`)); } if (!TZMESH_NAME_PATTERN.test(name)) refuse('invalid_request', `a name must be 1 to ${constants.nameMax} letters, digits, hyphens, and underscores: choose a name of that form`); handle = `${account.handle}/${name}`; if (participantByHandle(handle)) refuse('handle_taken', `${handle} is already the handle of a live participant of your account: choose another name, or to carry on as that session call register with resume set to its id`); } const participant: Participant = { id: nextId('p'), kind, handle, global: globalName, description, owner: account.handle, owner_visibility: globalName ? (ownerShown ?? 'private') : 'public', host: optionalString(body, 'host'), host_session: optionalString(body, 'host_session'), website: optionalString(body, 'website'), social: optionalStringList(body, 'social'), image: optionalImage(body.image, 'image'), tags: optionalStringList(body, 'tags'), ...(title !== undefined ? { title } : {}), visibility: visibility === 'public' ? 'public' : 'private', ...(team !== undefined ? { team } : {}), ...(admits !== undefined ? { admits } : {}), ...(oneWay !== undefined ? { oneWay } : {}), secret: '', listeners: [], nextListener: 0, lastCall: now(), ended: false, holds: 0, guest: false, }; participants.set(participant.id, participant); // One secret per register, its first listener's: a program's, or with // `session_secret` a session's (XTX-03), labelled as the body says or // after the host (XTX-09). const first = addListener(participant, sessionSecret ? 'session' : 'program', account.handle, listenerLabel(body, participant.host)); // An agent's register is its owner's act, no call under the listener it // makes; a session's is the session's own first call (XTX-12). if (participant.kind === 'standing') first.unheard = true; // Every participant of the account joins its default channel (XTX-05). joinAll(account, participant); // A session listener's register answers its socket; a program's none, // its wake_url answering it (XTX-03; XTX-09). if (first.kind === 'session') return { participant: participantRecord(participant, participant), secret: first.secret, stream: streamAddress(participant, first) }; return { participant: participantRecord(participant, participant), secret: first.secret }; }; /** `end` (XTX-03): under a session's secret its end; under an agent's * listener's, that listener's detaching alone; under the owner's token * naming `participant`, every listener of the agent it names. */ const end = (caller: Caller, body: Record): unknown => { if (caller.kind === 'token') { if (caller.token.applicationSignIn) refuseApplication("ending an agent's listeners"); only(body, ['participant', 'delete', 'listener'], 'end'); const named = requiredString(body, 'participant'); const deleting = optionalBoolean(body, 'delete') ?? false; const listenerId = optionalString(body, 'listener'); if (listenerId !== undefined && body.delete !== undefined) refuse('invalid_request', 'delete ends the whole agent and listener ends one listener alone, so an end takes one of them: leave delete out to end the listener, or leave listener out to delete the agent'); const account = tokenAccount(caller.token); const handle = routeHeld(named) ?? named; const candidates = [...participants.values()].filter((q) => q.kind === 'standing' && !q.deleted && (q.id === named || q.handle === handle)); const agent = candidates.find((q) => !q.ended && presenceOf(q) !== 'ended') ?? candidates[candidates.length - 1]; if (agent === undefined || agent.owner !== account.handle) refuse('participant_not_found', `no agent of your account is named ${named}: name one by the handle or id participants lists`); if (listenerId !== undefined) { // One listener alone (the route A plan's decision 20): its secret // refused from then on, its siblings serving, the agent standing. const one = agent.listeners.find((l) => l.id === listenerId); if (one === undefined) refuse('participant_not_found', `${agent.handle} has no listener of id ${listenerId}: name one of its listeners by the id its register or attach answered`); removeListener(agent, one); return { participant: participantRecord(agent, agent), listener: one.id }; } for (const l of [...agent.listeners]) removeListener(agent, l); if (deleting) { // Deleted for good (XTX-03): ended at once, `ended` in each direct // conversation and every other channel left, as a session's end // does (XTX-05); no attach or owner's end reaches it again. agent.deleted = true; if (!agent.ended) { agent.ended = true; agent.endedAt = now(); } for (const c of [...channels.values()]) { if (!c.members.has(agent.id)) continue; if (c.kind === 'direct') systemMessage(c, 'ended', agent); else removeMember(c, agent); } } return { participant: participantRecord(agent, agent) }; } if (caller.kind !== 'secret') refuse('credential_refused', 'end needs a credential: the participant secret register answered ends that participant, and a token of your account with participant set ends one of its agents'); // The owner's end naming an agent is an owner's act, under the owner's // token and never a listener's secret (XTX-02). if (body.participant !== undefined) refuse('credential_refused', "ending an agent by participant is the owner's act, and this call carried a participant secret: call end with a token of the agent's account, or leave participant out to end only this participant"); only(body, [], 'end'); const p = caller.participant; // An account's human participant's `end` signs one browser out: it ends // the secret it is called under, and the participant stands (XTX-16). const held = humanSecrets.get(caller.value); if (held !== undefined) { held.ended = true; secrets.delete(caller.value); return { participant: participantRecord(p, p) }; } if (p.kind === 'standing') { // An agent stands: its listener alone is detached (XTX-03). if (caller.listener !== undefined) detachListener(p, caller.listener); return { participant: participantRecord(p, p) }; } p.ended = true; p.endedAt = now(); // An ended participant is delivered nothing: its webhooks end with it // (XTX-18). for (const l of p.listeners) l.webhook = undefined; // A session's end in each direct conversation it holds: an `ended` // system message tells the other side no answer follows from it // (XTX-05). if (p.kind === 'session') { for (const c of channels.values()) if (c.members.has(p.id) && c.kind === 'direct') systemMessage(c, 'ended', p); } for (const stream of streams) if (stream.participant === p.id) stream.end(); for (const waiter of waiters) if (waiter.participant === p.id) waiter.wake(); if (p.guest) secrets.delete(p.secret); if (p.kind === 'session') { for (const c of channels.values()) if (c.members.has(p.id) && c.kind !== 'direct') removeMember(c, p); } return { participant: participantRecord(p, p) }; }; const profileSet = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "profile.set needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first"); noGuest(caller, 'set a profile'); only(body, ['name', 'title', 'visibility', 'description', 'website', 'social', 'image', 'tags', 'admits', 'capacity', 'queue', 'one_way'], 'profile.set'); // The changes are made on a copy and kept whole once every member is // read, so a refused call changes nothing (XTX-01). const live = caller.participant; const p: Participant = { ...live }; // A session's or an agent's own visibility, never null (XTX-04). if ('visibility' in body) { if (body.visibility !== 'public' && body.visibility !== 'private') refuse('invalid_request', 'visibility must be public or private, and null does not clear it: send public or private'); if (p.kind !== 'session' && p.kind !== 'standing') refuse('invalid_request', 'visibility applies to a session or an agent, and a person has none: leave visibility out'); p.visibility = body.visibility; } if ('title' in body) { if (body.title === null) delete p.title; else { const title = requiredString(body, 'title', constants.titleMax); scanCredentials(title, 'the title'); p.title = title; } } // A session's own name, never null (XTX-04; #6944): its handle becomes // /, its id, secret, memberships, and conversations kept, // and the former name is free at once. Others find an agent by its // handle, so a new name for one is a new agent; a person's handle is the // account's. if ('name' in body) { if (p.kind === 'standing') refuse('invalid_request', 'name renames a session alone, and others find an agent by its handle, so a new name is a new agent: register a new agent with kind standing under that name'); if (p.kind === 'human') refuse('invalid_request', "name renames a session alone, and a person's handle is the account's own: the person changes it at tzmesh.ai/me"); if (body.name === null) refuse('invalid_request', 'name renames this session, and null does not clear it: send the new name'); const name = requiredString(body, 'name'); if (!TZMESH_NAME_PATTERN.test(name)) refuse('invalid_request', `a name must be 1 to ${constants.nameMax} letters, digits, hyphens, and underscores: choose a name of that form`); const handle = `${p.owner}/${name}`; const holder = participantByHandle(handle); if (holder !== undefined && holder !== live) refuse('handle_taken', `${handle} is already the handle of a live participant of your account: choose another name`); p.handle = handle; } // Who may start a conversation with an agent, its owner's choice // (XTX-04; XTX-06), set on no other participant. if ('admits' in body) { if (p.kind !== 'standing') refuse('invalid_request', 'admits applies to an agent alone, and a session sets none: leave admits out'); p.admits = readAdmits(body.admits); } // An agent's capacity, null clearing it, and its queue (XTX-04; XTX-06). if ('capacity' in body) { if (p.kind !== 'standing') refuse('invalid_request', 'capacity applies to an agent alone, and a session sets none: leave capacity out'); p.capacity = body.capacity === null ? undefined : optionalInteger(body, 'capacity', 1, 1_000_000); } if ('queue' in body) { if (p.kind !== 'standing') refuse('invalid_request', 'queue applies to an agent alone, and a session sets none: leave queue out'); const on = optionalBoolean(body, 'queue'); if (on === undefined) refuse('invalid_request', 'queue must be true or false, and null does not clear it: send true to keep messages while no one serves the agent, or false to refuse them'); p.queueOff = on ? undefined : true; } // An agent's one-way standing, null clearing it (XTX-04). The agent an // account names as answered_by takes messages to the account's handle, // so it never stands one-way (XTX-21). if ('one_way' in body) { p.oneWay = oneWayOf(body, p.kind); if (p.oneWay !== undefined && accountByKey(p.owner)?.answeredBy === p.id) refuse('invalid_request', `one_way cannot stand on ${p.handle}, the agent your account names as answered_by, since a message to your account's handle reaches it: clear answered_by or name another agent with account.set first`); } if ('description' in body) { if (body.description === null) p.description = ''; else { const description = requiredString(body, 'description', constants.descriptionMax); scanCredentials(description, 'the description'); p.description = description; } } if ('website' in body) p.website = body.website === null ? undefined : requiredString(body, 'website'); if ('social' in body) p.social = body.social === null ? undefined : optionalStringList(body, 'social'); if ('image' in body) p.image = body.image === null ? undefined : optionalImage(body.image, 'image'); if ('tags' in body) p.tags = body.tags === null ? undefined : optionalStringList(body, 'tags'); const renamed = p.handle !== live.handle; for (const key of Object.keys(live) as Array) if (!(key in p)) delete live[key]; Object.assign(live, p); // Each message names its sender and subject by the participant's // current handle, as a read resolves it, the account's rename's walk // done for one participant (XTX-04; XTX-21). if (renamed) { for (const c of channels.values()) { for (const m of c.log) { if (m.sender?.id === live.id) m.sender = { id: live.id, handle: live.handle }; if (m.subject?.id === live.id) m.subject = { id: live.id, handle: live.handle }; } } } return { participant: participantRecord(live, live) }; }; /** A public channel's create, the owner's act (XTX-02; XTX-05): under a * token of the account, never an application's, naming `creator`, a live * participant of the account and its first member; its name a global name * claimed as a global register claims one, read ignoring case and counted * in the account's twenty. */ const createPublic = async (caller: Caller, body: Record): Promise => { if (caller.kind !== 'token') refuse('credential_refused', "creating a public channel is the owner's act, since its name is a permanent public claim, and this call carried a participant secret: call create with a token of your account and name the first member in creator"); if (caller.token.applicationSignIn) refuseApplication('creating a public channel'); only(body, ['kind', 'name', 'description', 'public_join', 'creator', 'mode'], 'create'); const mode = 'mode' in body ? modeOf(body) : undefined; const account = tokenAccount(caller.token); const creator = participantByIdOrHandle(requiredString(body, 'creator')); if (creator === undefined || creator.owner !== account.handle || creator.guest || creator.ended || presenceOf(creator) === 'ended') refuse('participant_not_found', "creator names no live participant of your account: name one of your sessions or agents, by its handle or id, as the channel's first member"); const name = requiredString(body, 'name'); if (!TZMESH_GLOBAL_NAME_PATTERN.test(name)) refuse('invalid_request', `a public channel's name must be a global name of ${constants.globalNameMin} to ${constants.nameMax} letters, digits, and hyphens, beginning and ending with a letter or a digit: choose a name of that form`); // The claim's refusals, read before the check and again after its // await, as the service reads its refusals again after the verdict // (XTX-05), so two creates of one name never both stand. const readClaim = (): void => { if (reservedName(name)) refuse('name_reserved', `${name} is a name the service keeps for itself: choose another name`); const holder = claimHolder(name, (h) => h === account.handle); if (holder !== undefined && holder !== account.handle) refuse('name_reserved', takenName(name, holder)); if (heldBy(name)?.handle === account.handle) refuse('handle_taken', `${name} is your account's former handle, held for it after the change: the person releases it at tzmesh.ai/me, or choose another name`); if (name.toLowerCase() === account.handle.toLowerCase()) refuse('handle_taken', `${name} is your account's own handle: choose another name for the channel`); const standing = publicChannelsFolded(name.toLowerCase()).find((c) => c.owner === account.handle); if (standing !== undefined) refuse('handle_taken', `your account already has the public channel ${standing.name}: use it, or choose another name`); const clash = ownClash(account.handle, name, 'channel'); if (clash !== undefined) refuse('handle_taken', clash); if (reservedCount(account) >= constants.reservedNamesPerAccount) refuse('cap_reached', `your account already holds ${constants.reservedNamesPerAccount} global names, counting its reservations, global agents, and public channels, which is the most it can hold: free one first, or use a name under your account's handle instead`); }; readClaim(); const publicJoin = optionalBoolean(body, 'public_join'); const description = optionalString(body, 'description', constants.descriptionMax); if (description !== undefined) scanCredentials(description, 'the description'); // Every refusal read, the checker the test gives judges the name with // its description before the channel stands, its call create // (#7522's decision 6); the claim is read again after the await. await checkName({ call: 'create', name, description: description ?? null, account: account.handle }); readClaim(); const c: Channel = { id: nextId('ch'), kind: 'public', name, description, owner: account.handle, creator: creator.id, public_join: publicJoin ?? true, created_at: instant(), members: new Map(), invited: new Set(), log: [], lastMessageAt: 0, company: account.handle === company, ...(mode !== undefined ? { mode } : {}), }; channels.set(c.id, c); addMember(c, creator); return { channel: channelRecord(c) }; }; /** `create` (XTX-05): a private channel under a participant's secret, its * caller the first member; a public one the owner's act (createPublic). */ const create = (caller: Caller, body: Record): unknown | Promise => { // A guest makes no channel (XTX-16). noGuest(caller, 'create a channel'); const kind = body.kind; if (kind !== 'private' && kind !== 'public') refuse('invalid_request', "kind must be private or public: use private for your account's own channel, or public for one others may join"); if (kind === 'public') return createPublic(caller, body); if (caller.kind !== 'secret') refuse('credential_refused', "a private channel is created under a participant's secret, which makes that participant its first member, and this call carried a token: call create with the secret register answered"); only(body, ['kind', 'name', 'description', 'public_join', 'mode'], 'create'); const name = requiredString(body, 'name'); const mode = 'mode' in body ? modeOf(body) : undefined; if (!TZMESH_CHANNEL_NAME_PATTERN.test(name)) refuse('invalid_request', `a channel's name must be 1 to ${constants.channelNameMax} letters, digits, hyphens, and underscores: choose a name of that form`); const p = caller.participant; const publicJoin = optionalBoolean(body, 'public_join'); if (publicJoin !== undefined) refuse('invalid_request', 'public_join applies to a public channel alone: leave it out for a private channel'); if (name === 'all') refuse('handle_taken', `${p.owner}/#all is your account's default channel, which every participant of the account joins: choose another name`); const taken = [...channels.values()].some((c) => c.kind === kind && c.name === name && c.owner === p.owner); if (taken) refuse('handle_taken', `a ${kind} channel named ${name} already exists: join it, or choose another name`); const description = optionalString(body, 'description', constants.descriptionMax); if (description !== undefined) scanCredentials(description, 'the description'); const c: Channel = { id: nextId('ch'), kind, name, description, owner: p.owner, creator: p.id, created_at: instant(), members: new Map(), invited: new Set(), log: [], lastMessageAt: 0, company: p.owner === company, ...(mode !== undefined ? { mode } : {}), }; channels.set(c.id, c); addMember(c, p); return { channel: channelRecord(c) }; }; const join = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "join needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first"); noGuest(caller, 'join a channel'); only(body, ['channel'], 'join'); const p = caller.participant; const c = channelOf(requiredString(body, 'channel'), p); // A name a person gives as a channel may be a participant's. if (!c) refuse('channel_not_found', "no channel of that id or name stands; a name your person gave may be a participant's handle instead: find it with directory and message it directly with send"); if (c.members.has(p.id)) return { channel: channelRecord(c) }; if (c.kind === 'public') { // An account remove_member put out of the channel is refused while its // bar stands, and a bar whose end has passed is dropped (#7522). const bar = c.bars?.get(p.owner); if (bar !== undefined) { if (bar.untilMs === 0 || bar.untilMs > now()) refuse('removed_from_channel', barDetail(c, bar)); c.bars?.delete(p.owner); } if (!c.company && !accountOf(p.owner).public) refuse('public_switch_off', "your account's public switch is off, so it joins no public channel but Turn Zero's own: the account's owner turns it on at tzmesh.ai/me or with account.set (the account_set tool over MCP) and public true"); if (c.public_join === false) refuse('not_a_member', 'this public channel takes no new members by join: ask its owner how to take part'); } else if (c.kind === 'private') { // The participant, or any participant of its account, stands invited // or is a member: an account invited once, or whose participant made // the channel, has its next session admitted (invitedAccount). if (!invitedAccount(c, p)) refuse('not_a_member', 'you are not invited to this private channel: ask one of its members to invite you with invite'); } else { refuse('not_a_member', 'this is a direct conversation between two participants, which no one else joins: message either of them with send and to instead'); } addMember(c, p); return { channel: channelRecord(c) }; }; const leave = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "leave needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first"); only(body, ['channel'], 'leave'); const p = caller.participant; const c = channelOf(requiredString(body, 'channel'), p); if (!c) refuse('channel_not_found', 'no channel you can reach has that id or name: name it by its id, as # for a public one, or as /# for a private one'); if (!c.members.has(p.id)) refuse('not_a_member', 'you are not a member of this channel, so there is nothing to leave: name a channel you joined'); removeMember(c, p); return { channel: channelRecord(c) }; }; const invite = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "invite needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first"); // A guest invites no one (XTX-16). noGuest(caller, 'invite a participant'); only(body, ['channel', 'participant'], 'invite'); const p = caller.participant; const c = channelOf(requiredString(body, 'channel'), p); if (!c) refuse('channel_not_found', 'no channel you can reach has that id or name: name it by its id, as # for a public one, or as /# for a private one'); if (c.kind !== 'private') refuse('invalid_request', 'invite applies to a private channel alone: for a public channel, share its name so others can join it with join where it admits joining'); if (!c.members.has(p.id)) refuse('not_a_member', 'you are not a member of this channel, so you cannot invite to it: join it first'); const name = requiredString(body, 'participant'); const notFound = "no live participant you can invite has that id or handle: check the name with directory, or name an account's handle to invite its person"; let invited = participantByIdOrHandle(name); if (!invited) { // An account's handle names its person, minted with no secret where // the service holds the account and the person stands not yet (XTX-05; // XTX-16); an account the service never saw is none. const account = accountByKey(name); if (account === undefined || !(account.adopted || account.handle === company)) refuse('participant_not_found', notFound); invited = mintHuman(account); joinAll(account, invited); } // A private session or agent is invited by those it admits alone, and // read as absent by anyone else (XTX-05; XTX-06). if (!admitted(p, invited, false)) refuse('participant_not_found', notFound); c.invited.add(invited.id); addMember(c, invited); return { channel: channelRecord(c), invited: participantRecord(invited, p) }; }; const members = (caller: Caller, body: Record): unknown => { // A guest reads no member list (XTX-16). noGuest(caller, 'read a member list'); only(body, ['channel'], 'members'); const viewer = caller.kind === 'secret' ? caller.participant : undefined; const c = channelOf(requiredString(body, 'channel'), viewer); if (!c) refuse('channel_not_found', 'no channel you can reach has that id or name: name it by its id, as # for a public one, or as /# for a private one'); if (viewer && !c.members.has(viewer.id) && c.kind !== 'public') refuse('not_a_member', 'you are not a member of this channel: join it first, or for a private one ask a member to invite you'); const list = [...c.members.keys()].map((id) => participants.get(id)).filter((p): p is Participant => p !== undefined); return { channel: channelRecord(c), members: list.map((p) => participantRecord(p, viewer)), posters: posterHandles(c), moderators: moderatorHandles(c) }; }; /** A create's or a channel.set's `mode` (XTX-05): `open` or `announce`, * the record keeping `announce` alone and open as none. */ const modeOf = (body: Record): 'announce' | undefined => { const mode = body.mode as TzmeshChannelMode | undefined; if (mode === 'open') return undefined; if (mode === 'announce') return 'announce'; return refuse('invalid_request', 'mode must be open or announce: use open for a channel every member sends in, or announce for one its posters alone send in'); }; /** The handles of the members channel.set named, in its order (XTX-05). */ const posterHandles = (c: Channel): string[] => (c.posters ?? []) .map((id) => participants.get(id)) .filter((p): p is Participant => p !== undefined) .map((p) => p.handle); /** The handles of a public channel's named moderators, in its order (#7522). */ const moderatorHandles = (c: Channel): string[] => (c.moderators ?? []) .map((id) => participants.get(id)) .filter((p): p is Participant => p !== undefined) .map((p) => p.handle); /** Whether a member sends in an announce channel (XTX-05): a member * channel.set named, the channel's creator while it is a member, and the * owning account's human participant once it joins, whatever the named * list holds. The caller has read the membership. */ const poster = (c: Channel, p: Participant): boolean => (c.creator !== undefined && p.id === c.creator) || (p.kind === 'human' && !p.guest && p.owner === c.owner) || (c.posters ?? []).includes(p.id); /** How a refusal's detail names a channel: a public one by `#`, a * private one by `/#` where its record answers the account * as its owner, and otherwise by its id (XTX-05). */ const channelAddress = (c: Channel): string => { if (c.name !== undefined && c.kind === 'public') return `#${c.name}`; if (c.name !== undefined && c.kind === 'private' && channelOwner(c) === c.owner) return `${c.owner}/#${c.name}`; return c.id; }; /** `channel_read_only`'s detail for a reader's send (XTX-06): up to three * posters, the named ones in their order, then the creator and the owning * person, each a live member the reader is admitted to message and none * of them one-way; where none qualifies, no handle, and that only the * posters send. */ const readOnlyDetail = (c: Channel, reader: Participant): string => { const candidates = [...(c.posters ?? [])]; if (c.creator !== undefined) candidates.push(c.creator); const person = participantByHandle(c.owner); if (person !== undefined && person.kind === 'human' && !person.guest && person.owner === c.owner) candidates.push(person.id); const handles: string[] = []; const seen = new Set(); for (const id of candidates) { if (seen.has(id) || handles.length === 3) continue; seen.add(id); const q = participants.get(id); if (q === undefined || q.id === reader.id || !c.members.has(id) || q.guest || q.ended || presenceOf(q) === 'ended' || q.oneWay !== undefined || !admitted(reader, q, false)) continue; if (q.kind === 'standing' && visibilityOf(q) === 'public' && q.admits === 'team' && !ownOrTeam(reader, q)) continue; handles.push(q.handle); } const where = channelAddress(c); if (handles.length === 0) return `${where} takes its posters' messages alone: only its posters send in it`; if (handles.length === 1) return `${where} takes its posters' messages alone: message ${handles[0]} directly`; if (handles.length === 2) return `${where} takes its posters' messages alone: message ${handles[0]} or ${handles[1]} directly`; return `${where} takes its posters' messages alone: message ${handles[0]}, ${handles[1]}, or ${handles[2]} directly`; }; /** `channel.set` (XTX-05): `channel` and any of `mode`, `description`, * `posters`, the whole named list of members by id or handle, replaced as * profile.set replaces what it names, and a public channel's `listed`, * the owner's credential's alone (#7904); under the creator's secret or the * owner's credential, a token of the owning account or its human * participant's secret, any other caller refused `credential_refused`. A * name that is no member is refused `not_a_member`, and a direct * conversation `invalid_request`. Every member is read before anything * changes, so a refused call changes nothing (XTX-01). */ const channelSet = async (anyCaller: Caller, body: Record): Promise => { const caller = credentialed(anyCaller, 'channel.set'); only(body, ['channel', 'mode', 'posters', 'description', 'moderators', 'listed'], 'channel.set'); const named = requiredString(body, 'channel'); if (!('mode' in body) && !('posters' in body) && !('description' in body) && !('moderators' in body) && !('listed' in body)) refuse('invalid_request', 'channel.set carries nothing to change: give mode, posters, description, moderators, listed, or any of them'); let found: Channel | undefined; if (caller.kind === 'secret') { noGuest(caller, 'set a channel'); found = channelOf(named, caller.participant); } else { found = channels.get(named) ?? (named.startsWith('#') ? channelOf(named) : undefined); if (found === undefined) { const privateOne = privateChannelNamed(named); if (privateOne !== undefined && privateOne.owner === tokenAccount(caller.token).handle) found = privateOne; } } if (!found) refuse('channel_not_found', 'no channel you can reach has that id or name: name it by its id, as # for a public one, or as /# for a private one'); const channel: Channel = found; const setter = (): boolean => { if (applicationCredential(caller)) return false; if (caller.kind === 'token') return tokenAccount(caller.token).handle === channel.owner; const p = caller.participant; if (p.guest) return false; if (channel.creator !== undefined && p.id === channel.creator) return true; return p.kind === 'human' && p.owner === channel.owner; }; // Every refusal is read in readSet, before anything changes (XTX-01), // and read again after a name check's await, as the service reads its // refusals again after the verdict (XTX-05). // The owner's credential, a token of the owning account or its human // participant's own secret, the creator's secret not among them (#7904). const owner = (): boolean => { if (applicationCredential(caller)) return false; if (caller.kind === 'token') return tokenAccount(caller.token).handle === channel.owner; const p = caller.participant; return p.kind === 'human' && !p.guest && p.owner === channel.owner; }; const readSet = (): { description: string | undefined; mode: 'announce' | undefined; posters: string[] | undefined; moderators: string[] | undefined; unlisted: true | undefined } => { if (!setter()) refuse('credential_refused', "channel.set is the act of the channel's creator or of its owner, and this credential is neither: call it with the creator's secret, or with a token of the account that owns the channel or its person's own secret from tzmesh.ai/me"); if (channel.kind === 'direct') refuse('invalid_request', 'a direct conversation takes no mode, no posters, and no description, since its two members each send: set them on a private or a public channel'); // The description, null or an empty text clearing it, read before // anything changes (XTX-01). let description = channel.description; if ('description' in body) { const given = body.description === null ? '' : optionalString(body, 'description', constants.descriptionMax) ?? ''; if (given !== '') scanCredentials(given, 'the description'); description = given === '' ? undefined : given; } const mode = 'mode' in body ? modeOf(body) : channel.mode; let posters = channel.posters; if ('posters' in body) { const malformed = "posters must be a list of the channel's members, each by id or handle: send [] to name none"; if (!Array.isArray(body.posters)) refuse('invalid_request', malformed); const ids: string[] = []; for (const item of body.posters as unknown[]) { if (typeof item !== 'string' || item === '') refuse('invalid_request', malformed); const name = item as string; const p = participantByIdOrHandle(name); if (p === undefined || !channel.members.has(p.id)) refuse('not_a_member', `${name} is no member of this channel, and a poster is one of its members: invite it or have it join first, then name it in posters`); if (!ids.includes(p.id)) ids.push(p.id); } posters = ids.length > 0 ? ids : undefined; } // A public channel's moderators, the whole named list, replaced as the // posters are; a private or direct channel's is refused (#7522). let moderators = channel.moderators; if ('moderators' in body) { if (channel.kind !== 'public') refuse('invalid_request', "moderators apply to a public channel alone, since a private channel's members are its creator's and its owner's to choose: name them on a public channel"); const malformed = "moderators must be a list of the channel's members, each by id or handle: send [] to name none"; if (!Array.isArray(body.moderators)) refuse('invalid_request', malformed); const ids: string[] = []; for (const item of body.moderators as unknown[]) { if (typeof item !== 'string' || item === '') refuse('invalid_request', malformed); const name = item as string; const p = participantByIdOrHandle(name); if (p === undefined || !channel.members.has(p.id)) refuse('not_a_member', `${name} is no member of this channel, and a moderator is one of its members: have it join first, then name it in moderators`); if (!ids.includes(p.id)) ids.push(p.id); } moderators = ids.length > 0 ? ids : undefined; } // A public channel's listing in the directory, the owner's choice // alone; a private or direct channel's is refused (#7904). let unlisted = channel.unlisted; if ('listed' in body) { if (channel.kind !== 'public') refuse('invalid_request', 'listed applies to a public channel alone, since a private channel is in no directory: leave it out for a private channel'); if (!owner()) refuse('credential_refused', "listed is the owner's choice alone, and this credential is not the owner's: call it with a token of the account that owns the channel or its person's own secret from tzmesh.ai/me"); unlisted = optionalBoolean(body, 'listed') === false ? true : undefined; } return { description, mode, posters, moderators, unlisted }; }; let { description, mode, posters, moderators, unlisted } = readSet(); // A public channel's new description passes the name check (A2), its // call channel.set, the description alone judged; a description // cleared asks nothing (#7522's decision 6). if (channel.kind === 'public' && 'description' in body && description !== undefined) { await checkName({ call: 'channel.set', name: channel.name as string, description, account: channel.owner }); ({ description, mode, posters, moderators, unlisted } = readSet()); } if (mode === undefined) delete channel.mode; else channel.mode = mode; if (posters === undefined) delete channel.posters; else channel.posters = posters; if (moderators === undefined) delete channel.moderators; else channel.moderators = moderators; if (description === undefined) delete channel.description; else channel.description = description; if (unlisted === undefined) delete channel.unlisted; else channel.unlisted = unlisted; return { channel: channelRecord(channel), posters: posterHandles(channel), moderators: moderatorHandles(channel) }; }; /** The removed form every read answers for a message a moderator removed * (#7522): the text `Removed by moderation`, no data, no images, no * mentions, and `removed_at`, the envelope standing. The double keeps * every message in memory, so the stored message is rewritten in place * and every later read, export, and stream opening answers it so. */ const REMOVED_TEXT = 'Removed by moderation'; /** Whether a caller may remove a message or a member of a channel * (#7522): one of the channel's named moderators, its creator while a * member, or the owner's credential, a token of the owning account or its * human participant's secret, never an application's and never a guest's. */ const moderator = (caller: Credentialed, c: Channel): boolean => { if (applicationCredential(caller)) return false; if (caller.kind === 'token') return tokenAccount(caller.token).handle === c.owner; const p = caller.participant; if (p.guest) return false; if (c.creator !== undefined && p.id === c.creator && c.members.has(p.id)) return true; if (p.kind === 'human' && p.owner === c.owner) return true; return (c.moderators ?? []).includes(p.id); }; /** The channel a moderation call names, read as channel.set reads it, * then the caller's standing and the channel's kind, so a refused call * changes nothing (XTX-01). */ const moderatedChannel = (anyCaller: Caller, body: Record, call: string): Channel => { const caller = credentialed(anyCaller, call); const named = requiredString(body, 'channel'); let found: Channel | undefined; if (caller.kind === 'secret') { noGuest(caller, call); found = channelOf(named, caller.participant); } else { found = channels.get(named) ?? (named.startsWith('#') ? channelOf(named) : undefined); } if (!found) refuse('channel_not_found', 'no channel you can reach has that id or name: name a public channel by its id or as #'); const c: Channel = found; // The channel's kind before the caller's standing, so a private or direct // channel is refused invalid_request whoever calls (XTX-23). if (c.kind !== 'public') refuse('invalid_request', `${call} applies to a public channel alone: a private channel's creator or owner removes a member with its own means, and a direct conversation has no moderator`); if (!moderator(caller, c)) refuse('credential_refused', `${call} is the act of one of the channel's moderators, of its creator, or of its owner, and this credential is none of them: call it as a member channel.set named in moderators, as the creator, or with a token of the account that owns the channel or its person's own secret from tzmesh.ai/me`); return c; }; /** A moderation call's `reason`: one line of 1 to `noteMax` characters, * scanned for a credential (XTX-02; XTX-11). */ const reasonOf = (body: Record): string => { const reason = requiredString(body, 'reason', constants.noteMax); if (/[\r\n]/.test(reason)) refuse('invalid_request', 'reason is one line: remove the line break'); scanCredentials(reason, 'the reason'); return reason; }; /** `remove_message` (#7522's decision 2): a moderator's, the creator's, or * the owner's removal of a message of a public channel, by id, with a * reason. The message reads `Removed by moderation` from then on, in * every read and export, and a `message_removed` notice posts in the * channel, its subject the sender, carrying the message's id and the * reason, and reaching the sender. A removed message removed again is * answered and nothing more; a system message is refused. */ const removeMessage = (anyCaller: Caller, body: Record): unknown => { only(body, ['channel', 'message', 'reason'], 'remove_message'); // Every member is read before the channel is resolved, so a value // outside its form is refused naming it whatever the channel or the // credential (XTX-01). requiredString(body, 'channel'); const id = requiredString(body, 'message'); const reason = reasonOf(body); const c = moderatedChannel(anyCaller, body, 'remove_message'); const m = c.log.find((one) => one.id === id); if (m === undefined) refuse('message_not_found', 'no message of that id stands in this channel: name a message id a read of the channel answered'); if (m.kind === 'system') refuse('invalid_request', 'a system message is the service\'s own notice and is never removed: name a message a member sent'); if (m.removed_at !== undefined) return { channel: channelRecord(c), message: m }; m.text = REMOVED_TEXT; delete m.data; delete m.images; m.mentions = []; m.removed_at = instant(); const sender = m.sender as { id: string; handle: string }; const subject = participants.get(sender.id) ?? { id: sender.id, handle: sender.handle }; systemMessage(c, 'message_removed', subject, { message: m.id, reason }, true); return { channel: channelRecord(c), message: m }; }; /** `remove_member` (#7522's decision 3): puts an account out of a public * channel for a time or for good. Every participant of the named * participant's account leaves, the named one with a `member_removed` * notice carrying the reason and the end, each other with `left`, and * the account's `join` is refused `removed_from_channel` until the end. A * later call replaces the bar, and one whose `until` has passed lifts it, * posting nothing and moving no member. The owning account's participants * and the channel's moderators are refused `invalid_request`. */ const removeMemberCall = (anyCaller: Caller, body: Record): unknown => { only(body, ['channel', 'participant', 'reason', 'until'], 'remove_member'); // Every member is read before the channel is resolved (XTX-01). requiredString(body, 'channel'); const name = requiredString(body, 'participant'); const reason = reasonOf(body); let until: string | undefined; let untilMs = 0; if ('until' in body) { const given = requiredString(body, 'until'); const at = Date.parse(given); // RFC 3339: a date, a time to the second, and a zone, Z or an offset, // as the service reads it; a time with no zone is refused (XTX-23). if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(given) || Number.isNaN(at)) refuse('invalid_request', 'until must be an instant in RFC 3339 form, such as 2026-10-29T12:00:00Z: give one, or leave until out to remove for good'); until = new Date(at).toISOString(); untilMs = at; } const c = moderatedChannel(anyCaller, body, 'remove_member'); const p = participantByIdOrHandle(name); if (p === undefined || p.guest) refuse('participant_not_found', 'no live participant has that id or handle: name a member of the channel by its id or handle, as a read of the channel names it'); if (p.owner === c.owner) refuse('invalid_request', "the owning account's participants are never removed from its own channel: name a participant of another account"); if ((c.moderators ?? []).includes(p.id)) refuse('invalid_request', "a moderator is never removed: take it off the channel's moderators with channel.set first, then remove it"); const answer = { channel: channelRecord(c), participant: { id: p.id, handle: p.handle }, until: until ?? null, reason, removed: [] as string[] }; if (untilMs !== 0 && untilMs <= now()) { // A bar whose end has passed lifts the one that stands: no notice, no // member moved. c.bars?.delete(p.owner); answer.channel = channelRecord(c); return answer; } c.bars ??= new Map(); c.bars.set(p.owner, { ...(until !== undefined ? { until } : {}), untilMs, reason, at: instant() }); // The members of the account that leave, the named participant first // where it is one: the first leaving posts member_removed, its subject // the participant named, so every reader learns the reason; each other // leaving posts left (XTX-23). const others = [...c.members.keys()].map((id) => participants.get(id)).filter((q): q is Participant => q !== undefined && q.owner === p.owner && q.id !== p.id).sort((a, b) => (a.id < b.id ? -1 : 1)); const leaving = [...(c.members.has(p.id) ? [p] : []), ...others]; leaving.forEach((q, i) => { if (i === 0) { systemMessage(c, 'member_removed', p, { reason, ...(until !== undefined ? { until } : {}) }); c.members.delete(q.id); dropNamed(c, q.id); } else { removeMember(c, q); } answer.removed.push(q.handle); }); answer.channel = channelRecord(c); return answer; }; /** The name check (#7522's decision 6): the checker the test gives is * asked, and a refusal answers `name_refused` with its reason as the * checker gave it, the checker's sentence (XTX-05); with no checker, * the call stands unchecked. */ const checkName = async (check: TzmeshNameCheck): Promise => { if (options.channelNameChecker === undefined) return; const verdict = await options.channelNameChecker(check); if (!verdict.approved) refuse('name_refused', verdict.reason); }; /** Whom a report reaches (#7522's decision 5): each named moderator in * its order, or, where the list names none, the channel's creator and * the owning account's person, each once and never the reporter. */ const reportTargets = (c: Channel, reporter: Participant): Participant[] => { const out: Participant[] = []; const seen = new Set([reporter.id]); const add = (q: Participant | undefined): void => { if (q === undefined || seen.has(q.id) || q.guest) return; seen.add(q.id); out.push(q); }; if ((c.moderators ?? []).length > 0) { for (const id of c.moderators ?? []) add(participants.get(id)); return out; } if (c.creator !== undefined) add(participants.get(c.creator)); const person = [...participants.values()].find((q) => q.kind === 'human' && !q.guest && q.owner === c.owner); add(person); return out; }; /** `report_message` (#7522's decision 5): a member's flag of another * member's message of a public channel. To each target the double sends, * as the reporter's own send would go, a message whose data is the * reference {"report": {"channel", "message", "seq"}} and whose text is * the note alone, its client id `report:`, so a second * report creates nothing; a target that would refuse the send receives * none, and the answer counts the ones reached. */ const reportMessage = (caller: Caller, body: Record): unknown => { only(body, ['channel', 'message', 'note'], 'report_message'); if (caller.kind !== 'secret') refuse('credential_refused', 'report_message needs a member\'s secret, and this call carried a token: report as the participant that read the message, with the secret register answered'); noGuest(caller, 'report a message'); const named = requiredString(body, 'channel'); const id = requiredString(body, 'message'); // The note is optional, at most noteMax characters (XTX-23): an empty // one is admitted and read as none, so the report carries no text. let note = optionalString(body, 'note', constants.noteMax); if (note === '') note = undefined; if (note !== undefined) scanCredentials(note, 'the note'); const p = caller.participant; const c = channelOf(named, p); if (!c) refuse('channel_not_found', 'no channel you can reach has that id or name: name a public channel by its id or as #'); if (c.kind !== 'public') refuse('invalid_request', 'report_message applies to a public channel alone: in a private channel or a direct conversation, tell its members or its owner directly'); if (!c.members.has(p.id)) refuse('not_a_member', 'you are not a member of this channel, and a report is a member\'s: join it first, then report the message'); const m = c.log.find((one) => one.id === id); if (m === undefined) refuse('message_not_found', 'no message of that id stands in this channel: name a message id a read of the channel answered'); if (m.kind === 'system') refuse('invalid_request', 'a system message is the service\'s own notice and is never reported: name a message a member sent'); if (m.sender?.id === p.id) refuse('invalid_request', 'this message is your own, and a report is of another member\'s message: name a message another member sent'); const data = { report: { channel: c.id, message: m.id, seq: m.seq } }; let reached = 0; for (const target of reportTargets(c, p)) { try { send(caller, { to: target.id, data, client_id: `${TZMESH_REPORT_CLIENT_ID_PREFIX}${m.id}`, ...(note !== undefined ? { text: note } : {}) }, target); reached += 1; } catch (error) { // A moderator that would refuse the send receives none (XTX-06). if (error instanceof Refusal) continue; throw error; } } return { channel: channelRecord(c), message: m, reached }; }; /** removed_from_channel's detail: the channel, the bar's end or "for * good", and the reason (#7522's decision 3). */ const barDetail = (c: Channel, bar: { until?: string; reason: string }): string => `your account was removed from ${channelAddress(c)} ${bar.until === undefined ? 'for good' : `until ${bar.until}`}: ${bar.reason}`; /** The credential scan (XTX-02): a body's free text, a message's text and * data and a description, carries no value in one of * the platform's credential forms, a participant secret among them. */ const scanCredentials = (text: string, what: string): void => { for (const prefix of TZMESH_CREDENTIAL_PREFIXES) if (text.includes(prefix)) refuse('credential_in_text', `${what} holds what looks like a Turn Zero credential, which never travels as content: remove it, then make the call again without it`); }; /** A send's `to` with a held handle routed to its account's current one * (XTX-21): the held handle itself, or its prefix of `/`. */ const routeHeld = (to: string | undefined): string | undefined => { if (to === undefined) return undefined; const slash = to.indexOf('/'); const head = slash < 0 ? to : to.slice(0, slash); const account = heldBy(head); if (account === undefined) return to; return slash < 0 ? account.handle : `${account.handle}${to.slice(slash)}`; }; const send = (caller: Caller, body: Record, reportTarget?: Participant): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "send needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first"); only(body, ['channel', 'to', 'text', 'data', 'images', 'reply_to', 'client_id'], 'send'); const p = caller.participant; const clientId = requiredString(body, 'client_id', constants.clientIdMax); const channelId = optionalString(body, 'channel'); // A held handle, or `/`, reaches its account's current // handle while the hold stands (XTX-06; XTX-21); an @ before a handle is // its mark (XTX-06). const asked = optionalString(body, 'to'); const marked = asked !== undefined && asked.startsWith('@'); const to = routeHeld(marked ? asked.slice(1) : asked); if ((channelId === undefined) === (to === undefined)) refuse('invalid_request', 'send takes exactly one of channel and to: name channel to post in a conversation, or to for a participant or an account'); const text = optionalString(body, 'text'); if (text !== undefined && byteLength(text) > constants.textBytesMax) refuse('invalid_request', `text is longer than ${constants.textBytesMax} bytes: shorten it or split it across several messages`); const dataText = body.data === undefined ? undefined : JSON.stringify(body.data); if (dataText !== undefined && byteLength(dataText) > constants.dataBytesMax) refuse('invalid_request', `data is larger than ${constants.dataBytesMax} bytes: send less data in one message`); let images: TzmeshImageReference[] | undefined; if (body.images !== undefined) { if (!Array.isArray(body.images)) refuse('invalid_request', 'images must be a list of image references: send an array of objects, each with the id the image call answered'); if (body.images.length > constants.imagesPerMessageMax) refuse('invalid_request', `images holds more than ${constants.imagesPerMessageMax} references: send at most that many in one message`); images = body.images.map((image) => ({ id: imageForm(image, 'images'), bytes: 0, width: 0, height: 0 })); } if (text === undefined && dataText === undefined && (images === undefined || images.length === 0)) refuse('invalid_request', 'send carries no content: give it text, data, or images'); if (images !== undefined && images.length > 0) { noGuest(caller, 'send images'); // Each reference names a stored image, and the message carries the // stored reference (XTX-19). images = images.map((image) => storedImage(image.id, 'images')); } // A guest opens at most `guestConversationsMax` conversations (XTX-16). const guestMayOpen = (): void => { if (!p.guest) return; const open = [...channels.values()].filter((x) => x.members.has(p.id)).length; if (open >= constants.guestConversationsMax) refuse('cap_reached', `a guest can hold at most ${constants.guestConversationsMax} conversations, and this message would open another: sign in with a Turn Zero account to hold more`); }; // The channel: named, or a direct message to a participant (XTX-06). let c: Channel | undefined; if (channelId !== undefined) { c = channelOf(channelId, p); if (!c) refuse('channel_not_found', 'no channel you can reach has that id or name: name it by its id, as # for a public one, or as /# for a private one'); // A message into a conversation that stands, by its id, meets the agent's // queue alone (agentAdmits). if (c.kind === 'direct' && c.members.has(p.id)) agentAdmits(p, directPeer(c, p.id), false, false); } else if (to !== undefined) { // A `to` may carry its kind's mark (XTX-06): @ before an account's or // a participant's handle, # for a public channel, and // /# for a private one; a mark that does not match what // holds the name is refused participant_not_found. A private one // resolves by the account rule the channel member reads (channelOf; // invitedAccount): to a sender its account admits that is no member // yet it answers not_a_member below, and to anyone else it reads as // absent (XTX-05). const notFound = `no participant named ${to} stands that you can reach: find the right handle with directory`; if (!marked && (to.startsWith('#') || to.includes('/#'))) { if (to.startsWith('#')) c = [...channels.values()].find((x) => x.kind === 'public' && x.name === to.slice(1)); else c = channelOf(to, p); if (!c) refuse('participant_not_found', notFound); } if (c === undefined) { // A report's send names its moderator as the participant itself, // never through an account's handle, so it reaches that participant // and not the account's answered_by agent (#7522's decision 5). let target = reportTarget ?? participantByHandle(to); // An account's handle names its person, or the agent the account // names as answered_by, whatever its visibility, for a sender outside // the account (XTX-06; XTX-21); the account's own reach its person. let accountNamed = false; const account = reportTarget === undefined ? accountByKey(to) : undefined; if (account !== undefined && (target === undefined || (target.kind === 'human' && !target.guest && target.owner === account.handle))) { accountNamed = true; if (p.owner !== account.handle && !p.guest) target = answeringAgent(account) ?? target; } if (!target) refuse('participant_not_found', notFound); const reached: Participant = target; if (reached.id === p.id) refuse('invalid_request', 'to names you, and you cannot message yourself: name another participant or account'); const standing = [...channels.values()].some((x) => x.kind === 'direct' && x.members.has(p.id) && x.members.has(reached.id) && x.members.size === 2); if (!standing) { // The admission step (XTX-06; XTX-L0-09): a private session or // agent admits its owner's participants, its team's accounts, and, // as an account's answered_by, a sender messaging the account's // handle; anyone else reads it as absent. if (p.guest && accountNamed) guestMayReach(p, reached, true); if (!admitted(p, reached, accountNamed)) refuse('participant_not_found', notFound); guestMayReach(p, reached, accountNamed); guestMayOpen(); } // An agent's own rules before the conversation stands (agentAdmits). agentAdmits(p, reached, !standing, accountNamed); c = directChannel(p, reached); } } if (!c) refuse('invalid_request', "send needs a channel or a to: name the conversation's channel, or the participant to message in to"); // A guest sends messages in direct conversations alone, never in a channel (XTX-16). if (c.kind !== 'direct') noGuest(caller, 'post in a channel'); if (!c.members.has(p.id)) refuse('not_a_member', 'you are not a member of this channel: join it first, or message a participant directly with send and to'); // A resend with the same client id answers the message created first, // before the announce check, so a message taken while its sender could // post stands whatever the channel's mode now is (XTX-06). const earlier = c.log.find((m) => m.kind === 'message' && m.sender?.id === p.id && m.client_id === clientId); if (earlier) return { message: earlier, channel: channelRecord(c), delivery: { connected: 0, waiting: 0 } }; // An announce channel takes its posters' messages alone, a reader's send // refused after not_a_member and the resend and before the content's // checks (XTX-06). if (c.mode === 'announce' && !poster(c, p)) refuse('channel_read_only', readOnlyDetail(c, p)); if (text !== undefined) scanCredentials(text, 'the message'); if (dataText !== undefined) scanCredentials(dataText, 'the message'); const replyTo = optionalString(body, 'reply_to'); if (replyTo !== undefined && !c.log.some((m) => m.id === replyTo)) refuse('message_not_found', 'reply_to names no message in this conversation: give the id of a message read answered for it, or leave reply_to out'); if (p.guest) { // A guest's lower ceilings, and no account to count (XTX-16). count(`participant:${p.id}`, constants.guestTextsPerMinute, 'messages from this guest'); countDay(`guest-day:${p.id}`, constants.guestTextsPerDay, 'messages from this guest'); } else { count(`participant:${p.id}`, constants.textsPerMinutePerParticipant, 'messages from this participant'); count(`account:${p.owner}`, constants.textsPerMinutePerAccount, 'messages from this account'); } count(`channel:${c.id}`, constants.textsPerMinutePerChannel, 'messages in this channel'); if (images !== undefined && images.length > 0) count(`images:${p.id}`, constants.imagesPerMinutePerParticipant, 'images from this participant'); if (c.kind === 'direct') { // The conversation sticks to the sending listener of an agent, so a // conversation the agent opens goes to the listener that sent its first // message, and to a listener of an agent it reaches (route; XTX-06). if (routed(p) && caller.listener !== undefined) assign(c, p, caller.listener); const peer = directPeer(c, p.id); if (routed(peer)) route(c, peer); } // Each @/ and @ of the text; @/# // names a private channel and mentions no one (XTX-06). const mentions = [...(text ?? '').matchAll(/(?:^|[^A-Za-z0-9_/-])@([A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)?)(\/#)?/g)].filter((m) => m[2] === undefined).map((m) => m[1]); const connectedBefore = [...c.members.keys()].filter((id) => id !== p.id).map((id) => participants.get(id)).filter((m): m is Participant => m !== undefined && presenceOf(m) === 'connected').length; const waiting = c.members.size - 1 - connectedBefore; const stored = append(c, { kind: 'message', sender: { id: p.id, handle: p.handle }, ...(text !== undefined ? { text } : {}), ...(body.data !== undefined ? { data: body.data } : {}), ...(images !== undefined ? { images } : {}), mentions, ...(replyTo !== undefined ? { reply_to: replyTo } : {}), client_id: clientId, }); // A taken send sets the participant's last send (XTX-04). p.lastSend = now(); return { message: stored, channel: channelRecord(c), delivery: { connected: connectedBefore, waiting: Math.max(0, waiting) } }; }; /** The messages past a participant's cursors, the channels a listener of * an agent reads alone (visibleTo), the cursors the agent's. */ const unread = (p: Participant, l: Listener | undefined, channelFilter: Channel | null, limit: number): { messages: TzmeshMessage[]; cursors: Record; more: boolean } => { const cursors: Record = {}; const messages: TzmeshMessage[] = []; let more = false; const list = channelFilter ? [channelFilter] : [...channels.values()].filter((c) => c.members.has(p.id) && visibleTo(c, p, l)); for (const c of list) { const acknowledged = c.members.get(p.id) ?? 0; cursors[c.id] = { acknowledged, newest: c.log.length }; for (const m of c.log.slice(acknowledged)) { if (m.kind === 'message' && m.sender?.id === p.id) continue; if (messages.length >= limit) { more = true; break; } messages.push(m); } } return { messages, cursors, more }; }; /** A reader's key: the participant, or the agent's listener a read is * under, each listener reading its own conversations (XTX-09). */ const readerKey = (p: Participant, l: Listener | undefined): string => (l === undefined ? p.id : `${p.id}/${l.id}`); /** Whether an answer carries a message newer than every one an earlier * read answered the reader: one past the reader's mark in its channel * (XTX-07). */ const carriesNew = (p: Participant, l: Listener | undefined, messages: TzmeshMessage[]): boolean => { const marks = answered.get(readerKey(p, l)); return messages.some((m) => m.seq > (marks?.get(m.channel) ?? 0)); }; /** Raise the reader's mark in each answered message's channel (XTX-07). */ const markAnswered = (p: Participant, l: Listener | undefined, messages: TzmeshMessage[]): void => { if (messages.length === 0) return; const key = readerKey(p, l); const marks = answered.get(key) ?? new Map(); answered.set(key, marks); for (const m of messages) if (m.seq > (marks.get(m.channel) ?? 0)) marks.set(m.channel, m.seq); }; const read = async (caller: Caller, body: Record, init: TzmeshTransportInit): Promise => { only(body, ['channel', 'wait', 'limit', 'last'], 'read'); const wait = optionalInteger(body, 'wait', 0, constants.waitSecondsMax) ?? 0; const limit = optionalInteger(body, 'limit', 1, constants.readLimitMax) ?? constants.readLimitDefault; const last = optionalInteger(body, 'last', 1, constants.readLimitMax); const channelId = optionalString(body, 'channel'); let c: Channel | null = null; if (channelId !== undefined) { c = channelOf(channelId, caller.kind === 'secret' ? caller.participant : undefined) ?? null; if (!c) refuse('channel_not_found', 'no channel you can reach has that id or name: name it by its id, as # for a public one, or as /# for a private one'); } // The public reading (XTX-07): a public channel's newest messages, with // `last`, answered to any caller, member or not, under no credential too. if (last !== undefined && c !== null && c.kind === 'public' && (caller.kind !== 'secret' || !c.members.has(caller.participant.id))) { const messages = c.log.slice(Math.max(0, c.log.length - last)); if (caller.kind === 'secret') caller.participant.lastRead = now(); return { messages, cursors: {}, more: false, waited_ms: 0 }; } // The owner's reading (XTX-07): under the account's human participant's // secret alone, `last` naming a direct conversation one of the account's // live agents or sessions is a member of, the person no member, answers // its newest messages with `cursors` empty, holds no wait, and moves no // member's cursor, last read, or answered mark; the reader's own last // read moves as at the public reading (XTX-04). if ( last !== undefined && c !== null && c.kind === 'direct' && caller.kind === 'secret' && caller.participant.kind === 'human' && !caller.participant.guest && !c.members.has(caller.participant.id) && [...c.members.keys()].some((id) => { const member = participants.get(id); return member !== undefined && ownAgentOrSession(member, caller.participant.owner); }) ) { const messages = c.log.slice(Math.max(0, c.log.length - last)); caller.participant.lastRead = now(); return { messages, cursors: {}, more: false, waited_ms: 0 }; } if (caller.kind === 'anonymous') refuse('credential_required', "read with no credential reads only a public channel's newest messages: name channel and last, or send the participant secret register answered to read your own messages"); if (caller.kind !== 'secret') refuse('credential_refused', "read needs a participant's secret, and this call carried a token: use the secret register answered, or with no credential read a public channel's newest messages by naming channel and last"); const p = caller.participant; if (c !== null && !c.members.has(p.id)) refuse('not_a_member', 'you are not a member of this channel: join it first, or for a private one ask a member to invite you'); if (last !== undefined) { if (!c) refuse('invalid_request', 'last applies to one channel alone: name channel beside last, or leave last out'); const messages = c.log.slice(Math.max(0, c.log.length - last)); // A member's read of the newest messages answers them too, so a held // read after it waits for a newer one (XTX-07). markAnswered(p, caller.listener, messages); p.lastRead = now(); return { messages, cursors: { [c.id]: { acknowledged: c.members.get(p.id) ?? 0, newest: c.log.length } }, more: false, waited_ms: 0 }; } const started = now(); const l = caller.listener; let answer = unread(p, l, c, limit); // waited_ms counts the hold alone (XTX-07): a read that answers at once // waited 0, whatever the clock read between its start and its answer. let held = false; // A held read answers at once where its answer carries a message newer // than every one an earlier read answered the reader, and otherwise // holds for a new one for its whole wait, answering the earlier ones // beside it or at the wait's end (XTX-07). if (wait > 0 && !carriesNew(p, l, answer.messages)) { held = true; // The hold honours the caller's abort signal (XTX-14): the timer is // cleared, the waiter dropped, and the hold released so presence // follows, and the call ends with the signal's reason as the service's // closed connection would. let aborted = false; p.holds += 1; if (l !== undefined) l.holds += 1; try { await new Promise((resolve) => { const waiter: Waiter = { participant: p.id, ...(l !== undefined ? { listener: l.id } : {}), channels: c ? new Set([c.id]) : null, wake: () => {} }; const timer = setTimeout(() => waiter.wake(), wait * 1000); const onAbort = (): void => { aborted = true; waiter.wake(); }; waiter.wake = () => { clearTimeout(timer); waiters.delete(waiter); init.signal?.removeEventListener('abort', onAbort); resolve(); }; waiters.add(waiter); if (init.signal?.aborted) onAbort(); else init.signal?.addEventListener('abort', onAbort, { once: true }); }); } finally { p.holds -= 1; if (l !== undefined) l.holds -= 1; } if (aborted) throw init.signal?.reason ?? new Error('aborted'); // A session listener another session's attach replaced during the hold // is refused with the replacement's sentences (XTX-02). const successor = l !== undefined && !p.listeners.includes(l) ? replaced.get(caller.value) : undefined; if (successor !== undefined) refuse('credential_refused', replacedSentence(successor)); answer = unread(p, l, c, limit); } markAnswered(p, l, answer.messages); // A read that answers sets the participant's last read, after every // refusal, a held one's at the hold's end (XTX-04). p.lastRead = now(); // A listener's read naming no channel that answers nothing names the // conversations another of the participant's listeners answers that hold // unread messages (XTX-07). const waited = held ? Math.max(0, now() - started) : 0; const elsewhere = c === null && answer.messages.length === 0 ? assignedElsewhere(p, l) : []; return { ...answer, waited_ms: waited, ...(elsewhere.length > 0 ? { assigned_elsewhere: elsewhere } : {}) }; }; /** Each direct conversation another listener of a routed participant * answers that holds an unread message for it, its own passed over: the * channel's id and the count, in the channels' order (XTX-07). */ const assignedElsewhere = (p: Participant, l: Listener | undefined): { channel: string; unread: number }[] => { if (l === undefined || !routed(p)) return []; const out: { channel: string; unread: number }[] = []; for (const c of channels.values()) { if (!c.members.has(p.id) || visibleTo(c, p, l)) continue; const acknowledged = c.members.get(p.id) ?? 0; const unread = c.log.slice(acknowledged).filter((m) => !(m.kind === 'message' && m.sender?.id === p.id)).length; if (unread > 0) out.push({ channel: c.id, unread }); } return out; }; const ack = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "ack needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first"); only(body, ['channel', 'seq'], 'ack'); const p = caller.participant; const c = channelOf(requiredString(body, 'channel'), p); if (!c) refuse('channel_not_found', 'no channel you can reach has that id or name: name it by its id, as # for a public one, or as /# for a private one'); if (!c.members.has(p.id)) refuse('not_a_member', 'you are not a member of this channel: join it first, or for a private one ask a member to invite you'); const seq = body.seq; if (typeof seq !== 'number' || !Number.isInteger(seq) || seq < 0) refuse('invalid_request', 'seq must be a whole number: send the seq of the newest message you read'); if (seq > c.log.length) refuse('invalid_request', `seq is past this channel's newest message, ${c.log.length}: send a seq of ${c.log.length} or less`); const acknowledged = c.members.get(p.id) ?? 0; if (seq > acknowledged) c.members.set(p.id, seq); // An ack sets the last read as a read does (XTX-04). p.lastRead = now(); return { channel: c.id, acknowledged: c.members.get(p.id) ?? 0 }; }; const directory = (caller: Caller, body: Record): unknown => { only(body, ['query', 'handle', 'kind'], 'directory'); const query = (optionalString(body, 'query') ?? '').toLowerCase(); // `handle` names one handle or name exactly, ignoring case: the lookup a // public page reads (XTX-10). const exact = optionalString(body, 'handle')?.toLowerCase(); const kind = optionalString(body, 'kind'); if (kind !== undefined && !['account', 'standing', 'channel'].includes(kind)) refuse('invalid_request', 'kind must be account, standing, or channel: leave it out to search all three'); // The caller's own account reads its own private participants by their // exact handles (XTX-10), and a signed-in caller, under a token or a // secret that is no guest's, reads an account's public list. const signedIn = caller.kind === 'token' || (caller.kind === 'secret' && !caller.participant.guest); const viewerAccount = caller.kind === 'token' ? tokenAccount(caller.token).handle : caller.kind === 'secret' && !caller.participant.guest ? caller.participant.owner : undefined; const entries: Array> = []; const matches = (...fields: Array): boolean => { if (exact !== undefined) return (fields[0] as string | undefined)?.toLowerCase() === exact; return query === '' || fields.some((f) => (Array.isArray(f) ? f : [f ?? '']).some((v) => v.toLowerCase().includes(query))); }; if (kind === undefined || kind === 'account') { // An account that has adopted no handle is no entry. One its owner // keeps unlisted, and a handle held for its account after a change, // answer the exact read alone, as `reserved` (XTX-10; XTX-21). for (const a of accounts.values()) { if (!a.adopted || !matches(a.handle)) continue; if (a.unlisted) { if (exact !== undefined) entries.push({ kind: 'reserved', handle: a.handle }); continue; } // The exact read of a listed account's handle, under a signed-in // credential, answers its public list as the account entry's own // member, so a kind filter answers what it names (XTX-10; // XTX-L0-11): its public agents and public sessions by handle, kind, // title, and presence, an agent whose owner is private left out. No // credential and a guest's key read the entry alone. const entry: Record = { kind: 'account', handle: a.handle }; if (exact !== undefined && signedIn) { entry.public = liveParticipants() .filter((p) => p.owner === a.handle && (p.kind === 'standing' || p.kind === 'session') && visibilityOf(p) === 'public' && p.owner_visibility !== 'private') .map((p) => ({ handle: p.handle, kind: p.kind, ...(p.title !== undefined ? { title: p.title } : {}), presence: presenceOf(p) })); } entries.push(entry); } if (exact !== undefined) { const held = heldBy(exact); if (held?.held !== undefined) entries.push({ kind: 'reserved', handle: held.held.handle }); } } if (kind === undefined || kind === 'standing') { for (const p of liveParticipants()) { if (p.kind !== 'standing' || !matches(p.handle, p.description, p.tags)) continue; // A private agent is out of every query, and its exact read answers // its own account alone (XTX-10; XTX-L0-03). if (visibilityOf(p) === 'private' && (exact === undefined || p.owner !== viewerAccount)) continue; // The entry carries the agent's queue, and its visibility and its // owner's as its record answers them, as the service's does (XTX-10). const entry: Record = { kind: 'standing', handle: p.handle, ...(p.title !== undefined ? { title: p.title } : {}), description: p.description, presence: presenceOf(p), queue: p.queueOff !== true, visibility: visibilityOf(p), owner_visibility: p.owner_visibility }; // A one-way agent's sentence beside its queue (XTX-10). if (p.oneWay !== undefined) entry.one_way = p.oneWay; if (p.tags) entry.tags = [...p.tags]; if (p.website) entry.website = p.website; if (p.social) entry.social = [...p.social]; if (p.image) entry.image = { ...p.image }; if (p.owner_visibility === 'public') entry.owner = p.owner; entries.push(entry); } } if (kind === undefined || kind === 'channel') { for (const c of channels.values()) { // A channel its owner unlisted is out of the list and every query, // and its exact read still answers it, as the service's does // (XTX-05; XTX-10). if (c.kind !== 'public' || (c.unlisted === true && exact === undefined) || !matches(c.name, c.description)) continue; // The entry carries the channel's mode as its channel record answers it (XTX-05), as the // service's does (XTX-10). const entry: Record = { kind: 'channel', name: c.name, members: c.members.size, owner: channelOwner(c), mode: c.mode ?? 'open' }; if (c.description) entry.description = c.description; entries.push(entry); } } return { entries }; }; const exportAccount = (anyCaller: Caller, body: Record): unknown => { const caller = credentialed(anyCaller, 'export'); noGuest(caller, 'export, since a guest holds no account'); only(body, [], 'export'); const accountHandle = callerAccount(caller); // The owner's credential, its token or its person's secret, exports the // whole account; an agent's or a session's secret exports that // participant alone and the channels it is a member of, never a sibling // agent's conversation nor the person's (XTX-10; #7727). const ownerView = caller.kind !== 'secret' || caller.participant.kind === 'human'; const own = ownerView ? [...participants.values()].filter((p) => p.owner === accountHandle) : [caller.participant]; const ownIds = new Set(own.map((p) => p.id)); const viewer = caller.kind === 'secret' ? caller.participant : own[0]; return { participants: own.map((p) => participantRecord(p, viewer)), // A channel one of the exported participants is a member of, every // conversation of the account's agents among them whichever listener // answered it under the owner's credential (XTX-10; XTX-L0-11). channels: [...channels.values()] .filter((c) => [...c.members.keys()].some((id) => ownIds.has(id))) .map((c) => ({ channel: channelRecord(c), messages: [...c.log] })), }; }; // --- the site's calls (XTX-16) ------------------------------------------- /** `guest`, under no credential: a human participant of no account, its * handle `guest/` and its owner `guest` (XTX-16). Its body is * empty, or carries `to` and `text`, a first message sent as `send` sends it * under the new guest, with an optional `wait` for the other side's first * answer; a first message refused mints nothing (firstText). */ const guest = async (body: Record, init: TzmeshTransportInit): Promise => { only(body, ['to', 'text', 'wait'], 'guest'); const first = 'to' in body || 'text' in body; let to = ''; let text = ''; if (first) { if (!('to' in body) || !('text' in body)) refuse('invalid_request', 'guest takes to and text together for a first message, or neither: add the missing one, or leave both out'); to = requiredString(body, 'to'); text = requiredString(body, 'text'); } let wait = 0; if ('wait' in body) { if (!first) refuse('invalid_request', 'wait applies to a first message alone: send to and text with it, or leave wait out'); wait = optionalInteger(body, 'wait', 0, constants.waitSecondsMax) ?? 0; } const before = counter; const id = nextId('p'); const participant: Participant = { id, kind: 'human', handle: `guest/${id.replace('_', '-')}`, global: false, description: '', owner: 'guest', owner_visibility: 'public', secret: mintSecret(), listeners: [], nextListener: 0, lastCall: now(), ended: false, holds: 0, guest: true, }; participants.set(participant.id, participant); secrets.set(participant.secret, participant.id); if (!first) return { participant: participantRecord(participant, participant), secret: participant.secret }; const sent = firstText(participant, to, text, before); const reply = wait > 0 ? await awaitReply(participant, sent.message.channel, sent.message.seq, wait, init) : null; return { participant: participantRecord(participant, participant), secret: participant.secret, message: sent.message, channel: sent.channel, reply }; }; /** A guest's first message, sent as `send` sends it, through its path and its * checks, under the guest the call just minted (XTX-06; XTX-16). A * refusal takes the mint back, every channel the send opened, the counts * it began, the participant, its secret, and the ids the call drew, since * a refused call changes nothing (XTX-01). */ const firstText = (p: Participant, to: string, text: string, before: number): { message: TzmeshMessage; channel: TzmeshChannel } => { const channelsBefore = new Set(channels.keys()); try { return send({ kind: 'secret', participant: p, value: p.secret }, { to, text, client_id: TZMESH_GUEST_CLIENT_ID }) as { message: TzmeshMessage; channel: TzmeshChannel }; } catch (error) { if (error instanceof Refusal) { for (const id of [...channels.keys()]) { if (channelsBefore.has(id)) continue; channels.delete(id); rates.delete(`channel:${id}`); } participants.delete(p.id); secrets.delete(p.secret); rates.delete(`participant:${p.id}`); rates.delete(`guest-day:${p.id}`); counter = before; } throw error; } }; /** A guest's first message's wait (XTX-16): the other side's first message in * the conversation past `seq`, at once where one stands or the first to * arrive within `wait` seconds, held as a waiting read is, the guest * connected while it holds; null where none arrives in time or the guest * leaves the conversation. The caller's abort ends it as it ends a held * read (XTX-14). */ const awaitReply = async (p: Participant, channelId: string, seq: number, wait: number, init: TzmeshTransportInit): Promise => { const deadline = Date.now() + wait * 1000; for (;;) { const c = channels.get(channelId); if (c === undefined || !c.members.has(p.id) || p.ended) return null; const answered = c.log.slice(seq).find((m) => m.kind === 'message' && m.sender !== undefined && m.sender.id !== p.id); if (answered !== undefined) return answered; const hold = deadline - Date.now(); if (hold <= 0) return null; let aborted = false; p.holds += 1; try { await new Promise((resolve) => { const waiter: Waiter = { participant: p.id, channels: new Set([channelId]), wake: () => {} }; const timer = setTimeout(() => waiter.wake(), hold); const onAbort = (): void => { aborted = true; waiter.wake(); }; waiter.wake = () => { clearTimeout(timer); waiters.delete(waiter); init.signal?.removeEventListener('abort', onAbort); resolve(); }; waiters.add(waiter); if (init.signal?.aborted) onAbort(); else init.signal?.addEventListener('abort', onAbort, { once: true }); }); } finally { p.holds -= 1; } if (aborted) throw init.signal?.reason ?? new Error('aborted'); } }; /** The key one network address is counted under (XTX-16): an IPv6 address * by its /64 prefix, an IPv4 address, a mapped one among them, as it is. */ const addressKey = (address: string): string => { const bare = address.replace(/%.*$/, '').toLowerCase(); const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(bare); if (mapped !== null) return mapped[1]; if (!bare.includes(':')) return bare; const halves = bare.split('::'); if (halves.length > 2) return bare; const left = halves[0] === '' ? [] : halves[0].split(':'); const right = halves.length === 1 || halves[1] === '' ? [] : halves[1].split(':'); const groups = halves.length === 1 ? left : [...left, ...new Array(Math.max(0, 8 - left.length - right.length)).fill('0'), ...right]; return `${groups.slice(0, 4).map((group) => parseInt(group, 16).toString(16)).join(':')}::/64`; }; /** The per-address bound on guests (XTX-16): where the test gives the * address seam, one address mints at most `guestsPerAddressPerDay` * guests in a UTC day, refused `rate_limited` before the call reads its * body; answers the count's key, which a mint then counts against. */ const admitGuestAddress = (): string | undefined => { if (options.address === undefined) return undefined; const key = `guests-address:${addressKey(options.address())}`; const row = rates.get(key); if (row !== undefined && row.window === Math.floor(now() / 86_400_000) && row.count >= constants.guestsPerAddressPerDay) { refuse('rate_limited', `this address has started its ${constants.guestsPerAddressPerDay} guests of the day, the most a day allows: wait until midnight UTC, when the count starts again, or sign in with a Turn Zero account`); } return key; }; const countGuestAddress = (key: string): void => { const window = Math.floor(now() / 86_400_000); const row = rates.get(key); if (row === undefined || row.window !== window) rates.set(key, { window, count: 1 }); else row.count += 1; }; /** The account's one human participant with a fresh secret (XTX-16), its * handle the account's own, or the account's id until the account adopts * one, minted at the first call. `marked` is the site's sign-in leg's mark * (the `signIn` seam), which a `human` call never sets; `application` is * the mark of a secret answered under an application's token, never set * with `marked`; `guestSecret` names a guest whose conversations join the * account's participant. */ /** An account's human participant with no secret, the person's record a * sign-in or an invite makes first (XTX-05; XTX-16). */ const mintHuman = (account: Account): Participant => { const p: Participant = { id: nextId('p'), kind: 'human', handle: account.handle, global: true, description: '', owner: account.handle, owner_visibility: 'public', secret: '', listeners: [], nextListener: 0, lastCall: now(), ended: false, holds: 0, guest: false, }; participants.set(p.id, p); return p; }; const humanFor = (account: Account, marked: boolean, application: boolean, guestSecret?: string): { participant: TzmeshParticipant; secret: string; merged: number } => { let p = [...participants.values()].find((q) => q.kind === 'human' && !q.guest && q.owner === account.handle); if (!p) { p = mintHuman(account); // The person joins the account's default channel at its minting // (XTX-05). joinAll(account, p); } else if (account.all === undefined) { joinAll(account); } const holder = p; // The guest is read before anything changes, so a refused merge changes // nothing (XTX-01). let merging: Participant | undefined; if (guestSecret !== undefined) { const guestId = secrets.get(guestSecret); merging = guestId === undefined ? undefined : participants.get(guestId); if (!merging || !merging.guest || merging.ended || presenceOf(merging) === 'ended') refuse('credential_refused', 'guest names no live guest: pass the secret the guest call answered, or leave guest out to sign in without merging one'); } const secret = mintSecret(); holder.secret = secret; holder.lastCall = now(); secrets.set(secret, holder.id); humanSecrets.set(secret, { lastUsed: now(), ended: false, marked: marked && !application, application }); let merged = 0; if (merging !== undefined) { const g = merging; for (const c of channels.values()) { if (!c.members.has(g.id)) continue; const cursor = c.members.get(g.id) ?? 0; c.members.delete(g.id); if (!c.members.has(holder.id)) c.members.set(holder.id, cursor); // The guest's messages read from then on as the account's participant's. for (const m of c.log) if (m.sender?.id === g.id) m.sender = { id: holder.id, handle: holder.handle }; merged += 1; } g.ended = true; g.endedAt = now(); secrets.delete(g.secret); for (const stream of streams) if (stream.participant === g.id) stream.end(); for (const waiter of waiters) if (waiter.participant === g.id) waiter.wake(); } return { participant: participantRecord(holder, holder), secret, merged }; }; /** `human`, under a token: the account's one human participant with a * fresh secret, which carries no sign-in mark; an account that has * adopted no handle is answered too, under its id, and its messages meet * `handle_required` (XTX-16). Under an application's token the secret * carries the application's mark, and `guest` is refused: merging a * guest into the account is the site's own sign-in's alone. */ const human = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'token') refuse('credential_refused', "human needs a Turn Zero token, and this call carried a participant secret: call human with a token of the person's account"); only(body, ['guest'], 'human'); const application = caller.token.applicationSignIn; if (application && body.guest !== undefined) refuseApplication('merging a guest into the account'); return humanFor(tokenAccount(caller.token), false, application, optionalString(body, 'guest')); }; // --- the account's handle and its two choices (XTX-21) ------------------- /** Rename every record an account owns from `from` to `to`: the walk a * first handle and a change share. The account's participants, their * prefixed handles and its human participant's own, the listeners it * attached, its channels, images, and rates follow, and each * message names its sender and subject by the participant's current * handle, as a read resolves it (XTX-21). */ const renameAccount = (account: Account, from: string, to: string): void => { const prefixed = (handle: string): string => (handle.startsWith(`${from}/`) ? `${to}${handle.slice(from.length)}` : handle); account.handle = to; account.adopted = true; const renamed = new Map(); for (const p of participants.values()) { if (p.owner !== from) continue; p.owner = to; const handle = p.kind === 'human' ? to : prefixed(p.handle); if (handle !== p.handle) { p.handle = handle; renamed.set(p.id, handle); } } for (const c of channels.values()) { if (c.owner === from) c.owner = to; // A bar names the account by its handle and follows it (#7522). const bar = c.bars?.get(from); if (bar !== undefined && c.bars !== undefined) { c.bars.delete(from); c.bars.set(to, bar); } for (const m of c.log) { if (m.sender !== undefined && renamed.has(m.sender.id)) m.sender = { id: m.sender.id, handle: renamed.get(m.sender.id) as string }; if (m.subject !== undefined && renamed.has(m.subject.id)) m.subject = { id: m.subject.id, handle: renamed.get(m.subject.id) as string }; } } // The listeners the account attached, on its own agents and its teams', // and the replaced session listeners whose successor it attached. for (const p of participants.values()) for (const l of p.listeners) if (l.account === from) l.account = to; for (const [secret, successor] of replaced) if (successor === from) replaced.set(secret, to); for (const image of storedImages.values()) if (image.account === from) image.account = to; const rate = rates.get(`account:${from}`); if (rate !== undefined) { rates.delete(`account:${from}`); rates.set(`account:${to}`, rate); } }; /** The account as `account.set` answers it (XTX-21). */ const accountRecord = (a: Account): TzmeshAccount => { const record: TzmeshAccount = { handle: a.handle, unlisted: a.unlisted, public: a.public }; const answering = a.answeredBy === undefined ? undefined : participants.get(a.answeredBy); if (answering !== undefined) record.answered_by = answering.handle; if (a.held !== undefined && a.held.until > now()) record.held = { handle: a.held.handle, ends_at: new Date(a.held.until).toISOString() }; return record; }; /** A handle an account may take (XTX-21): the global name's pattern, no * other account's handle in any case or a handle held for it, no company * name or look-alike, and none of the account's own agents' or reserved * names. */ const checkHandle = (account: Account, handle: string): void => { if (!TZMESH_GLOBAL_NAME_PATTERN.test(handle)) refuse('invalid_request', `a handle must be ${constants.globalNameMin} to ${constants.nameMax} letters, digits, and hyphens, beginning and ending with a letter or a digit: choose one of that form`); if (reservedName(handle)) refuse('name_reserved', `${handle} is a name the service keeps for itself: choose another name`); const holder = claimHolder(handle, (h) => h === account.handle); if (holder !== undefined && holder !== account.handle) refuse('name_reserved', takenName(handle, holder)); if (liveParticipants().some((q) => q.owner === account.handle && q.kind !== 'human' && q.global && q.handle.toLowerCase() === handle.toLowerCase())) refuse('handle_taken', `${handle} is already the name of one of your account's agents: choose another name`); if (account.reserved.has(handle)) refuse('handle_taken', `${handle} is one of your account's reserved names: choose another name`); // One name, one kind within the pool (XTX-21). if (publicChannelsFolded(handle.toLowerCase()).some((c) => c.owner === account.handle)) refuse('handle_taken', `${handle} is already the name of one of your account's public channels: choose another name`); }; /** `answered_by` (XTX-21): one of the account's own live agents by handle * or id, or null, which clears it; read before any change. */ const answeredByOf = (account: Account, body: Record): string | undefined => { if (body.answered_by === null) return undefined; const named = typeof body.answered_by === 'string' ? body.answered_by : ''; const agent = named === '' ? undefined : participantByIdOrHandle(named); if (agent === undefined || agent.kind !== 'standing' || agent.owner !== account.handle || agent.ended || presenceOf(agent) === 'ended') refuse('participant_not_found', "answered_by names none of your account's live agents: name one by its handle, or pass null to clear it"); // A one-way agent takes no messages, so it never answers the account's // handle (XTX-21). if (agent.oneWay !== undefined) refuse('invalid_request', `answered_by names ${agent.handle}, a one-way agent, which takes no messages, so a message to your account's handle would reach no one: name an agent that takes messages, or clear its one_way with profile.set first`); return agent.id; }; /** `account.set` (XTX-21): the owner's credential, a token of the account * or its human participant's secret, never an agent's and never an * application's, in any form. A first handle names `handle`, `unlisted`, * and `public`. A change of handle and `release_held` take the secret the * site's sign-in leg marked alone; the two choices take any other owner's * credential. */ const accountSet = (anyCaller: Caller, body: Record): unknown => { const caller = credentialed(anyCaller, 'account.set'); noGuest(caller, 'call account.set, since a guest holds no account'); if (caller.kind === 'secret' && caller.participant.kind !== 'human') { refuse('credential_refused', "account.set is the owner's act, which a session's or an agent's secret cannot make: call it with your account's token, or with the person's own secret from tzmesh.ai/me"); } if (applicationCredential(caller)) refuseApplication('account.set'); only(body, ['handle', 'unlisted', 'public', 'answered_by', 'release_held'], 'account.set'); const handle = optionalString(body, 'handle'); const unlisted = optionalBoolean(body, 'unlisted'); const joining = optionalBoolean(body, 'public'); const naming = 'answered_by' in body; const release = optionalBoolean(body, 'release_held') ?? false; const account = accountByKey(callerAccount(caller)) as Account; if (account.handle === company) refuse('invalid_request', "the company's account keeps its handle and its choices fixed, so account.set changes nothing for it: make no account.set call for this account"); // A hold that has ended is deleted inside this call, at its clock. if (account.held !== undefined && account.held.until <= now()) account.held = undefined; if (!account.adopted) { if (handle === undefined || unlisted === undefined || joining === undefined) { refuse('invalid_request', "your account has no handle yet, so account.set needs handle, unlisted, and public together, each the person's own answer: ask them for all three, and never choose one yourself"); } checkHandle(account, handle); const answering = naming ? answeredByOf(account, body) : account.answeredBy; renameAccount(account, account.handle, handle); account.unlisted = unlisted; account.public = joining; account.answeredBy = answering; return { account: accountRecord(account) }; } const changing = handle !== undefined && handle !== account.handle; if (!changing && !release && unlisted === undefined && joining === undefined && handle === undefined && !naming) refuse('invalid_request', 'account.set carries nothing to change: give at least one of handle, unlisted, public, answered_by, and release_held'); const answering = naming ? answeredByOf(account, body) : account.answeredBy; if (changing || release) { const marked = caller.kind === 'secret' && humanSecrets.get(caller.value)?.marked === true; if (!marked) { refuse( 'credential_refused', caller.kind === 'secret' ? 'changing or releasing your handle needs the secret a sign-in at tzmesh.ai/me gives, and this secret came from elsewhere: sign in again there and make the change' : "changing or releasing your handle is the person's own act at tzmesh.ai/me, never a token's: the person signs in there and makes the change", ); } } if (changing && handle !== undefined) { checkHandle(account, handle); countDay(`handle-changes:${account.id}`, constants.handleChangesPerDay, "changes of this account's handle"); const old = account.handle; renameAccount(account, old, handle); // One held handle per account: this change's replaces any earlier one, // and a take-back of the held handle holds the one it leaves. account.held = { handle: old, until: now() + constants.handleHoldMs }; } if (release) account.held = undefined; if (unlisted !== undefined) account.unlisted = unlisted; if (joining !== undefined) account.public = joining; account.answeredBy = answering; return { account: accountRecord(account) }; }; /** `participants`, under the account's token or a secret: the account's * participants, every kind, ended ones excepted, and, under the token or * the account's human participant's secret, the account as `account.set` * answers it, its handle its id until it adopts one, and the account's own * private channels, each its channel record with its member count, whether * or not the person is a member (XTX-16; XTX-21). */ const participantsOf = (anyCaller: Caller, body: Record): unknown => { const caller = credentialed(anyCaller, 'participants'); noGuest(caller, "list an account's participants, since a guest holds no account"); only(body, ['conversations_of'], 'participants'); const accountHandle = callerAccount(caller); const viewer = caller.kind === 'secret' ? caller.participant : undefined; const own = [...participants.values()].filter((p) => p.owner === accountHandle && !p.guest && presenceOf(p) !== 'ended'); // The owner's credential, its token or its person's secret, reads what // the account's own view holds and a session's or an agent's secret does // not (XTX-04; XTX-16): the account's record, its private channels, and // each agent's listener rows. const ownerView = viewer === undefined || viewer.kind === 'human'; // `conversations_of` names one of the account's live agents or sessions // by its id, under the owner's credential alone, for the owner's list of // its direct conversations (XTX-16), read before anything is answered. const of = optionalString(body, 'conversations_of'); let conversationsOf: Participant | undefined; if (of !== undefined) { if (!ownerView) refuse('credential_refused', "conversations_of is the owner's reading, which a session's or an agent's secret cannot make: call participants with your account's token, or with the person's own secret from tzmesh.ai/me"); const named = participants.get(of); if (named === undefined || !ownAgentOrSession(named, accountHandle)) refuse('participant_not_found', "conversations_of names none of your account's live agents or sessions: name one by the id participants lists"); conversationsOf = named; } // The owner's view alone counts an agent's live listeners and the // session listeners among them (#5920's point 10), as the service does. const answer: { participants: TzmeshParticipant[]; account?: TzmeshAccount; channels?: TzmeshChannel[]; conversations?: TzmeshConversation[] } = { participants: own.map((p) => { const record = participantRecord(p, viewer ?? p); if (p.kind === 'standing') { const live = p.listeners.filter((l) => listenerLive(p, l)); record.listeners = live.length; record.session_listeners = live.filter((l) => l.kind === 'session').length; // Each listener's row, to the owner alone (XTX-09; the route A // plan's decision 20): its id, kind, label, creation, and last // call, never a secret; the register's listener has no last call // until its first (XTX-12). if (ownerView) { record.served_by = p.listeners.map((l) => { const row: TzmeshListenerRow = { id: l.id, kind: l.kind }; if (l.createdAt !== undefined) row.created_at = l.createdAt; if (l.label !== undefined) row.label = l.label; if (l.unheard !== true && l.fresh !== true) row.last_call = new Date(l.lastCall).toISOString(); if (l.ended) row.ended = true; return row; }); } } return record; }), }; if (ownerView) { answer.account = accountRecord(accountByKey(accountHandle) as Account); answer.channels = [...channels.values()].filter((c) => c.kind === 'private' && c.owner === accountHandle).map((c) => channelRecord(c)); } if (conversationsOf !== undefined) answer.conversations = conversationList(conversationsOf, accountHandle); return answer; }; /** Whether a participant is a live agent or session of the account: the * participants the owner's list and reading reach (XTX-07; XTX-16). */ const ownAgentOrSession = (p: Participant, accountHandle: string): boolean => p.owner === accountHandle && !p.guest && (p.kind === 'standing' || p.kind === 'session') && !p.ended && presenceOf(p) !== 'ended'; /** Whether the account's own person, its human participant, is a member * of the channel. */ const personMember = (c: Channel, accountHandle: string): boolean => [...c.members.keys()].some((id) => { const q = participants.get(id); return q !== undefined && q.kind === 'human' && !q.guest && q.owner === accountHandle; }); /** The counter's value a message's id drew, so two messages at one * instant order by which came last, as the service orders them. */ const idNumber = (id: string | undefined): number => (id === undefined ? 0 : parseInt(id.slice(id.indexOf('_') + 1), 36) || 0); /** The owner's list of a participant's direct conversations (XTX-16): * newest message first, at most `CONVERSATIONS_MAX`, the conversation * with the account's own person left out. */ const conversationList = (p: Participant, accountHandle: string): TzmeshConversation[] => { const list = [...channels.values()].filter((c) => c.kind === 'direct' && c.members.has(p.id) && !personMember(c, accountHandle)); const order = new Map([...channels.values()].map((c, i) => [c, i])); list.sort((a, b) => b.lastMessageAt - a.lastMessageAt || idNumber(b.log.at(-1)?.id) - idNumber(a.log.at(-1)?.id) || (order.get(b) ?? 0) - (order.get(a) ?? 0)); return list.slice(0, CONVERSATIONS_MAX).map((c) => { const other = [...c.members.keys()].find((id) => id !== p.id); const row: TzmeshConversation = { channel: c.id, newest: c.log.length, last_at: new Date(c.lastMessageAt).toISOString() }; if (other !== undefined) row.with = participants.get(other)?.handle ?? 'erased'; return row; }); }; // --- the stream (XTX-08) ------------------------------------------------- // --- images (XTX-19) ----------------------------------------------------- /** The upload: the format and size read from the header, an animation * refused, the size scaled as the service scales it, counted against the * image rate before any reading and against the account's store, and the bytes kept as uploaded; * the double re-encodes nothing (the contract's Verification section). */ const imageUpload = (caller: Caller, data: Uint8Array): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "image needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first"); noGuest(caller, 'upload an image'); // Counted before the image is read, as the service counts it before its // decode, so an upload past the rate is refused unread. count(`uploads:${caller.participant.id}`, constants.imagesPerMinutePerParticipant, 'image uploads from this participant'); const header = readImageHeader(data); if (header === undefined || header.width <= 0 || header.height <= 0) refuse('invalid_request', 'an image is PNG, JPEG, or WebP, never SVG, GIF, or another format'); if (header.animated) refuse('invalid_request', 'an animated image is refused; upload a still PNG, JPEG, or WebP'); const p = caller.participant; const size = scaledSize(header.width, header.height, constants.imageLongSideMax); const used = [...storedImages.values()].filter((img) => img.account === p.owner).reduce((sum, img) => sum + img.bytes, 0); if (used + data.length > constants.imageStoreBytesPerAccount) refuse('cap_reached', `your account's image store holds ${used} of its ${constants.imageStoreBytesPerAccount} bytes, so this upload does not fit: send an image you already uploaded by its id`); const id = mintSecret('img_').slice(0, 4 + 22); const stored: StoredImage = { id, account: p.owner, bytes: data.length, width: size.width, height: size.height, data: data.slice() }; storedImages.set(id, stored); return { image: { id, bytes: stored.bytes, width: stored.width, height: stored.height } }; }; // --- the wake URL (XTX-09) ------------------------------------------------ /** `wake_url` (XTX-09): the calling listener's wake URL, a session's or * an agent's, a program's among them, with `hold`, the hold line with the * URL in place, as the service answers them. The URL is the wait's * address, naming the participant's id and the listener's and holding no * secret, so each call answers the same one. The double serves no wait * and holds nothing on it, the hold line being the tzmesh member's, so * the URL stands at a reserved origin that never resolves and the line's * figures are production's. A person's and a guest's secret hold no * listener and are refused. */ const wakeUrl = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "wake_url needs a participant's secret, and this call carried a token: use the secret register answered, or over MCP call register in this session first"); if (caller.listener === undefined) refuse('invalid_request', "a wake URL belongs to a session's or an agent's listener, and a person's or a guest's secret has none: read your messages with read instead"); only(body, [], 'wake_url'); const url = `${DOUBLE_WAKE_ORIGIN}${DOUBLE_WAIT_PATH}${caller.participant.id}/${caller.listener.id}`; // `stream`, the listener's socket beside its wait (XTX-09). return { wake_url: url, hold: `curl -s -m ${DOUBLE_HOLD_SECONDS + 120} "${url}?seconds=${DOUBLE_HOLD_SECONDS}"`, stream: streamAddress(caller.participant, caller.listener) }; }; // --- webhooks (XTX-18) --------------------------------------------------- const webhookSet = (caller: Caller, body: Record): unknown => { if (caller.kind !== 'secret') refuse('credential_refused', "webhook_set needs an agent's program secret, and this call carried a token: use the program secret register or attach answered for the agent"); noGuest(caller, 'register a webhook'); only(body, ['url'], 'webhook_set'); const p = caller.participant; if (p.kind !== 'standing') refuse('invalid_request', 'a webhook belongs to an agent, and this participant is not one: register an agent with kind standing and set its webhook there'); const l = caller.listener; if (l === undefined || l.kind !== 'program') refuse('invalid_request', "a webhook serves an agent's program, and this call carried a session's secret: call webhook_set with the program secret register or attach answered"); if (!('url' in body)) refuse('invalid_request', 'url is missing: give the https address to deliver to, or null to remove the webhook'); if (body.url === null) { l.webhook = undefined; return { participant: participantRecord(p, p) }; } if (typeof body.url !== 'string' || !webhookAddressAdmitted(body.url, constants.webhookUrlMax)) { refuse('invalid_request', `url must be an https address of at most ${constants.webhookUrlMax} characters, with a host and no user name, password, or fragment: give one of that form`); } const secret = mintSecret(TZMESH_WEBHOOK_SECRET_PREFIX); l.webhook = { url: body.url, secret, failures: 0, nextAttemptAt: 0 }; return { participant: participantRecord(p, p), signing_secret: secret }; }; /** A listener's next entry to deliver (XTX-18): the first message or * system message past the participant's cursor in a channel the listener * reads, channel by channel, its own messages passed over, as the event * the stream would carry. */ const nextDelivery = (p: Participant, l: Listener): { channel: Channel; seq: number; event: TzmeshEvent } | undefined => { for (const c of channels.values()) { if (!c.members.has(p.id) || !visibleTo(c, p, l)) continue; const acknowledged = c.members.get(p.id) ?? 0; for (const m of c.log.slice(acknowledged)) { if (m.kind === 'message' && m.sender?.id === p.id) continue; const event: TzmeshEvent = m.kind === 'system' ? systemEvent(c, m) : { event: 'message', id: eventId(c.id, m.seq), data: { message: m, channel: channelRecord(c) } }; return { channel: c, seq: m.seq, event }; } } return undefined; }; const webhookBackoff = (failures: number): number => Math.min(constants.webhookBackoffBaseMs * 2 ** Math.max(0, failures - 1), constants.webhookBackoffMaxMs); const deliverWebhooks = async (): Promise => { let delivered = 0; for (const p of [...participants.values()]) { for (const l of [...p.listeners]) { for (;;) { const hook = l.webhook; if (hook === undefined || p.ended || l.ended || hook.nextAttemptAt > now()) break; const next = nextDelivery(p, l); if (next === undefined) break; const payload = JSON.stringify({ event: next.event.event, id: next.event.id, data: next.event.data }); const headers: Record = { 'content-type': 'application/json', [TZMESH_WEBHOOK_SIGNATURE_HEADER]: await signTzmeshWebhook(hook.secret, payload, now()), [TZMESH_WEBHOOK_EVENT_HEADER]: next.event.event, [TZMESH_WEBHOOK_DELIVERY_HEADER]: next.event.id, }; let status = 0; try { status = options.webhookFetch === undefined ? 0 : (await options.webhookFetch(hook.url, { method: 'POST', headers, body: payload })).status; } catch { status = 0; } // A webhook set again or cleared during the attempt keeps nothing of it. if (l.webhook !== hook) break; if (status >= 200 && status < 300) { // A delivery acknowledges the entry, as `ack` would (XTX-07). const acknowledged = next.channel.members.get(p.id); if (acknowledged !== undefined && next.seq > acknowledged) next.channel.members.set(p.id, next.seq); hook.failures = 0; hook.nextAttemptAt = 0; delivered += 1; continue; } // Undelivered, the entry stays past the cursor, `read` answers it, // and the next attempt waits its backoff. hook.failures += 1; hook.nextAttemptAt = now() + webhookBackoff(hook.failures); break; } } } return delivered; }; const openStream = (caller: Caller, body: Record, init: TzmeshTransportInit): TzmeshTransportResponse => { if (caller.kind !== 'secret') refuse('credential_refused', "the stream needs a participant's secret, and this request carried a token: open it with the secret register answered"); only(body, ['after'], 'stream'); const p = caller.participant; const after: Record = {}; if (body.after !== undefined) { if (!isPlainObject(body.after)) refuse('invalid_request', 'after must be an object mapping each channel id to the seq of the newest message you read there'); for (const [channel, seq] of Object.entries(body.after)) { if (typeof seq !== 'number' || !Number.isInteger(seq) || seq < 0) refuse('invalid_request', "after's values must be whole numbers, each the seq of the newest message you read in that channel"); after[channel] = seq; } } const queue = new ChunkQueue(); const l = caller.listener; const stream: OpenStream = { participant: p.id, ...(l !== undefined ? { listener: l.id } : {}), push: (event) => queue.push(eventFrame(event)), end: () => { if (!streams.has(stream)) return; streams.delete(stream); p.holds -= 1; if (l !== undefined) l.holds -= 1; queue.end(); }, }; p.holds += 1; if (l !== undefined) l.holds += 1; streams.add(stream); // A client that closes its stream aborts the request's signal; the double // ends the stream as the service would at the connection's close. init.signal?.addEventListener('abort', () => stream.end(), { once: true }); // The backlog: every message past the cursor, or past `after`, each as // the event its kind takes, a system message as joined or left (XTX-08). for (const c of channels.values()) { // A listener of an agent hears the channels it reads alone (visibleTo). if (!c.members.has(p.id) || !visibleTo(c, p, l)) continue; const from = after[c.id] ?? c.members.get(p.id) ?? 0; for (const m of c.log.slice(from)) { if (m.kind === 'system') { // A message_removed notice reaches its subject, the sender it tells // why (XTX-23); every other notice passes over its own subject. if (m.subject?.id === p.id && m.event !== 'message_removed') continue; queue.push(eventFrame(systemEvent(c, m))); continue; } if (m.sender?.id === p.id) continue; queue.push(eventFrame({ event: 'message', id: eventId(c.id, m.seq), data: { message: m, channel: channelRecord(c) } })); } } return { status: 200, ok: true, headers: { get: (name: string) => (name.toLowerCase() === 'content-type' ? 'text/event-stream' : null) }, text: async () => '', body: queue, }; }; // --- the transport ------------------------------------------------------- const record = (path: string, credential: TzmeshDoubleCall['credential'], status: number, refusal?: TzmeshRefusalName): void => { calls.push(refusal === undefined ? { path, credential, status } : { path, credential, status, refusal }); }; const transport: TzmeshTransport = async (path, init = {}) => { const method = (init.method ?? 'GET').toUpperCase(); const { caller, recorded, replacedBy } = readCredential(init); const call = path.startsWith(`${TZMESH_PATH_PREFIX}/`) ? path.slice(TZMESH_PATH_PREFIX.length + 1) : null; const known = path === TZMESH_STREAM_PATH || (call !== null && (TZMESH_CALLS as readonly string[]).includes(call)); if (!known) { record(path, recorded, 404); return jsonResponse(404, { error: 'not_found', detail: 'there is no call of that name: use one of the calls the wire lists, such as register, send, or read' }); } // The guest door shut: the guest call answers 404 with no body before // anything else is read, as the service's wire answers a dormant call // (XTX-16). if (call === 'guest' && options.guestDoor === false) { record(path, recorded, 404); return { status: 404, ok: false, headers: { get: () => null }, text: async () => '', body: null }; } if (method !== 'POST') { record(path, recorded, 405); return jsonResponse(405, { error: 'method_not_allowed', detail: 'every call is a POST: send this one as a POST with a JSON body' }); } if (init.signal?.aborted) throw init.signal.reason ?? new Error('aborted'); try { if (call === 'image') { // The upload's body is the image's bytes, bounded before any other // reading (XTX-11; XTX-19). const data = init.body === undefined ? new Uint8Array() : typeof init.body === 'string' ? encoder.encode(init.body) : init.body; if (data.length > constants.imageUploadBytesMax) refuse('body_too_large', `this image upload is over ${constants.imageUploadBytesMax} bytes, the most an upload takes: scale the image down or compress it first`); const uploader = admit('image', caller, recorded, replacedBy); requireHandle('image', uploader); const answer = imageUpload(uploader, data); record(path, recorded, 200); return jsonResponse(200, answer); } // A guest is counted against its address's day before its body is // read, as the service's door counts it (XTX-16). const guestAddress = call === 'guest' ? admitGuestAddress() : undefined; const body = readBody(init); if (path === TZMESH_STREAM_PATH) { const admitted = admit('stream', caller, recorded, replacedBy); const response = openStream(admitted, body, init); record(path, recorded, 200); return response; } const name = call as TzmeshCall; const admitted = admit(name, caller, recorded, replacedBy); requireHandle(name, admitted); let answer: unknown; switch (name) { case 'register': answer = register(admitted, body); break; case 'end': answer = end(admitted, body); break; case 'account.set': answer = accountSet(admitted, body); break; case 'profile.set': answer = profileSet(admitted, body); break; case 'create': answer = await create(admitted, body); break; case 'join': answer = join(admitted, body); break; case 'leave': answer = leave(admitted, body); break; case 'invite': answer = invite(admitted, body); break; case 'members': answer = members(admitted, body); break; case 'channel.set': answer = await channelSet(admitted, body); break; case 'remove_message': answer = removeMessage(admitted, body); break; case 'remove_member': answer = removeMemberCall(admitted, body); break; case 'report_message': answer = reportMessage(admitted, body); break; case 'send': answer = send(admitted, body); break; case 'read': answer = await read(admitted, body, init); break; case 'ack': answer = ack(admitted, body); break; case 'directory': answer = directory(admitted, body); break; case 'export': answer = exportAccount(admitted, body); break; case 'guest': answer = await guest(body, init); if (guestAddress !== undefined) countGuestAddress(guestAddress); break; case 'human': answer = human(admitted, body); break; case 'participants': answer = participantsOf(admitted, body); break; case 'webhook_set': answer = webhookSet(admitted, body); break; case 'wake_url': answer = wakeUrl(admitted, body); break; default: answer = refuse('invalid_request', 'there is no call of that name: use one of the calls the wire lists, such as register, send, or read'); } record(path, recorded, 200); return jsonResponse(200, answer); } catch (error) { if (error instanceof Refusal) { const status = TZMESH_DOUBLE_REFUSALS[error.refusal]; record(path, recorded, status, error.refusal); return jsonResponse(status, { error: error.refusal, detail: error.detail }); } throw error; } }; return { transport, account(name, accountOptions = {}) { let account = accounts.get(name) ?? accountByKey(name); if (account === undefined && accountOptions.adopted === false) { account = { id: name, handle: name, adopted: false, public: false, unlisted: false, reserved: new Set() }; accounts.set(account.id, account); } account ??= accountOf(name); if (accountOptions.public !== undefined) account.public = accountOptions.public; if (accountOptions.unlisted !== undefined) account.unlisted = accountOptions.unlisted; return { handle: account.adopted ? account.handle : null, public: account.public, unlisted: account.unlisted }; }, mintToken, signIn(name) { const { participant, secret } = humanFor(accountNamed(name), true, false); return { participant, secret }; }, dropConnections() { let dropped = 0; for (const stream of [...streams]) { stream.end(); dropped += 1; } for (const waiter of [...waiters]) { waiter.wake(); dropped += 1; } // A restart forgets what reads answered, so the first held read // after it answers at once what stands (XTX-07). answered = new Map(); return dropped; }, deliverWebhooks, calls, constants, plane, reset, }; } /** The plane's sign-in stood in (XTX-17): a public client's registration, * the person's consent, and the code's single exchange with PKCE, each * answered as the plane answers it (cloud:MCP-02), the token minted by the * double's own seam. */ function createPlaneStandIn(mint: (request: { account: string; level: TzmeshLevel; label?: string; application_sign_in?: true }) => string): TzmeshPlaneDouble & { reset(): void } { let clients: Array<{ clientId: string; name: string; redirectUris: string[] }> = []; let codes = new Map(); let counter = 0; const answer = (status: number, body: unknown) => ({ ok: status >= 200 && status < 300, status, json: async () => body }); const callback = (redirectUri: string, members: Record): string => { const url = new URL(redirectUri); for (const [name, value] of Object.entries(members)) url.searchParams.set(name, value); return url.toString(); }; const fetchFn: TzmeshSignInFetch = async (url, init = {}) => { const address = new URL(url); if (address.pathname === '/.well-known/oauth-authorization-server') { return answer(200, { issuer: address.origin, authorization_endpoint: `${address.origin}/oauth/authorize`, token_endpoint: `${address.origin}/oauth/token`, registration_endpoint: `${address.origin}/oauth/register`, code_challenge_methods_supported: ['S256'], scopes_supported: ['tzcloud', 'tzmesh'], }); } if (address.pathname === '/oauth/register' && init.method === 'POST') { const body = JSON.parse(init.body ?? '{}') as { client_name?: unknown; redirect_uris?: unknown; token_endpoint_auth_method?: unknown }; const uris = Array.isArray(body.redirect_uris) ? body.redirect_uris.filter((u): u is string => typeof u === 'string') : []; if (uris.length === 0 || body.token_endpoint_auth_method !== 'none') return answer(400, { error: 'invalid_client_metadata' }); const client = { clientId: `client_${(++counter).toString(36)}`, name: typeof body.client_name === 'string' ? body.client_name : '', redirectUris: uris }; clients.push(client); return answer(201, { client_id: client.clientId, client_name: client.name, redirect_uris: uris, token_endpoint_auth_method: 'none' }); } if (address.pathname === '/oauth/token' && init.method === 'POST') { const form = new URLSearchParams(init.body ?? ''); const held = codes.get(form.get('code') ?? ''); codes.delete(form.get('code') ?? ''); if (held === undefined || form.get('grant_type') !== 'authorization_code' || form.get('client_id') !== held.clientId || form.get('redirect_uri') !== held.redirectUri) { return answer(400, { error: 'invalid_grant' }); } if ((await tzmeshChallengeOf(form.get('code_verifier') ?? '')) !== held.challenge) return answer(400, { error: 'invalid_grant' }); const token = mint({ account: held.account, level: held.level, label: held.label, ...(held.application ? { application_sign_in: true as const } : {}) }); return answer(200, { access_token: token, token_type: 'Bearer', scope: 'tzmesh', level: held.level, label: held.label, expires_at: null, token_id: `tid_${token}` }); } return answer(404, { error: 'not_found' }); }; return { fetch: fetchFn, consent(authorizationUrl, person) { const address = new URL(authorizationUrl); const q = address.searchParams; const client = clients.find((c) => c.clientId === q.get('client_id')); const redirectUri = q.get('redirect_uri') ?? ''; if (client === undefined || !client.redirectUris.includes(redirectUri)) throw new Error('unknown client or redirect_uri'); if (q.get('response_type') !== 'code' || q.get('code_challenge_method') !== 'S256' || !q.get('code_challenge')) throw new Error('OAuth 2.1: response_type=code with S256 PKCE'); const state = q.get('state') ?? ''; if (person.cancel === true) return callback(redirectUri, { error: 'access_denied', state }); // An application's request (the scope `tzmesh.application`) is given // `chat` under ` at `, its token carrying // `application_sign_in`, as the plane gives it; a tool's is // given `admin` under the client's registered name, the person's level // and label ignored, since the person chooses neither (XTX-17; // cloud:MCP-02). A name repeats: each exchange mints its own token. const application = (q.get('scope') ?? '').split(' ').includes(TZMESH_APPLICATION_SCOPE); const level: TzmeshLevel = application ? 'chat' : 'admin'; const label = application ? `${client.name || 'An application'} at ${new URL(redirectUri).host}` : client.name || 'An application'; const code = `code_${(++counter).toString(36)}`; codes.set(code, { clientId: client.clientId, redirectUri, challenge: q.get('code_challenge') ?? '', account: person.account, level, label, application }); return callback(redirectUri, { code, state }); }, get clients() { return clients; }, reset() { clients = []; codes = new Map(); counter = 0; }, }; }