@dotwire/host 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +256 -2
- package/dist/index.cjs +1000 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +464 -0
- package/dist/index.d.ts +464 -0
- package/dist/index.js +964 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types.ts","../src/errors.ts","../src/signer.ts","../src/room.ts","../src/client.ts","../src/sse.ts","../src/util.ts","../src/moderation.ts"],"sourcesContent":["export * from './types.js';\nexport * from './errors.js';\nexport * from './signer.js';\nexport * from './room.js';\nexport * from './client.js';\nexport * from './moderation.js';\n","export type DotwireRole = 'member' | 'auditor' | 'admin';\n\nexport interface DotwireHostConfig {\n baseUrl: string;\n issuer: string;\n audience?: string;\n keyId: string;\n privateKeyPem: string;\n adminUserId?: string;\n auditorUserId?: string;\n /** jose-style duration string (e.g. \"15m\"), passed straight through to `SignJWT#setExpirationTime`. */\n defaultTokenTtl?: string;\n requestTimeoutMs?: number;\n /** HMAC-SHA256 secret used to verify `X-Dotwire-Signature` on incoming webhook calls. */\n webhookSecret?: string;\n roleCacheTtlMs?: number;\n /** Every request goes through this so tests can inject a fake. Defaults to global `fetch`. */\n fetch?: typeof fetch;\n /**\n * Runs in-process before {@link DotwireHostClient.sendAsUser} and {@link DotwireHostClient.handleClientSend},\n * and inside {@link DotwireHostClient.handlePresendWebhook} - one place for moderation logic that\n * covers both host-initiated sends and messages sent directly by `@dotwire/client`.\n */\n presend?: (ctx: PresendContext) => Promise<PresendResult> | PresendResult;\n /** Same idea as {@link presend}, run after a message is accepted; see {@link PostSendContext}. */\n postSend?: (ctx: PostSendContext) => Promise<void> | void;\n}\n\nexport interface MintTokenOptions {\n role?: DotwireRole;\n displayName?: string;\n /** jose-style duration string, e.g. \"15m\". Defaults to `config.defaultTokenTtl`. */\n ttl?: string;\n}\n\nexport interface PresendContext {\n roomId: string;\n content: string;\n senderUserId: string;\n}\n\nexport interface PresendResult {\n allow: boolean;\n /** Rewritten content; only meaningful when `allow` is true. */\n content?: string;\n rejectionReason?: string;\n}\n\nexport const PresendResult = {\n accept(content?: string): PresendResult {\n return { allow: true, content };\n },\n reject(reason: string): PresendResult {\n return { allow: false, rejectionReason: reason };\n }\n};\n\n/** What a postSend hook sees: the accepted message plus its ack, so it can be found again by (roomId, seq). */\nexport interface PostSendContext {\n roomId: string;\n seq: number;\n time: string;\n content: string;\n senderUserId: string;\n}\n\nexport interface SendMessageResult {\n seq: number;\n time: string;\n}\n\nexport interface HistoryMessage {\n seq: number;\n time: string;\n senderId: string;\n content: string;\n}\n\nexport interface RoomHistoryResponse {\n roomId: string;\n messages: HistoryMessage[];\n hasMore: boolean;\n}\n\nexport interface UserRoleResponse {\n userId: string;\n role: DotwireRole;\n}\n\nexport interface RoomMembersResponse {\n roomId: string;\n members: string[];\n}\n\nexport interface UserRoomsResponse {\n userId: string;\n roomIds: string[];\n}\n\nexport interface RedactMessageResponse {\n roomId: string;\n seq: number;\n removedFromHistory: boolean;\n removedFromStream: boolean;\n}\n\nexport interface RetentionResponse {\n messagesDays: number;\n}\n\nexport interface AuditEntry {\n id: number;\n eventType: string;\n roomId: string | null;\n messageSeq: number | null;\n actorId: string | null;\n subjectId: string | null;\n value: number | null;\n time: string;\n hash: string;\n}\n\nexport interface AuditPageResponse {\n events: AuditEntry[];\n hasMore: boolean;\n}\n\nexport interface AuditVerificationResponse {\n ok: boolean;\n fromId: number;\n toId: number;\n checked: number;\n firstInvalidId: number | null;\n anchor: string | null;\n}\n\nexport interface AuditCheckpointEntry {\n day: string;\n lastId: number;\n hash: string;\n createdAt: string;\n}\n\nexport interface AuditCheckpointsResponse {\n checkpoints: AuditCheckpointEntry[];\n}\n\nexport interface UserExportMessage {\n roomId: string;\n seq: number;\n time: string;\n content: string | null;\n undecryptable?: boolean;\n}\n\nexport interface UserExportAuditEvent {\n id: number;\n eventType: string;\n roomId: string | null;\n messageSeq: number | null;\n actorId: string | null;\n subjectId: string | null;\n value: number | null;\n time: string;\n}\n\nexport interface UserExport {\n userId: string;\n exportedAt: string;\n role: DotwireRole | null;\n roomIds: string[];\n messages: UserExportMessage[];\n auditEvents: UserExportAuditEvent[];\n}\n\nexport interface GetMessagesOptions {\n asUserId: string;\n afterSeq?: number;\n beforeSeq?: number;\n limit?: number;\n}\n\nexport interface AddRoomMembersOptions {\n /** Grants `member` to any listed user with no existing role, without touching an existing auditor/admin. Default true. */\n ensureMemberRole?: boolean;\n}\n\nexport interface SendSystemMessageOptions {\n /** Defaults to \"system\". */\n senderId?: string;\n}\n\nexport interface StreamRoomEventsOptions {\n asUserId: string;\n afterSeq?: number;\n /** Reconnect on drop with `Last-Event-ID`, exponential backoff 1s..30s, until cancelled or revoked. Default true. */\n autoResume?: boolean;\n signal?: AbortSignal;\n}\n\nexport interface TokenResponseOptions {\n userId: string;\n role?: DotwireRole;\n displayName?: string;\n ttl?: string;\n}\n\n/** Discriminated union of everything `GET /rooms/{roomId}/events` can emit (spec §3.9). */\nexport type RoomEvent =\n | { type: 'message'; roomId: string; seq: number; senderId: string; time: string; content: string; replayed?: boolean }\n | { type: 'retracted'; roomId: string; seq: number }\n | { type: 'typing'; roomId: string; userId: string; isTyping: boolean }\n | { type: 'presence'; roomId: string; joined: string[]; left: string[] }\n | { type: 'revoked'; roomId: string };\n\n/** Result of {@link DotwireHostClient.handleClientSendBody} / the Request-based `handleClientSend`. */\nexport interface ClientSendResult {\n status: number;\n body: { seq: number; time: string } | { error: string; reason?: string };\n}\n\n/** Result of the two webhook handlers' body-based variants. */\nexport interface WebhookResult {\n status: number;\n body: { allow: boolean; content?: string; reason?: string } | Record<string, never>;\n}\n","import type { SendMessageResult } from './types.js';\n\n/**\n * Mirrors `Dotwire.Host.DotwireApiException.Kind` (.NET) / the wire error contract in spec §3.14.\n * Mapped from HTTP status first, then the `error` code in the body.\n */\nexport type DotwireApiErrorKind =\n | 'BadRequest'\n | 'Unauthorized'\n | 'Forbidden'\n | 'NotFound'\n | 'PresendRejected'\n | 'RateLimited'\n | 'Unavailable'\n | 'Unknown';\n\nexport interface DotwireApiErrorInit {\n status: number;\n kind: DotwireApiErrorKind;\n code?: string;\n reason?: string;\n retryAfterMs?: number;\n method: string;\n path: string;\n}\n\n/** Thrown when the dotwire server answers a request with a non-success status. */\nexport class DotwireApiError extends Error {\n readonly status: number;\n readonly kind: DotwireApiErrorKind;\n readonly code?: string;\n readonly reason?: string;\n readonly retryAfterMs?: number;\n readonly method: string;\n readonly path: string;\n\n constructor(init: DotwireApiErrorInit) {\n super(`dotwire ${init.method} ${init.path} failed: ${init.status}${init.code ? ` ${init.code}` : ''}`);\n this.name = 'DotwireApiError';\n this.status = init.status;\n this.kind = init.kind;\n this.code = init.code;\n this.reason = init.reason;\n this.retryAfterMs = init.retryAfterMs;\n this.method = init.method;\n this.path = init.path;\n }\n}\n\n/**\n * Maps an HTTP status (and, where ambiguous, the wire error code from spec §3.14) to a\n * {@link DotwireApiErrorKind}. Status is authoritative; code only disambiguates 422/503/429/400.\n */\nexport function mapErrorKind(status: number, code?: string): DotwireApiErrorKind {\n switch (status) {\n case 400:\n return 'BadRequest';\n case 401:\n return 'Unauthorized';\n case 403:\n return 'Forbidden';\n case 404:\n return 'NotFound';\n case 422:\n return code === 'presend_rejected' ? 'PresendRejected' : 'BadRequest';\n case 429:\n return 'RateLimited';\n case 503:\n return 'Unavailable';\n default:\n return 'Unknown';\n }\n}\n\n/**\n * Thrown when the host's own {@link DotwireHostConfig.presend} hook rejects a message, evaluated\n * in-process before the message ever reaches the wire (no network round-trip). Distinct from a\n * {@link DotwireApiError} with kind `PresendRejected`, which is the *server's* configured presend\n * webhook rejecting a message on `POST /rooms/{roomId}/messages` (a different, server-side hook).\n */\nexport class PresendRejectedError extends Error {\n readonly reason: string;\n\n constructor(reason: string) {\n super(reason);\n this.name = 'PresendRejectedError';\n this.reason = reason;\n }\n}\n\n/**\n * The message WAS sent (see {@link sent}); only the local `postSend` hook failed afterwards.\n * Kept distinct from a send failure so callers never retry a send that already succeeded.\n */\nexport class PostSendError extends Error {\n readonly sent: SendMessageResult;\n\n constructor(sent: SendMessageResult, cause: unknown) {\n super('The message was sent, but the postSend hook failed.', { cause });\n this.name = 'PostSendError';\n this.sent = sent;\n }\n}\n","import { importPKCS8, SignJWT, type CryptoKey } from 'jose';\nimport type { DotwireHostConfig, MintTokenOptions } from './types.js';\n\nexport class DotwireTokenSigner {\n private readonly config: DotwireHostConfig;\n private readonly keyPromise: Promise<CryptoKey>;\n private readonly defaultTtl: string;\n\n constructor(config: DotwireHostConfig) {\n this.config = config;\n this.keyPromise = importPKCS8(config.privateKeyPem, 'RS256');\n this.defaultTtl = config.defaultTokenTtl ?? '15m';\n }\n\n async mintToken(userId: string, options: MintTokenOptions = {}): Promise<string> {\n const key = await this.keyPromise;\n const jwt = new SignJWT({\n 'dw:role': options.role ?? 'member',\n ...(options.displayName ? { 'dw:name': options.displayName } : {})\n })\n .setProtectedHeader({ alg: 'RS256', kid: this.config.keyId })\n .setIssuer(this.config.issuer)\n .setSubject(userId)\n .setAudience(this.config.audience ?? 'dotwire')\n .setIssuedAt()\n .setExpirationTime(options.ttl ?? this.defaultTtl);\n\n return jwt.sign(key);\n }\n}\n","import type { DotwireHostClient } from './client.js';\nimport type {\n AddRoomMembersOptions,\n GetMessagesOptions,\n RedactMessageResponse,\n RoomEvent,\n RoomHistoryResponse,\n SendMessageResult,\n SendSystemMessageOptions,\n StreamRoomEventsOptions\n} from './types.js';\n\n/** Mirrors {@link DotwireHostClient}'s room-scoped methods without the repeated `roomId` argument. */\nexport class DotwireHostRoom {\n private readonly client: DotwireHostClient;\n readonly roomId: string;\n\n constructor(client: DotwireHostClient, roomId: string) {\n this.client = client;\n this.roomId = roomId;\n }\n\n addMembers(userIds: string[], options?: AddRoomMembersOptions): Promise<string[]> {\n return this.client.addRoomMembers(this.roomId, userIds, options);\n }\n\n removeMember(userId: string): Promise<void> {\n return this.client.removeRoomMember(this.roomId, userId);\n }\n\n getMembers(): Promise<string[]> {\n return this.client.getRoomMembers(this.roomId);\n }\n\n getParticipants(asUserId: string): Promise<string[]> {\n return this.client.getRoomParticipants(this.roomId, asUserId);\n }\n\n sendAsUser(userId: string, content: string): Promise<SendMessageResult> {\n return this.client.sendAsUser(this.roomId, userId, content);\n }\n\n sendSystemMessage(content: string, options?: SendSystemMessageOptions): Promise<SendMessageResult> {\n return this.client.sendSystemMessage(this.roomId, content, options);\n }\n\n getMessages(options: GetMessagesOptions): Promise<RoomHistoryResponse> {\n return this.client.getMessages(this.roomId, options);\n }\n\n events(options: StreamRoomEventsOptions): AsyncIterable<RoomEvent> {\n return this.client.streamRoomEvents(this.roomId, options);\n }\n\n redact(seq: number): Promise<RedactMessageResponse> {\n return this.client.redactMessage(this.roomId, seq);\n }\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport { DotwireTokenSigner } from './signer.js';\nimport { DotwireHostRoom } from './room.js';\nimport { DotwireApiError, mapErrorKind, PresendRejectedError, PostSendError } from './errors.js';\nimport { parseSseStream, toRoomEvent } from './sse.js';\nimport { delay, isAbortError } from './util.js';\nimport type {\n AddRoomMembersOptions,\n AuditCheckpointsResponse,\n AuditPageResponse,\n AuditVerificationResponse,\n ClientSendResult,\n DotwireHostConfig,\n DotwireRole,\n GetMessagesOptions,\n MintTokenOptions,\n PostSendContext,\n PresendContext,\n RedactMessageResponse,\n RetentionResponse,\n RoomEvent,\n RoomHistoryResponse,\n RoomMembersResponse,\n SendMessageResult,\n SendSystemMessageOptions,\n StreamRoomEventsOptions,\n TokenResponseOptions,\n UserExport,\n UserRoleResponse,\n UserRoomsResponse,\n WebhookResult\n} from './types.js';\n\nconst DEFAULT_ADMIN_USER_ID = 'host-admin';\nconst DEFAULT_AUDITOR_USER_ID = 'host-auditor';\nconst DEFAULT_AUDIENCE = 'dotwire';\nconst DEFAULT_TOKEN_TTL = '15m';\nconst DEFAULT_REQUEST_TIMEOUT_MS = 15_000;\nconst DEFAULT_ROLE_CACHE_TTL_MS = 300_000;\n\n/** Re-mints a cached admin/auditor token when less than this much life remains. */\nconst TOKEN_REFRESH_MARGIN_SECONDS = 60;\n\ninterface RawFetchOptions {\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n token?: string;\n extraHeaders?: Record<string, string>;\n signal?: AbortSignal;\n /** When false, no `AbortSignal.timeout` is applied - used for the long-lived SSE GET. */\n timeout?: boolean;\n}\n\n/** Lazily mints a token and re-mints it once fewer than a minute of life remains. */\nclass CachedToken {\n private readonly mint: () => Promise<string>;\n private token: string | undefined;\n private expiresAtSeconds = 0;\n\n constructor(mint: () => Promise<string>) {\n this.mint = mint;\n }\n\n async get(): Promise<string> {\n const now = Date.now() / 1000;\n if (this.token && this.expiresAtSeconds - now > TOKEN_REFRESH_MARGIN_SECONDS) {\n return this.token;\n }\n this.token = await this.mint();\n this.expiresAtSeconds = decodeExpiry(this.token) ?? now + 900;\n return this.token;\n }\n}\n\nfunction decodeExpiry(jwt: string): number | undefined {\n try {\n const payloadSegment = jwt.split('.')[1];\n const json = Buffer.from(payloadSegment, 'base64url').toString('utf8');\n const payload = JSON.parse(json) as { exp?: number };\n return typeof payload.exp === 'number' ? payload.exp : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Caches a resolved role per user id for `roleCacheTtlMs`. */\nclass RoleCache {\n private readonly entries = new Map<string, { role: DotwireRole; expiresAt: number }>();\n private readonly ttlMs: number;\n\n constructor(ttlMs: number) {\n this.ttlMs = ttlMs;\n }\n\n get(userId: string): DotwireRole | undefined {\n const entry = this.entries.get(userId);\n if (!entry) return undefined;\n if (Date.now() > entry.expiresAt) {\n this.entries.delete(userId);\n return undefined;\n }\n return entry.role;\n }\n\n set(userId: string, role: DotwireRole): void {\n this.entries.set(userId, { role, expiresAt: Date.now() + this.ttlMs });\n }\n}\n\n/**\n * Node/TypeScript counterpart of `Dotwire.Host.DotwireHostClient` (.NET). Mints its own tokens\n * and talks to a dotwire server entirely over its frozen HTTP surface - see spec §3 for the\n * server's wire shapes and §4/§5 for the identity and role-resolution rules this class follows.\n */\nexport class DotwireHostClient {\n private readonly config: DotwireHostConfig;\n private readonly signer: DotwireTokenSigner;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly requestTimeoutMs: number;\n private readonly adminUserId: string;\n private readonly auditorUserId: string;\n private readonly defaultTtl: string;\n private readonly roleCache: RoleCache;\n private readonly adminToken: CachedToken;\n private readonly auditorToken: CachedToken;\n\n presend?: (ctx: PresendContext) => Promise<import('./types.js').PresendResult> | import('./types.js').PresendResult;\n postSend?: (ctx: PostSendContext) => Promise<void> | void;\n\n constructor(config: DotwireHostConfig) {\n this.config = config;\n this.signer = new DotwireTokenSigner({\n ...config,\n audience: config.audience ?? DEFAULT_AUDIENCE,\n defaultTokenTtl: config.defaultTokenTtl ?? DEFAULT_TOKEN_TTL\n });\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.fetchImpl = config.fetch ?? fetch;\n this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n this.adminUserId = config.adminUserId ?? DEFAULT_ADMIN_USER_ID;\n this.auditorUserId = config.auditorUserId ?? DEFAULT_AUDITOR_USER_ID;\n this.defaultTtl = config.defaultTokenTtl ?? DEFAULT_TOKEN_TTL;\n this.roleCache = new RoleCache(config.roleCacheTtlMs ?? DEFAULT_ROLE_CACHE_TTL_MS);\n this.adminToken = new CachedToken(() => this.signer.mintToken(this.adminUserId, { role: 'admin', ttl: this.defaultTtl }));\n this.auditorToken = new CachedToken(() => this.signer.mintToken(this.auditorUserId, { role: 'auditor', ttl: this.defaultTtl }));\n this.presend = config.presend;\n this.postSend = config.postSend;\n }\n\n // ---------------------------------------------------------------------\n // Tokens\n // ---------------------------------------------------------------------\n\n /** Mints a token directly. An explicit `role` bypasses all role resolution. */\n async mintToken(userId: string, options: MintTokenOptions = {}): Promise<string> {\n return this.signer.mintToken(userId, { ttl: this.defaultTtl, ...options });\n }\n\n /**\n * Returns a `Response` with body `{ token }`. When `role` is omitted, resolves it through the\n * cached admin lookup and falls back to `member` for an unknown user (404 on the lookup) since\n * there is no request of its own to fail here.\n */\n async tokenResponse(options: TokenResponseOptions): Promise<Response> {\n const role = options.role ?? (await this.resolveRoleForToken(options.userId));\n const token = await this.mintToken(options.userId, { role, displayName: options.displayName, ttl: options.ttl });\n return jsonResponse(200, { token });\n }\n\n private async resolveRoleForToken(userId: string): Promise<DotwireRole> {\n const cached = this.roleCache.get(userId);\n if (cached !== undefined) return cached;\n const looked = await this.lookupRole(userId);\n const role = looked ?? 'member';\n this.roleCache.set(userId, role);\n return role;\n }\n\n // ---------------------------------------------------------------------\n // Roles\n // ---------------------------------------------------------------------\n\n async setUserRole(userId: string, role: DotwireRole): Promise<UserRoleResponse> {\n const res = await this.adminRequest('PUT', `/admin/users/${encodeURIComponent(userId)}/role`, { body: { role } });\n await this.throwIfError(res, 'PUT', `/admin/users/${userId}/role`);\n this.roleCache.set(userId, role);\n return (await res.json()) as UserRoleResponse;\n }\n\n async getUserRole(userId: string): Promise<UserRoleResponse | null> {\n const path = `/admin/users/${userId}/role`;\n const res = await this.adminRequest('GET', `/admin/users/${encodeURIComponent(userId)}/role`);\n if (res.status === 404) return null;\n await this.throwIfError(res, 'GET', path);\n return (await res.json()) as UserRoleResponse;\n }\n\n async deleteUserRole(userId: string): Promise<void> {\n const path = `/admin/users/${userId}/role`;\n const res = await this.adminRequest('DELETE', `/admin/users/${encodeURIComponent(userId)}/role`);\n if (res.status === 404) return;\n await this.throwIfError(res, 'DELETE', path);\n }\n\n async getUserRooms(userId: string): Promise<string[]> {\n const path = `/admin/users/${userId}/rooms`;\n const res = await this.adminRequest('GET', `/admin/users/${encodeURIComponent(userId)}/rooms`);\n await this.throwIfError(res, 'GET', path);\n const data = (await res.json()) as UserRoomsResponse;\n return data.roomIds;\n }\n\n async exportUser(userId: string): Promise<UserExport> {\n const path = `/admin/users/${userId}/export`;\n const res = await this.adminRequest('GET', `/admin/users/${encodeURIComponent(userId)}/export`);\n await this.throwIfError(res, 'GET', path);\n return (await res.json()) as UserExport;\n }\n\n /** Returns the raw response stream so the caller can pipe an arbitrarily large export without buffering it. */\n async exportUserStream(userId: string): Promise<ReadableStream<Uint8Array>> {\n const path = `/admin/users/${userId}/export`;\n const res = await this.adminRequest('GET', `/admin/users/${encodeURIComponent(userId)}/export`);\n await this.throwIfError(res, 'GET', path);\n if (!res.body) throw new Error('dotwire export response had no body');\n return res.body;\n }\n\n private async lookupRole(userId: string): Promise<DotwireRole | null> {\n const res = await this.adminRequest('GET', `/admin/users/${encodeURIComponent(userId)}/role`);\n if (res.status === 404) return null;\n await this.throwIfError(res, 'GET', `/admin/users/${userId}/role`);\n const data = (await res.json()) as UserRoleResponse;\n return data.role;\n }\n\n // ---------------------------------------------------------------------\n // Rooms\n // ---------------------------------------------------------------------\n\n room(roomId: string): DotwireHostRoom {\n return new DotwireHostRoom(this, roomId);\n }\n\n async addRoomMembers(roomId: string, userIds: string[], options: AddRoomMembersOptions = {}): Promise<string[]> {\n const ensureMemberRole = options.ensureMemberRole ?? true;\n const path = `/admin/rooms/${roomId}/members`;\n const res = await this.adminRequest('POST', path, { body: { userIds, ensureMemberRole } });\n await this.throwIfError(res, 'POST', path);\n const data = (await res.json()) as RoomMembersResponse;\n return data.members;\n }\n\n async removeRoomMember(roomId: string, userId: string): Promise<void> {\n const path = `/admin/rooms/${roomId}/members/${userId}`;\n const res = await this.adminRequest('DELETE', `/admin/rooms/${roomId}/members/${encodeURIComponent(userId)}`);\n if (res.status === 404) return;\n await this.throwIfError(res, 'DELETE', path);\n }\n\n async getRoomMembers(roomId: string): Promise<string[]> {\n const path = `/admin/rooms/${roomId}/members`;\n const res = await this.adminRequest('GET', path);\n await this.throwIfError(res, 'GET', path);\n const data = (await res.json()) as RoomMembersResponse;\n return data.members;\n }\n\n /** Reads participants under the user's own room membership, without admin access. */\n async getRoomParticipants(roomId: string, asUserId: string): Promise<string[]> {\n const path = `/rooms/${roomId}/participants`;\n const res = await this.fetchAsUser('GET', path, asUserId);\n await this.throwIfError(res, 'GET', path);\n return (await res.json()) as string[];\n }\n\n // ---------------------------------------------------------------------\n // Sending\n // ---------------------------------------------------------------------\n\n async sendAsUser(roomId: string, userId: string, content: string): Promise<SendMessageResult> {\n if (this.presend) {\n const result = await this.presend({ roomId, content, senderUserId: userId });\n if (!result.allow) {\n throw new PresendRejectedError(result.rejectionReason ?? 'Message rejected by presend hook.');\n }\n if (result.content !== undefined) {\n content = result.content;\n }\n }\n\n const path = `/rooms/${roomId}/messages`;\n const res = await this.fetchAsUser('POST', `/rooms/${roomId}/messages`, userId, { body: { content } });\n await this.throwIfError(res, 'POST', path);\n const ack = (await res.json()) as SendMessageResult;\n\n if (this.postSend) {\n try {\n await this.postSend({ roomId, seq: ack.seq, time: ack.time, content, senderUserId: userId });\n } catch (err) {\n throw new PostSendError(ack, err);\n }\n }\n\n return ack;\n }\n\n /** Admin message injection (spec §3.3). Presend/postSend hooks do not run for system injections. */\n async sendSystemMessage(roomId: string, content: string, options: SendSystemMessageOptions = {}): Promise<SendMessageResult> {\n const senderId = options.senderId ?? 'system';\n const path = `/admin/rooms/${roomId}/messages`;\n const res = await this.adminRequest('POST', path, { body: { content, senderId } });\n await this.throwIfError(res, 'POST', path);\n return (await res.json()) as SendMessageResult;\n }\n\n async getMessages(roomId: string, options: GetMessagesOptions): Promise<RoomHistoryResponse> {\n const path = `/rooms/${roomId}/messages`;\n const res = await this.fetchAsUser('GET', path, options.asUserId, {\n query: { afterSeq: options.afterSeq, beforeSeq: options.beforeSeq, limit: options.limit }\n });\n await this.throwIfError(res, 'GET', path);\n return (await res.json()) as RoomHistoryResponse;\n }\n\n /** Parses SSE from `GET /rooms/{roomId}/events`, reconnecting on drop when `autoResume` (default true). */\n streamRoomEvents(roomId: string, options: StreamRoomEventsOptions): AsyncIterable<RoomEvent> {\n return this.streamRoomEventsGenerator(roomId, options);\n }\n\n private async *streamRoomEventsGenerator(roomId: string, options: StreamRoomEventsOptions): AsyncGenerator<RoomEvent> {\n const autoResume = options.autoResume ?? true;\n const signal = options.signal;\n let lastSeq = options.afterSeq;\n let backoffMs = 1000;\n const maxBackoffMs = 30_000;\n\n while (true) {\n if (signal?.aborted) return;\n\n let response: Response;\n try {\n response = await this.fetchAsUser('GET', `/rooms/${roomId}/events`, options.asUserId, {\n query: lastSeq === undefined ? {} : { afterSeq: lastSeq },\n extraHeaders: {\n Accept: 'text/event-stream',\n ...(lastSeq !== undefined ? { 'Last-Event-ID': String(lastSeq) } : {})\n },\n signal,\n timeout: false\n });\n } catch (err) {\n if (signal?.aborted || isAbortError(err)) return;\n if (!autoResume) throw err;\n await delay(backoffMs, signal).catch(() => undefined);\n backoffMs = Math.min(backoffMs * 2, maxBackoffMs);\n continue;\n }\n\n if (!response.ok) {\n await this.throwIfError(response, 'GET', `/rooms/${roomId}/events`);\n }\n if (!response.body) return;\n\n let revoked = false;\n try {\n for await (const raw of parseSseStream(response.body, signal)) {\n const event = toRoomEvent(raw);\n if (!event) continue;\n if (event.type === 'message') lastSeq = event.seq;\n yield event;\n if (event.type === 'revoked') {\n revoked = true;\n break;\n }\n }\n } catch (err) {\n if (signal?.aborted || isAbortError(err)) return;\n if (!autoResume) throw err;\n await delay(backoffMs, signal).catch(() => undefined);\n backoffMs = Math.min(backoffMs * 2, maxBackoffMs);\n continue;\n }\n\n if (revoked || !autoResume || signal?.aborted) return;\n\n backoffMs = 1000; // clean disconnect after a successful connection: reset backoff before reconnecting\n await delay(backoffMs, signal).catch(() => undefined);\n }\n }\n\n async redactMessage(roomId: string, seq: number): Promise<RedactMessageResponse> {\n const path = `/admin/rooms/${roomId}/messages/${seq}`;\n const res = await this.adminRequest('DELETE', path);\n await this.throwIfError(res, 'DELETE', path);\n return (await res.json()) as RedactMessageResponse;\n }\n\n // ---------------------------------------------------------------------\n // Audit\n // ---------------------------------------------------------------------\n\n async readAuditLog(afterId = 0, limit = 100): Promise<AuditPageResponse> {\n const path = '/audit';\n const res = await this.auditorRequest('GET', path, { query: { afterId, limit } });\n await this.throwIfError(res, 'GET', path);\n return (await res.json()) as AuditPageResponse;\n }\n\n async verifyAuditLog(full = false): Promise<AuditVerificationResponse> {\n const path = '/audit/verify';\n const res = await this.auditorRequest('GET', path, { query: { full } });\n await this.throwIfError(res, 'GET', path);\n return (await res.json()) as AuditVerificationResponse;\n }\n\n async getAuditCheckpoints(limit = 30): Promise<AuditCheckpointsResponse['checkpoints']> {\n const path = '/audit/checkpoints';\n const res = await this.auditorRequest('GET', path, { query: { limit } });\n await this.throwIfError(res, 'GET', path);\n const data = (await res.json()) as AuditCheckpointsResponse;\n return data.checkpoints;\n }\n\n // ---------------------------------------------------------------------\n // Retention\n // ---------------------------------------------------------------------\n\n async getMessageRetentionDays(): Promise<number> {\n const path = '/admin/retention';\n const res = await this.adminRequest('GET', path);\n await this.throwIfError(res, 'GET', path);\n return ((await res.json()) as RetentionResponse).messagesDays;\n }\n\n async setMessageRetentionDays(days: number): Promise<number> {\n const path = '/admin/retention';\n const res = await this.adminRequest('PUT', path, { body: { messagesDays: days } });\n await this.throwIfError(res, 'PUT', path);\n return ((await res.json()) as RetentionResponse).messagesDays;\n }\n\n // ---------------------------------------------------------------------\n // Request handlers (web-standard Request/Response)\n // ---------------------------------------------------------------------\n\n /** Parses `{ roomId, content }`, calls {@link sendAsUser}, and mirrors the server's own send response shape/status. */\n async handleClientSend(request: Request, senderUserId: string): Promise<Response> {\n let parsed: unknown;\n try {\n parsed = await request.json();\n } catch {\n return jsonResponse(400, { error: 'invalid_request' });\n }\n const result = await this.handleClientSendCore(parsed, senderUserId);\n return jsonResponse(result.status, result.body);\n }\n\n /** Express-friendly variant: `body` is already-parsed JSON. */\n async handleClientSendBody(body: unknown, senderUserId: string): Promise<ClientSendResult> {\n return this.handleClientSendCore(body, senderUserId);\n }\n\n private async handleClientSendCore(body: unknown, senderUserId: string): Promise<ClientSendResult> {\n const b = (body ?? {}) as { roomId?: unknown; content?: unknown };\n if (typeof b.roomId !== 'string' || typeof b.content !== 'string') {\n return { status: 400, body: { error: 'invalid_request' } };\n }\n try {\n const ack = await this.sendAsUser(b.roomId, senderUserId, b.content);\n return { status: 202, body: ack };\n } catch (err) {\n if (err instanceof PresendRejectedError) {\n return { status: 422, body: { error: 'presend_rejected', reason: err.reason } };\n }\n if (err instanceof DotwireApiError) {\n return { status: err.status, body: { error: err.code ?? 'unknown', reason: err.reason } };\n }\n if (err instanceof PostSendError) {\n return { status: 202, body: err.sent };\n }\n throw err;\n }\n }\n\n /** Verifies `X-Dotwire-Signature` (when `webhookSecret` is set), runs the local `presend` hook, and mirrors the server's presend webhook response shape. */\n async handlePresendWebhook(request: Request): Promise<Response> {\n const text = await request.text();\n const signature = request.headers.get('x-dotwire-signature');\n const result = await this.handlePresendWebhookCore(text, signature);\n return jsonResponse(result.status, result.body);\n }\n\n async handlePresendWebhookBody(body: unknown, signatureHeader?: string | null, rawBody?: string): Promise<WebhookResult> {\n const text = rawBody ?? JSON.stringify(body);\n return this.handlePresendWebhookCore(text, signatureHeader ?? null, body);\n }\n\n private async handlePresendWebhookCore(rawText: string, signatureHeader: string | null, parsedBody?: unknown): Promise<WebhookResult> {\n if (!this.verifySignature(rawText, signatureHeader)) {\n return { status: 401, body: { allow: false, reason: 'invalid_signature' } };\n }\n let payload: { roomId?: unknown; senderId?: unknown; content?: unknown };\n try {\n payload = (parsedBody ?? JSON.parse(rawText)) as typeof payload;\n } catch {\n return { status: 400, body: { allow: false, reason: 'invalid_request' } };\n }\n if (typeof payload.roomId !== 'string' || typeof payload.senderId !== 'string' || typeof payload.content !== 'string') {\n return { status: 400, body: { allow: false, reason: 'invalid_request' } };\n }\n\n if (!this.presend) {\n return { status: 200, body: { allow: true } };\n }\n\n try {\n const result = await this.presend({ roomId: payload.roomId, content: payload.content, senderUserId: payload.senderId });\n if (!result.allow) {\n return { status: 200, body: { allow: false, reason: result.rejectionReason } };\n }\n return { status: 200, body: { allow: true, content: result.content } };\n } catch (err) {\n return { status: 200, body: { allow: false, reason: err instanceof Error ? err.message : 'presend hook failed' } };\n }\n }\n\n /** Verifies the signature, runs the local `postSend` hook, and always answers `{}` (postsend is best-effort). */\n async handlePostSendWebhook(request: Request): Promise<Response> {\n const text = await request.text();\n const signature = request.headers.get('x-dotwire-signature');\n const result = await this.handlePostSendWebhookCore(text, signature);\n return jsonResponse(result.status, result.body);\n }\n\n async handlePostSendWebhookBody(body: unknown, signatureHeader?: string | null, rawBody?: string): Promise<WebhookResult> {\n const text = rawBody ?? JSON.stringify(body);\n return this.handlePostSendWebhookCore(text, signatureHeader ?? null, body);\n }\n\n private async handlePostSendWebhookCore(rawText: string, signatureHeader: string | null, parsedBody?: unknown): Promise<WebhookResult> {\n if (!this.verifySignature(rawText, signatureHeader)) {\n return { status: 401, body: {} };\n }\n let payload: { roomId?: unknown; seq?: unknown; senderId?: unknown; content?: unknown; time?: unknown };\n try {\n payload = (parsedBody ?? JSON.parse(rawText)) as typeof payload;\n } catch {\n return { status: 400, body: {} };\n }\n if (\n typeof payload.roomId !== 'string' ||\n typeof payload.seq !== 'number' ||\n typeof payload.senderId !== 'string' ||\n typeof payload.content !== 'string' ||\n typeof payload.time !== 'string'\n ) {\n return { status: 400, body: {} };\n }\n\n if (this.postSend) {\n try {\n await this.postSend({ roomId: payload.roomId, seq: payload.seq, time: payload.time, content: payload.content, senderUserId: payload.senderId });\n } catch {\n // Best-effort: postsend never affects the response the server sees.\n }\n }\n return { status: 200, body: {} };\n }\n\n private verifySignature(rawBody: string, signatureHeader: string | null): boolean {\n if (!this.config.webhookSecret) return true;\n if (!signatureHeader) return false;\n const prefix = 'sha256=';\n if (!signatureHeader.startsWith(prefix)) return false;\n const provided = signatureHeader.slice(prefix.length);\n const expected = createHmac('sha256', this.config.webhookSecret).update(rawBody, 'utf8').digest('hex');\n const providedBuf = Buffer.from(provided, 'hex');\n const expectedBuf = Buffer.from(expected, 'hex');\n if (providedBuf.length !== expectedBuf.length) return false;\n return timingSafeEqual(providedBuf, expectedBuf);\n }\n\n // ---------------------------------------------------------------------\n // HTTP plumbing\n // ---------------------------------------------------------------------\n\n private async adminRequest(method: string, path: string, opts: Omit<RawFetchOptions, 'token'> = {}): Promise<Response> {\n const token = await this.adminToken.get();\n return this.rawFetch(method, path, { ...opts, token });\n }\n\n private async auditorRequest(method: string, path: string, opts: Omit<RawFetchOptions, 'token'> = {}): Promise<Response> {\n const token = await this.auditorToken.get();\n return this.rawFetch(method, path, { ...opts, token });\n }\n\n /**\n * Role resolution for user-scoped operations (spec §4.3 / §5): try the cached role (or\n * `member` when nothing is cached); on 403, look the role up through the admin API, cache it,\n * and retry once. A second 403 surfaces as a `Forbidden` {@link DotwireApiError}.\n */\n private async fetchAsUser(method: string, path: string, userId: string, opts: Omit<RawFetchOptions, 'token'> = {}): Promise<Response> {\n const cachedRole = this.roleCache.get(userId);\n const firstRole = cachedRole ?? 'member';\n\n const attempt = async (role: DotwireRole): Promise<Response> => {\n const token = await this.signer.mintToken(userId, { role, ttl: this.defaultTtl });\n return this.rawFetch(method, path, { ...opts, token });\n };\n\n let res = await attempt(firstRole);\n if (res.status === 403) {\n const looked = await this.lookupRole(userId);\n const resolvedRole = looked ?? 'member';\n this.roleCache.set(userId, resolvedRole);\n res = await attempt(resolvedRole);\n }\n return res;\n }\n\n private async rawFetch(method: string, path: string, opts: RawFetchOptions): Promise<Response> {\n const url = new URL(this.baseUrl + path);\n if (opts.query) {\n for (const [key, value] of Object.entries(opts.query)) {\n if (value !== undefined) url.searchParams.set(key, String(value));\n }\n }\n\n const headers: Record<string, string> = { ...opts.extraHeaders };\n if (opts.token) headers.Authorization = `Bearer ${opts.token}`;\n if (opts.body !== undefined) headers['Content-Type'] = 'application/json';\n\n const useTimeout = opts.timeout ?? true;\n let signal = opts.signal;\n if (useTimeout) {\n const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);\n signal = opts.signal ? AbortSignal.any([opts.signal, timeoutSignal]) : timeoutSignal;\n }\n\n return this.fetchImpl(url.toString(), {\n method,\n headers,\n body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,\n signal\n });\n }\n\n private async throwIfError(res: Response, method: string, path: string): Promise<void> {\n if (res.ok) return;\n let code: string | undefined;\n let reason: string | undefined;\n try {\n const clone = res.clone();\n const body = (await clone.json()) as { error?: string; reason?: string };\n code = body?.error;\n reason = body?.reason;\n } catch {\n // bare error response (e.g. plain 401/403/404) - no body to parse.\n }\n const retryAfterHeader = res.headers.get('retry-after');\n const retryAfterMs = retryAfterHeader ? Number(retryAfterHeader) * 1000 : undefined;\n throw new DotwireApiError({\n status: res.status,\n kind: mapErrorKind(res.status, code),\n code,\n reason,\n retryAfterMs,\n method,\n path\n });\n }\n}\n\nfunction jsonResponse(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n}\n","import type { RoomEvent } from './types.js';\n\ninterface RawSseEvent {\n event: string;\n data: string;\n id?: string;\n}\n\n/**\n * Parses a `text/event-stream` body into raw SSE events, per spec §3.9's wire format:\n * `id:`/`event:`/`data:` lines, blank-line terminated, `: ping` comment lines ignored.\n * Tolerant of both `\\n` and `\\r\\n` line endings.\n */\nexport async function* parseSseStream(\n body: ReadableStream<Uint8Array>,\n signal?: AbortSignal\n): AsyncGenerator<RawSseEvent> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n try {\n while (true) {\n if (signal?.aborted) return;\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, '\\n');\n\n let idx: number;\n while ((idx = buffer.indexOf('\\n\\n')) !== -1) {\n const block = buffer.slice(0, idx);\n buffer = buffer.slice(idx + 2);\n const parsed = parseEventBlock(block);\n if (parsed) yield parsed;\n }\n }\n } finally {\n try {\n reader.releaseLock();\n } catch {\n // already released/closed\n }\n }\n}\n\nfunction parseEventBlock(block: string): RawSseEvent | null {\n let eventName = 'message';\n let id: string | undefined;\n const dataLines: string[] = [];\n let sawData = false;\n\n for (const line of block.split('\\n')) {\n if (line.length === 0) continue;\n if (line.startsWith(':')) continue; // comment / keep-alive ping, e.g. \": ping\"\n if (line.startsWith('event:')) {\n eventName = line.slice('event:'.length).trim();\n } else if (line.startsWith('id:')) {\n id = line.slice('id:'.length).trim();\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice('data:'.length).trim());\n sawData = true;\n }\n }\n\n if (!sawData) return null;\n return { event: eventName, data: dataLines.join('\\n'), id };\n}\n\n/** Converts one raw SSE event to a typed {@link RoomEvent}, or null for anything unrecognized. */\nexport function toRoomEvent(raw: RawSseEvent): RoomEvent | null {\n let data: unknown;\n try {\n data = JSON.parse(raw.data);\n } catch {\n return null;\n }\n if (typeof data !== 'object' || data === null) return null;\n const d = data as Record<string, unknown>;\n\n switch (raw.event) {\n case 'message':\n return {\n type: 'message',\n roomId: String(d.roomId),\n seq: Number(d.seq),\n senderId: String(d.senderId),\n time: String(d.time),\n content: String(d.content),\n replayed: d.replayed === true\n };\n case 'retracted':\n return { type: 'retracted', roomId: String(d.roomId), seq: Number(d.seq) };\n case 'typing':\n return { type: 'typing', roomId: String(d.roomId), userId: String(d.userId), isTyping: Boolean(d.isTyping) };\n case 'presence':\n return {\n type: 'presence',\n roomId: String(d.roomId),\n joined: Array.isArray(d.joined) ? d.joined.map(String) : [],\n left: Array.isArray(d.left) ? d.left.map(String) : []\n };\n case 'revoked':\n return { type: 'revoked', roomId: String(d.roomId) };\n default:\n return null;\n }\n}\n","/** Resolves after `ms`, or rejects with an AbortError if `signal` fires first. */\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n const timer = setTimeout(() => {\n cleanup();\n resolve();\n }, ms);\n const onAbort = () => {\n cleanup();\n reject(new DOMException('Aborted', 'AbortError'));\n };\n const cleanup = () => {\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/** True if `err` represents cancellation via AbortSignal (fetch's AbortError, or our own {@link delay}). */\nexport function isAbortError(err: unknown): boolean {\n return err instanceof Error && err.name === 'AbortError';\n}\n","import { DotwireApiError } from './errors.js';\nimport type { DotwireHostClient } from './client.js';\nimport type { PostSendContext } from './types.js';\nimport { delay } from './util.js';\n\n/** An accepted message awaiting asynchronous review. (roomId, seq) is the redaction target. */\nexport interface PendingModeration {\n roomId: string;\n seq: number;\n time: string;\n senderUserId: string;\n content: string;\n}\n\n/** One classifier decision. Only flagged verdicts trigger an action. */\nexport interface ModerationVerdict {\n roomId: string;\n seq: number;\n flagged: boolean;\n category?: string;\n reason?: string;\n}\n\n/**\n * The expensive judgment call, run off the send path over a whole batch at once. Implementations\n * call an LLM (see `samples/moderation` for a Claude Message Batches API reference implementation),\n * a third-party moderation API, or anything else too slow to sit inside a send. Return one verdict\n * per input; inputs with no verdict are treated as not flagged.\n */\nexport interface ModerationClassifier {\n classify(batch: readonly PendingModeration[], signal?: AbortSignal): Promise<readonly ModerationVerdict[]>;\n}\n\n/**\n * In-process hand-off between the send path and the {@link BatchModerationWorker}. Enqueueing\n * (via {@link postSendHook}) is a microtask-cheap array push, so wiring it into\n * `DotwireHostConfig.postSend` adds no meaningful latency to a send. The verdict itself is\n * computed later, in batches, by the worker.\n *\n * Bounded at `capacity`: once full, {@link enqueue} (and therefore `postSendHook`) waits for\n * room rather than silently dropping - the queue applies back-pressure to sends, mirroring the\n * .NET port's `BoundedChannelFullMode.Wait` default.\n */\nexport class ModerationQueue {\n private readonly items: PendingModeration[] = [];\n private readonly waitingReaders: Array<() => void> = [];\n private readonly waitingWriters: Array<() => void> = [];\n private completed = false;\n\n private readonly capacity: number;\n\n constructor(capacity: number = 10_000) {\n this.capacity = capacity;\n }\n\n async enqueue(item: PendingModeration, signal?: AbortSignal): Promise<void> {\n if (this.completed) throw new Error('ModerationQueue is already completed');\n while (this.items.length >= this.capacity) {\n await this.waitForWriteSlot(signal);\n }\n this.items.push(item);\n this.wake(this.waitingReaders);\n }\n\n /** Assign directly: `client.postSend = queue.postSendHook`. */\n postSendHook = (ctx: PostSendContext): Promise<void> =>\n this.enqueue({ roomId: ctx.roomId, seq: ctx.seq, time: ctx.time, senderUserId: ctx.senderUserId, content: ctx.content });\n\n /** Signals the worker to drain what is queued and stop. */\n complete(): void {\n this.completed = true;\n this.wake(this.waitingReaders);\n }\n\n /** Blocks for an item; resolves null once completed and drained, or if `signal` fires. */\n async dequeue(signal?: AbortSignal): Promise<PendingModeration | null> {\n while (this.items.length === 0) {\n if (this.completed || signal?.aborted) return null;\n await new Promise<void>((resolve) => {\n const onAbort = () => resolve();\n signal?.addEventListener('abort', onAbort, { once: true });\n this.waitingReaders.push(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n });\n });\n }\n return this.take();\n }\n\n /** Waits up to `timeoutMs` for an item. Resolves 'timeout' on expiry, null on completion/abort. */\n async tryDequeue(timeoutMs: number, signal?: AbortSignal): Promise<PendingModeration | null | 'timeout'> {\n if (this.items.length > 0) return this.take();\n if (this.completed || signal?.aborted) return null;\n\n return new Promise((resolve) => {\n let settled = false;\n const finish = (value: PendingModeration | null | 'timeout') => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n signal?.removeEventListener('abort', onAbort);\n resolve(value);\n };\n const timer = setTimeout(() => finish('timeout'), timeoutMs);\n const onAbort = () => finish(null);\n const wake = (): void => {\n if (settled) return;\n if (this.items.length > 0) finish(this.take());\n else if (this.completed) finish(null);\n else this.waitingReaders.push(wake);\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n this.waitingReaders.push(wake);\n });\n }\n\n private take(): PendingModeration {\n const item = this.items.shift()!;\n const writer = this.waitingWriters.shift();\n if (writer) writer();\n return item;\n }\n\n private waitForWriteSlot(signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n const onAbort = () => resolve();\n signal?.addEventListener('abort', onAbort, { once: true });\n this.waitingWriters.push(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n });\n });\n }\n\n private wake(waiters: Array<() => void>): void {\n const pending = waiters.splice(0);\n for (const w of pending) w();\n }\n}\n\nexport interface BatchModerationOptions {\n /** Flush when this many messages are queued... Default 100. */\n maxBatchSize?: number;\n /** ...or when the oldest queued message has waited this long, whichever comes first. Default 30000. */\n maxBatchDelayMs?: number;\n /** Classifier attempts per batch (exponential backoff between them) before the batch is dropped. Default 3. */\n maxClassifierAttempts?: number;\n}\n\n/**\n * Drains a {@link ModerationQueue} in size-or-time bounded batches, hands each batch to the\n * {@link ModerationClassifier}, and runs `onFlagged` for every flagged verdict (typically\n * {@link BatchModerationWorker.redactWith}). Runs entirely off the send path.\n */\nexport class BatchModerationWorker {\n private readonly queue: ModerationQueue;\n private readonly classifier: ModerationClassifier;\n private readonly onFlagged: (verdict: ModerationVerdict, signal?: AbortSignal) => Promise<void> | void;\n private readonly onError?: (err: unknown) => void;\n private readonly maxBatchSize: number;\n private readonly maxBatchDelayMs: number;\n private readonly maxClassifierAttempts: number;\n\n constructor(\n queue: ModerationQueue,\n classifier: ModerationClassifier,\n onFlagged: (verdict: ModerationVerdict, signal?: AbortSignal) => Promise<void> | void,\n options: BatchModerationOptions = {},\n onError?: (err: unknown) => void\n ) {\n this.queue = queue;\n this.classifier = classifier;\n this.onFlagged = onFlagged;\n this.onError = onError;\n this.maxBatchSize = options.maxBatchSize ?? 100;\n this.maxBatchDelayMs = options.maxBatchDelayMs ?? 30_000;\n this.maxClassifierAttempts = options.maxClassifierAttempts ?? 3;\n }\n\n /** The usual action: physically redact the flagged message through the admin API, honouring `Retry-After` on 429. */\n static redactWith(host: DotwireHostClient): (verdict: ModerationVerdict) => Promise<void> {\n return async (verdict) => {\n const maxAttempts = 3;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n await host.redactMessage(verdict.roomId, verdict.seq);\n return;\n } catch (err) {\n if (err instanceof DotwireApiError && err.status === 429 && attempt < maxAttempts) {\n await delay(err.retryAfterMs ?? 1000 * attempt);\n continue;\n }\n throw err;\n }\n }\n };\n }\n\n /** Runs until `signal` fires or the queue is completed and drained. */\n async run(signal: AbortSignal): Promise<void> {\n while (true) {\n const batch = await this.fillBatch(signal);\n if (batch.length === 0) return;\n await this.processBatch(batch, signal);\n if (signal.aborted) return;\n }\n }\n\n private async fillBatch(signal: AbortSignal): Promise<PendingModeration[]> {\n const batch: PendingModeration[] = [];\n const first = await this.queue.dequeue(signal);\n if (first === null) return batch;\n batch.push(first);\n\n const deadline = Date.now() + this.maxBatchDelayMs;\n while (batch.length < this.maxBatchSize) {\n const remaining = deadline - Date.now();\n if (remaining <= 0) break;\n const item = await this.queue.tryDequeue(remaining, signal);\n if (item === 'timeout' || item === null) break;\n batch.push(item);\n }\n return batch;\n }\n\n private async processBatch(batch: PendingModeration[], signal: AbortSignal): Promise<void> {\n let verdicts: readonly ModerationVerdict[] | undefined;\n for (let attempt = 1; attempt <= this.maxClassifierAttempts; attempt++) {\n try {\n verdicts = await this.classifier.classify(batch, signal);\n break;\n } catch (err) {\n if (signal.aborted) return;\n this.onError?.(err);\n if (attempt === this.maxClassifierAttempts) return; // best-effort: drop the batch, don't wedge the worker\n await delay(1000 * Math.pow(2, attempt), signal).catch(() => undefined);\n }\n }\n\n if (!verdicts) return;\n\n for (const verdict of verdicts) {\n if (!verdict.flagged) continue;\n try {\n await this.onFlagged(verdict, signal);\n } catch (err) {\n if (signal.aborted) return;\n this.onError?.(err);\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
type DotwireRole = 'member' | 'auditor' | 'admin';
|
|
2
|
+
interface DotwireHostConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
issuer: string;
|
|
5
|
+
audience?: string;
|
|
6
|
+
keyId: string;
|
|
7
|
+
privateKeyPem: string;
|
|
8
|
+
adminUserId?: string;
|
|
9
|
+
auditorUserId?: string;
|
|
10
|
+
/** jose-style duration string (e.g. "15m"), passed straight through to `SignJWT#setExpirationTime`. */
|
|
11
|
+
defaultTokenTtl?: string;
|
|
12
|
+
requestTimeoutMs?: number;
|
|
13
|
+
/** HMAC-SHA256 secret used to verify `X-Dotwire-Signature` on incoming webhook calls. */
|
|
14
|
+
webhookSecret?: string;
|
|
15
|
+
roleCacheTtlMs?: number;
|
|
16
|
+
/** Every request goes through this so tests can inject a fake. Defaults to global `fetch`. */
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
/**
|
|
19
|
+
* Runs in-process before {@link DotwireHostClient.sendAsUser} and {@link DotwireHostClient.handleClientSend},
|
|
20
|
+
* and inside {@link DotwireHostClient.handlePresendWebhook} - one place for moderation logic that
|
|
21
|
+
* covers both host-initiated sends and messages sent directly by `@dotwire/client`.
|
|
22
|
+
*/
|
|
23
|
+
presend?: (ctx: PresendContext) => Promise<PresendResult> | PresendResult;
|
|
24
|
+
/** Same idea as {@link presend}, run after a message is accepted; see {@link PostSendContext}. */
|
|
25
|
+
postSend?: (ctx: PostSendContext) => Promise<void> | void;
|
|
26
|
+
}
|
|
27
|
+
interface MintTokenOptions {
|
|
28
|
+
role?: DotwireRole;
|
|
29
|
+
displayName?: string;
|
|
30
|
+
/** jose-style duration string, e.g. "15m". Defaults to `config.defaultTokenTtl`. */
|
|
31
|
+
ttl?: string;
|
|
32
|
+
}
|
|
33
|
+
interface PresendContext {
|
|
34
|
+
roomId: string;
|
|
35
|
+
content: string;
|
|
36
|
+
senderUserId: string;
|
|
37
|
+
}
|
|
38
|
+
interface PresendResult {
|
|
39
|
+
allow: boolean;
|
|
40
|
+
/** Rewritten content; only meaningful when `allow` is true. */
|
|
41
|
+
content?: string;
|
|
42
|
+
rejectionReason?: string;
|
|
43
|
+
}
|
|
44
|
+
declare const PresendResult: {
|
|
45
|
+
accept(content?: string): PresendResult;
|
|
46
|
+
reject(reason: string): PresendResult;
|
|
47
|
+
};
|
|
48
|
+
/** What a postSend hook sees: the accepted message plus its ack, so it can be found again by (roomId, seq). */
|
|
49
|
+
interface PostSendContext {
|
|
50
|
+
roomId: string;
|
|
51
|
+
seq: number;
|
|
52
|
+
time: string;
|
|
53
|
+
content: string;
|
|
54
|
+
senderUserId: string;
|
|
55
|
+
}
|
|
56
|
+
interface SendMessageResult {
|
|
57
|
+
seq: number;
|
|
58
|
+
time: string;
|
|
59
|
+
}
|
|
60
|
+
interface HistoryMessage {
|
|
61
|
+
seq: number;
|
|
62
|
+
time: string;
|
|
63
|
+
senderId: string;
|
|
64
|
+
content: string;
|
|
65
|
+
}
|
|
66
|
+
interface RoomHistoryResponse {
|
|
67
|
+
roomId: string;
|
|
68
|
+
messages: HistoryMessage[];
|
|
69
|
+
hasMore: boolean;
|
|
70
|
+
}
|
|
71
|
+
interface UserRoleResponse {
|
|
72
|
+
userId: string;
|
|
73
|
+
role: DotwireRole;
|
|
74
|
+
}
|
|
75
|
+
interface RoomMembersResponse {
|
|
76
|
+
roomId: string;
|
|
77
|
+
members: string[];
|
|
78
|
+
}
|
|
79
|
+
interface UserRoomsResponse {
|
|
80
|
+
userId: string;
|
|
81
|
+
roomIds: string[];
|
|
82
|
+
}
|
|
83
|
+
interface RedactMessageResponse {
|
|
84
|
+
roomId: string;
|
|
85
|
+
seq: number;
|
|
86
|
+
removedFromHistory: boolean;
|
|
87
|
+
removedFromStream: boolean;
|
|
88
|
+
}
|
|
89
|
+
interface RetentionResponse {
|
|
90
|
+
messagesDays: number;
|
|
91
|
+
}
|
|
92
|
+
interface AuditEntry {
|
|
93
|
+
id: number;
|
|
94
|
+
eventType: string;
|
|
95
|
+
roomId: string | null;
|
|
96
|
+
messageSeq: number | null;
|
|
97
|
+
actorId: string | null;
|
|
98
|
+
subjectId: string | null;
|
|
99
|
+
value: number | null;
|
|
100
|
+
time: string;
|
|
101
|
+
hash: string;
|
|
102
|
+
}
|
|
103
|
+
interface AuditPageResponse {
|
|
104
|
+
events: AuditEntry[];
|
|
105
|
+
hasMore: boolean;
|
|
106
|
+
}
|
|
107
|
+
interface AuditVerificationResponse {
|
|
108
|
+
ok: boolean;
|
|
109
|
+
fromId: number;
|
|
110
|
+
toId: number;
|
|
111
|
+
checked: number;
|
|
112
|
+
firstInvalidId: number | null;
|
|
113
|
+
anchor: string | null;
|
|
114
|
+
}
|
|
115
|
+
interface AuditCheckpointEntry {
|
|
116
|
+
day: string;
|
|
117
|
+
lastId: number;
|
|
118
|
+
hash: string;
|
|
119
|
+
createdAt: string;
|
|
120
|
+
}
|
|
121
|
+
interface AuditCheckpointsResponse {
|
|
122
|
+
checkpoints: AuditCheckpointEntry[];
|
|
123
|
+
}
|
|
124
|
+
interface UserExportMessage {
|
|
125
|
+
roomId: string;
|
|
126
|
+
seq: number;
|
|
127
|
+
time: string;
|
|
128
|
+
content: string | null;
|
|
129
|
+
undecryptable?: boolean;
|
|
130
|
+
}
|
|
131
|
+
interface UserExportAuditEvent {
|
|
132
|
+
id: number;
|
|
133
|
+
eventType: string;
|
|
134
|
+
roomId: string | null;
|
|
135
|
+
messageSeq: number | null;
|
|
136
|
+
actorId: string | null;
|
|
137
|
+
subjectId: string | null;
|
|
138
|
+
value: number | null;
|
|
139
|
+
time: string;
|
|
140
|
+
}
|
|
141
|
+
interface UserExport {
|
|
142
|
+
userId: string;
|
|
143
|
+
exportedAt: string;
|
|
144
|
+
role: DotwireRole | null;
|
|
145
|
+
roomIds: string[];
|
|
146
|
+
messages: UserExportMessage[];
|
|
147
|
+
auditEvents: UserExportAuditEvent[];
|
|
148
|
+
}
|
|
149
|
+
interface GetMessagesOptions {
|
|
150
|
+
asUserId: string;
|
|
151
|
+
afterSeq?: number;
|
|
152
|
+
beforeSeq?: number;
|
|
153
|
+
limit?: number;
|
|
154
|
+
}
|
|
155
|
+
interface AddRoomMembersOptions {
|
|
156
|
+
/** Grants `member` to any listed user with no existing role, without touching an existing auditor/admin. Default true. */
|
|
157
|
+
ensureMemberRole?: boolean;
|
|
158
|
+
}
|
|
159
|
+
interface SendSystemMessageOptions {
|
|
160
|
+
/** Defaults to "system". */
|
|
161
|
+
senderId?: string;
|
|
162
|
+
}
|
|
163
|
+
interface StreamRoomEventsOptions {
|
|
164
|
+
asUserId: string;
|
|
165
|
+
afterSeq?: number;
|
|
166
|
+
/** Reconnect on drop with `Last-Event-ID`, exponential backoff 1s..30s, until cancelled or revoked. Default true. */
|
|
167
|
+
autoResume?: boolean;
|
|
168
|
+
signal?: AbortSignal;
|
|
169
|
+
}
|
|
170
|
+
interface TokenResponseOptions {
|
|
171
|
+
userId: string;
|
|
172
|
+
role?: DotwireRole;
|
|
173
|
+
displayName?: string;
|
|
174
|
+
ttl?: string;
|
|
175
|
+
}
|
|
176
|
+
/** Discriminated union of everything `GET /rooms/{roomId}/events` can emit (spec §3.9). */
|
|
177
|
+
type RoomEvent = {
|
|
178
|
+
type: 'message';
|
|
179
|
+
roomId: string;
|
|
180
|
+
seq: number;
|
|
181
|
+
senderId: string;
|
|
182
|
+
time: string;
|
|
183
|
+
content: string;
|
|
184
|
+
replayed?: boolean;
|
|
185
|
+
} | {
|
|
186
|
+
type: 'retracted';
|
|
187
|
+
roomId: string;
|
|
188
|
+
seq: number;
|
|
189
|
+
} | {
|
|
190
|
+
type: 'typing';
|
|
191
|
+
roomId: string;
|
|
192
|
+
userId: string;
|
|
193
|
+
isTyping: boolean;
|
|
194
|
+
} | {
|
|
195
|
+
type: 'presence';
|
|
196
|
+
roomId: string;
|
|
197
|
+
joined: string[];
|
|
198
|
+
left: string[];
|
|
199
|
+
} | {
|
|
200
|
+
type: 'revoked';
|
|
201
|
+
roomId: string;
|
|
202
|
+
};
|
|
203
|
+
/** Result of {@link DotwireHostClient.handleClientSendBody} / the Request-based `handleClientSend`. */
|
|
204
|
+
interface ClientSendResult {
|
|
205
|
+
status: number;
|
|
206
|
+
body: {
|
|
207
|
+
seq: number;
|
|
208
|
+
time: string;
|
|
209
|
+
} | {
|
|
210
|
+
error: string;
|
|
211
|
+
reason?: string;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** Result of the two webhook handlers' body-based variants. */
|
|
215
|
+
interface WebhookResult {
|
|
216
|
+
status: number;
|
|
217
|
+
body: {
|
|
218
|
+
allow: boolean;
|
|
219
|
+
content?: string;
|
|
220
|
+
reason?: string;
|
|
221
|
+
} | Record<string, never>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Mirrors `Dotwire.Host.DotwireApiException.Kind` (.NET) / the wire error contract in spec §3.14.
|
|
226
|
+
* Mapped from HTTP status first, then the `error` code in the body.
|
|
227
|
+
*/
|
|
228
|
+
type DotwireApiErrorKind = 'BadRequest' | 'Unauthorized' | 'Forbidden' | 'NotFound' | 'PresendRejected' | 'RateLimited' | 'Unavailable' | 'Unknown';
|
|
229
|
+
interface DotwireApiErrorInit {
|
|
230
|
+
status: number;
|
|
231
|
+
kind: DotwireApiErrorKind;
|
|
232
|
+
code?: string;
|
|
233
|
+
reason?: string;
|
|
234
|
+
retryAfterMs?: number;
|
|
235
|
+
method: string;
|
|
236
|
+
path: string;
|
|
237
|
+
}
|
|
238
|
+
/** Thrown when the dotwire server answers a request with a non-success status. */
|
|
239
|
+
declare class DotwireApiError extends Error {
|
|
240
|
+
readonly status: number;
|
|
241
|
+
readonly kind: DotwireApiErrorKind;
|
|
242
|
+
readonly code?: string;
|
|
243
|
+
readonly reason?: string;
|
|
244
|
+
readonly retryAfterMs?: number;
|
|
245
|
+
readonly method: string;
|
|
246
|
+
readonly path: string;
|
|
247
|
+
constructor(init: DotwireApiErrorInit);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Maps an HTTP status (and, where ambiguous, the wire error code from spec §3.14) to a
|
|
251
|
+
* {@link DotwireApiErrorKind}. Status is authoritative; code only disambiguates 422/503/429/400.
|
|
252
|
+
*/
|
|
253
|
+
declare function mapErrorKind(status: number, code?: string): DotwireApiErrorKind;
|
|
254
|
+
/**
|
|
255
|
+
* Thrown when the host's own {@link DotwireHostConfig.presend} hook rejects a message, evaluated
|
|
256
|
+
* in-process before the message ever reaches the wire (no network round-trip). Distinct from a
|
|
257
|
+
* {@link DotwireApiError} with kind `PresendRejected`, which is the *server's* configured presend
|
|
258
|
+
* webhook rejecting a message on `POST /rooms/{roomId}/messages` (a different, server-side hook).
|
|
259
|
+
*/
|
|
260
|
+
declare class PresendRejectedError extends Error {
|
|
261
|
+
readonly reason: string;
|
|
262
|
+
constructor(reason: string);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The message WAS sent (see {@link sent}); only the local `postSend` hook failed afterwards.
|
|
266
|
+
* Kept distinct from a send failure so callers never retry a send that already succeeded.
|
|
267
|
+
*/
|
|
268
|
+
declare class PostSendError extends Error {
|
|
269
|
+
readonly sent: SendMessageResult;
|
|
270
|
+
constructor(sent: SendMessageResult, cause: unknown);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
declare class DotwireTokenSigner {
|
|
274
|
+
private readonly config;
|
|
275
|
+
private readonly keyPromise;
|
|
276
|
+
private readonly defaultTtl;
|
|
277
|
+
constructor(config: DotwireHostConfig);
|
|
278
|
+
mintToken(userId: string, options?: MintTokenOptions): Promise<string>;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Node/TypeScript counterpart of `Dotwire.Host.DotwireHostClient` (.NET). Mints its own tokens
|
|
283
|
+
* and talks to a dotwire server entirely over its frozen HTTP surface - see spec §3 for the
|
|
284
|
+
* server's wire shapes and §4/§5 for the identity and role-resolution rules this class follows.
|
|
285
|
+
*/
|
|
286
|
+
declare class DotwireHostClient {
|
|
287
|
+
private readonly config;
|
|
288
|
+
private readonly signer;
|
|
289
|
+
private readonly baseUrl;
|
|
290
|
+
private readonly fetchImpl;
|
|
291
|
+
private readonly requestTimeoutMs;
|
|
292
|
+
private readonly adminUserId;
|
|
293
|
+
private readonly auditorUserId;
|
|
294
|
+
private readonly defaultTtl;
|
|
295
|
+
private readonly roleCache;
|
|
296
|
+
private readonly adminToken;
|
|
297
|
+
private readonly auditorToken;
|
|
298
|
+
presend?: (ctx: PresendContext) => Promise<PresendResult> | PresendResult;
|
|
299
|
+
postSend?: (ctx: PostSendContext) => Promise<void> | void;
|
|
300
|
+
constructor(config: DotwireHostConfig);
|
|
301
|
+
/** Mints a token directly. An explicit `role` bypasses all role resolution. */
|
|
302
|
+
mintToken(userId: string, options?: MintTokenOptions): Promise<string>;
|
|
303
|
+
/**
|
|
304
|
+
* Returns a `Response` with body `{ token }`. When `role` is omitted, resolves it through the
|
|
305
|
+
* cached admin lookup and falls back to `member` for an unknown user (404 on the lookup) since
|
|
306
|
+
* there is no request of its own to fail here.
|
|
307
|
+
*/
|
|
308
|
+
tokenResponse(options: TokenResponseOptions): Promise<Response>;
|
|
309
|
+
private resolveRoleForToken;
|
|
310
|
+
setUserRole(userId: string, role: DotwireRole): Promise<UserRoleResponse>;
|
|
311
|
+
getUserRole(userId: string): Promise<UserRoleResponse | null>;
|
|
312
|
+
deleteUserRole(userId: string): Promise<void>;
|
|
313
|
+
getUserRooms(userId: string): Promise<string[]>;
|
|
314
|
+
exportUser(userId: string): Promise<UserExport>;
|
|
315
|
+
/** Returns the raw response stream so the caller can pipe an arbitrarily large export without buffering it. */
|
|
316
|
+
exportUserStream(userId: string): Promise<ReadableStream<Uint8Array>>;
|
|
317
|
+
private lookupRole;
|
|
318
|
+
room(roomId: string): DotwireHostRoom;
|
|
319
|
+
addRoomMembers(roomId: string, userIds: string[], options?: AddRoomMembersOptions): Promise<string[]>;
|
|
320
|
+
removeRoomMember(roomId: string, userId: string): Promise<void>;
|
|
321
|
+
getRoomMembers(roomId: string): Promise<string[]>;
|
|
322
|
+
/** Reads participants under the user's own room membership, without admin access. */
|
|
323
|
+
getRoomParticipants(roomId: string, asUserId: string): Promise<string[]>;
|
|
324
|
+
sendAsUser(roomId: string, userId: string, content: string): Promise<SendMessageResult>;
|
|
325
|
+
/** Admin message injection (spec §3.3). Presend/postSend hooks do not run for system injections. */
|
|
326
|
+
sendSystemMessage(roomId: string, content: string, options?: SendSystemMessageOptions): Promise<SendMessageResult>;
|
|
327
|
+
getMessages(roomId: string, options: GetMessagesOptions): Promise<RoomHistoryResponse>;
|
|
328
|
+
/** Parses SSE from `GET /rooms/{roomId}/events`, reconnecting on drop when `autoResume` (default true). */
|
|
329
|
+
streamRoomEvents(roomId: string, options: StreamRoomEventsOptions): AsyncIterable<RoomEvent>;
|
|
330
|
+
private streamRoomEventsGenerator;
|
|
331
|
+
redactMessage(roomId: string, seq: number): Promise<RedactMessageResponse>;
|
|
332
|
+
readAuditLog(afterId?: number, limit?: number): Promise<AuditPageResponse>;
|
|
333
|
+
verifyAuditLog(full?: boolean): Promise<AuditVerificationResponse>;
|
|
334
|
+
getAuditCheckpoints(limit?: number): Promise<AuditCheckpointsResponse['checkpoints']>;
|
|
335
|
+
getMessageRetentionDays(): Promise<number>;
|
|
336
|
+
setMessageRetentionDays(days: number): Promise<number>;
|
|
337
|
+
/** Parses `{ roomId, content }`, calls {@link sendAsUser}, and mirrors the server's own send response shape/status. */
|
|
338
|
+
handleClientSend(request: Request, senderUserId: string): Promise<Response>;
|
|
339
|
+
/** Express-friendly variant: `body` is already-parsed JSON. */
|
|
340
|
+
handleClientSendBody(body: unknown, senderUserId: string): Promise<ClientSendResult>;
|
|
341
|
+
private handleClientSendCore;
|
|
342
|
+
/** Verifies `X-Dotwire-Signature` (when `webhookSecret` is set), runs the local `presend` hook, and mirrors the server's presend webhook response shape. */
|
|
343
|
+
handlePresendWebhook(request: Request): Promise<Response>;
|
|
344
|
+
handlePresendWebhookBody(body: unknown, signatureHeader?: string | null, rawBody?: string): Promise<WebhookResult>;
|
|
345
|
+
private handlePresendWebhookCore;
|
|
346
|
+
/** Verifies the signature, runs the local `postSend` hook, and always answers `{}` (postsend is best-effort). */
|
|
347
|
+
handlePostSendWebhook(request: Request): Promise<Response>;
|
|
348
|
+
handlePostSendWebhookBody(body: unknown, signatureHeader?: string | null, rawBody?: string): Promise<WebhookResult>;
|
|
349
|
+
private handlePostSendWebhookCore;
|
|
350
|
+
private verifySignature;
|
|
351
|
+
private adminRequest;
|
|
352
|
+
private auditorRequest;
|
|
353
|
+
/**
|
|
354
|
+
* Role resolution for user-scoped operations (spec §4.3 / §5): try the cached role (or
|
|
355
|
+
* `member` when nothing is cached); on 403, look the role up through the admin API, cache it,
|
|
356
|
+
* and retry once. A second 403 surfaces as a `Forbidden` {@link DotwireApiError}.
|
|
357
|
+
*/
|
|
358
|
+
private fetchAsUser;
|
|
359
|
+
private rawFetch;
|
|
360
|
+
private throwIfError;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Mirrors {@link DotwireHostClient}'s room-scoped methods without the repeated `roomId` argument. */
|
|
364
|
+
declare class DotwireHostRoom {
|
|
365
|
+
private readonly client;
|
|
366
|
+
readonly roomId: string;
|
|
367
|
+
constructor(client: DotwireHostClient, roomId: string);
|
|
368
|
+
addMembers(userIds: string[], options?: AddRoomMembersOptions): Promise<string[]>;
|
|
369
|
+
removeMember(userId: string): Promise<void>;
|
|
370
|
+
getMembers(): Promise<string[]>;
|
|
371
|
+
getParticipants(asUserId: string): Promise<string[]>;
|
|
372
|
+
sendAsUser(userId: string, content: string): Promise<SendMessageResult>;
|
|
373
|
+
sendSystemMessage(content: string, options?: SendSystemMessageOptions): Promise<SendMessageResult>;
|
|
374
|
+
getMessages(options: GetMessagesOptions): Promise<RoomHistoryResponse>;
|
|
375
|
+
events(options: StreamRoomEventsOptions): AsyncIterable<RoomEvent>;
|
|
376
|
+
redact(seq: number): Promise<RedactMessageResponse>;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** An accepted message awaiting asynchronous review. (roomId, seq) is the redaction target. */
|
|
380
|
+
interface PendingModeration {
|
|
381
|
+
roomId: string;
|
|
382
|
+
seq: number;
|
|
383
|
+
time: string;
|
|
384
|
+
senderUserId: string;
|
|
385
|
+
content: string;
|
|
386
|
+
}
|
|
387
|
+
/** One classifier decision. Only flagged verdicts trigger an action. */
|
|
388
|
+
interface ModerationVerdict {
|
|
389
|
+
roomId: string;
|
|
390
|
+
seq: number;
|
|
391
|
+
flagged: boolean;
|
|
392
|
+
category?: string;
|
|
393
|
+
reason?: string;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* The expensive judgment call, run off the send path over a whole batch at once. Implementations
|
|
397
|
+
* call an LLM (see `samples/moderation` for a Claude Message Batches API reference implementation),
|
|
398
|
+
* a third-party moderation API, or anything else too slow to sit inside a send. Return one verdict
|
|
399
|
+
* per input; inputs with no verdict are treated as not flagged.
|
|
400
|
+
*/
|
|
401
|
+
interface ModerationClassifier {
|
|
402
|
+
classify(batch: readonly PendingModeration[], signal?: AbortSignal): Promise<readonly ModerationVerdict[]>;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* In-process hand-off between the send path and the {@link BatchModerationWorker}. Enqueueing
|
|
406
|
+
* (via {@link postSendHook}) is a microtask-cheap array push, so wiring it into
|
|
407
|
+
* `DotwireHostConfig.postSend` adds no meaningful latency to a send. The verdict itself is
|
|
408
|
+
* computed later, in batches, by the worker.
|
|
409
|
+
*
|
|
410
|
+
* Bounded at `capacity`: once full, {@link enqueue} (and therefore `postSendHook`) waits for
|
|
411
|
+
* room rather than silently dropping - the queue applies back-pressure to sends, mirroring the
|
|
412
|
+
* .NET port's `BoundedChannelFullMode.Wait` default.
|
|
413
|
+
*/
|
|
414
|
+
declare class ModerationQueue {
|
|
415
|
+
private readonly items;
|
|
416
|
+
private readonly waitingReaders;
|
|
417
|
+
private readonly waitingWriters;
|
|
418
|
+
private completed;
|
|
419
|
+
private readonly capacity;
|
|
420
|
+
constructor(capacity?: number);
|
|
421
|
+
enqueue(item: PendingModeration, signal?: AbortSignal): Promise<void>;
|
|
422
|
+
/** Assign directly: `client.postSend = queue.postSendHook`. */
|
|
423
|
+
postSendHook: (ctx: PostSendContext) => Promise<void>;
|
|
424
|
+
/** Signals the worker to drain what is queued and stop. */
|
|
425
|
+
complete(): void;
|
|
426
|
+
/** Blocks for an item; resolves null once completed and drained, or if `signal` fires. */
|
|
427
|
+
dequeue(signal?: AbortSignal): Promise<PendingModeration | null>;
|
|
428
|
+
/** Waits up to `timeoutMs` for an item. Resolves 'timeout' on expiry, null on completion/abort. */
|
|
429
|
+
tryDequeue(timeoutMs: number, signal?: AbortSignal): Promise<PendingModeration | null | 'timeout'>;
|
|
430
|
+
private take;
|
|
431
|
+
private waitForWriteSlot;
|
|
432
|
+
private wake;
|
|
433
|
+
}
|
|
434
|
+
interface BatchModerationOptions {
|
|
435
|
+
/** Flush when this many messages are queued... Default 100. */
|
|
436
|
+
maxBatchSize?: number;
|
|
437
|
+
/** ...or when the oldest queued message has waited this long, whichever comes first. Default 30000. */
|
|
438
|
+
maxBatchDelayMs?: number;
|
|
439
|
+
/** Classifier attempts per batch (exponential backoff between them) before the batch is dropped. Default 3. */
|
|
440
|
+
maxClassifierAttempts?: number;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Drains a {@link ModerationQueue} in size-or-time bounded batches, hands each batch to the
|
|
444
|
+
* {@link ModerationClassifier}, and runs `onFlagged` for every flagged verdict (typically
|
|
445
|
+
* {@link BatchModerationWorker.redactWith}). Runs entirely off the send path.
|
|
446
|
+
*/
|
|
447
|
+
declare class BatchModerationWorker {
|
|
448
|
+
private readonly queue;
|
|
449
|
+
private readonly classifier;
|
|
450
|
+
private readonly onFlagged;
|
|
451
|
+
private readonly onError?;
|
|
452
|
+
private readonly maxBatchSize;
|
|
453
|
+
private readonly maxBatchDelayMs;
|
|
454
|
+
private readonly maxClassifierAttempts;
|
|
455
|
+
constructor(queue: ModerationQueue, classifier: ModerationClassifier, onFlagged: (verdict: ModerationVerdict, signal?: AbortSignal) => Promise<void> | void, options?: BatchModerationOptions, onError?: (err: unknown) => void);
|
|
456
|
+
/** The usual action: physically redact the flagged message through the admin API, honouring `Retry-After` on 429. */
|
|
457
|
+
static redactWith(host: DotwireHostClient): (verdict: ModerationVerdict) => Promise<void>;
|
|
458
|
+
/** Runs until `signal` fires or the queue is completed and drained. */
|
|
459
|
+
run(signal: AbortSignal): Promise<void>;
|
|
460
|
+
private fillBatch;
|
|
461
|
+
private processBatch;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export { type AddRoomMembersOptions, type AuditCheckpointEntry, type AuditCheckpointsResponse, type AuditEntry, type AuditPageResponse, type AuditVerificationResponse, type BatchModerationOptions, BatchModerationWorker, type ClientSendResult, DotwireApiError, type DotwireApiErrorInit, type DotwireApiErrorKind, DotwireHostClient, type DotwireHostConfig, DotwireHostRoom, type DotwireRole, DotwireTokenSigner, type GetMessagesOptions, type HistoryMessage, type MintTokenOptions, type ModerationClassifier, ModerationQueue, type ModerationVerdict, type PendingModeration, type PostSendContext, PostSendError, type PresendContext, PresendRejectedError, PresendResult, type RedactMessageResponse, type RetentionResponse, type RoomEvent, type RoomHistoryResponse, type RoomMembersResponse, type SendMessageResult, type SendSystemMessageOptions, type StreamRoomEventsOptions, type TokenResponseOptions, type UserExport, type UserExportAuditEvent, type UserExportMessage, type UserRoleResponse, type UserRoomsResponse, type WebhookResult, mapErrorKind };
|