@usebridger/transport-web 0.0.0-stage → 0.2.0
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 +202 -0
- package/dist/client.d.mts +51 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +161 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +45 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +709 -0
- package/dist/index.mjs.map +1 -0
- package/dist/protocol-D7nZkQui.d.mts +108 -0
- package/dist/protocol-D7nZkQui.d.mts.map +1 -0
- package/dist/protocol-a9D3CajG.mjs +25 -0
- package/dist/protocol-a9D3CajG.mjs.map +1 -0
- package/package.json +42 -3
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/jwt.ts","../src/media-store.ts","../src/outbox.ts","../src/transport.ts","../src/index.ts"],"sourcesContent":["// Verificação do JWT do sistema do dono (ADR 0077). O `jose` cuida de assinatura, algoritmo, JWKS\n// e prazos; aqui fica só a escolha da chave e o que o transport exige do payload.\n\nimport type { JsonValue } from '@usebridger/core';\nimport { createRemoteJWKSet, type JWTPayload, type JWTVerifyGetKey, jwtVerify } from 'jose';\n\ninterface AuthCommon {\n /** `iss` esperado. Ausente: não confere. */\n readonly issuer?: string;\n /** `aud` esperado. Ausente: não confere. */\n readonly audience?: string;\n}\n\n/** Como validar o token: segredo compartilhado (`HS256`) ou JWKS do sistema do dono. */\nexport type WebAuth =\n | (AuthCommon & { readonly secret: string; readonly jwksUrl?: never })\n | (AuthCommon & { readonly jwksUrl: string; readonly secret?: never });\n\n/** Token aceito: o `sub`, o prazo e o payload inteiro, que vira `Contact.claims`. */\nexport interface VerifiedToken {\n readonly sub: string;\n /** `exp` em epoch de milissegundos. */\n readonly expiresAt: number;\n readonly claims: Readonly<Record<string, JsonValue>>;\n}\n\nexport type TokenVerifier = (token: string) => Promise<VerifiedToken>;\n\n/** Lançado para qualquer token recusado; a mensagem é do `jose` ou do transport. */\nexport class InvalidTokenError extends Error {\n override readonly name = 'InvalidTokenError';\n}\n\n// Lista fechada por tipo de chave: sem ela, um token `HS256` assinado com a chave pública do JWKS\n// passaria (confusão de algoritmo).\nconst SECRET_ALGORITHMS = ['HS256'];\nconst JWKS_ALGORITHMS = ['RS256', 'ES256'];\n\n/** Monta o verificador. Valida a config na hora: a fábrica a chama, e o erro vira `BotConfigError`. */\nexport function createVerifier(auth: WebAuth): TokenVerifier {\n let key: Uint8Array | JWTVerifyGetKey;\n let algorithms: string[];\n if (typeof auth.secret === 'string') {\n // Abaixo de 32 bytes, o HMAC-SHA256 fica abaixo da força do próprio hash (RFC 7518 §3.2).\n if (Buffer.byteLength(auth.secret) < 32) {\n throw new TypeError('web: auth.secret precisa de pelo menos 32 bytes');\n }\n key = new TextEncoder().encode(auth.secret);\n algorithms = SECRET_ALGORITHMS;\n } else if (typeof auth.jwksUrl === 'string') {\n // O `jose` busca o JWKS na primeira verificação, cacheia e busca de novo para um `kid` novo.\n key = createRemoteJWKSet(new URL(auth.jwksUrl));\n algorithms = JWKS_ALGORITHMS;\n } else {\n throw new TypeError('web: auth precisa de `secret` ou de `jwksUrl`');\n }\n const options = {\n algorithms,\n requiredClaims: ['exp', 'sub'],\n ...(auth.issuer === undefined ? null : { issuer: auth.issuer }),\n ...(auth.audience === undefined ? null : { audience: auth.audience }),\n };\n\n return async (token) => {\n let payload: JWTPayload;\n try {\n // As duas sobrecargas do `jose` (chave fixa e função de chave) não unem num só tipo.\n ({ payload } =\n key instanceof Uint8Array\n ? await jwtVerify(token, key, options)\n : await jwtVerify(token, key, options));\n } catch (error) {\n throw new InvalidTokenError(error instanceof Error ? error.message : String(error), {\n cause: error,\n });\n }\n // `requiredClaims` garante a presença; o tipo, não.\n if (typeof payload.sub !== 'string' || payload.sub === '' || typeof payload.exp !== 'number') {\n throw new InvalidTokenError('token sem `sub` em texto ou sem `exp` numérico');\n }\n return {\n sub: payload.sub,\n expiresAt: payload.exp * 1000,\n claims: payload as Readonly<Record<string, JsonValue>>,\n };\n };\n}\n\n/** Token do header `Authorization: Bearer <jwt>`, ou `null`. */\nexport function bearerToken(request: Request): string | null {\n const header = request.headers.get('authorization');\n const match = header === null ? null : /^Bearer\\s+(\\S+)$/i.exec(header);\n return match?.[1] ?? null;\n}\n","// Arquivos em memória do transport (ADR 0077): os uploads do usuário e a mídia que o bot envia. A\n// validade é conferida quando o mapa anda, sem timer: nada sobrevive ao `stop()` por causa dele.\n\nimport { randomBytes } from 'node:crypto';\n\nexport interface StoredMedia {\n readonly bytes: Buffer;\n readonly mimetype: string;\n readonly fileName?: string;\n /** Contato que fez o upload; ausente na mídia do bot, que qualquer um com a URL baixa. */\n readonly owner?: string;\n}\n\ninterface Entry extends StoredMedia {\n readonly expiresAt: number;\n}\n\nexport const MEDIA_TTL_MS: number = 60 * 60 * 1000;\n/** Teto de todos os arquivos juntos; acima dele, sai o mais antigo. */\nexport const MEDIA_STORE_MAX_BYTES: number = 256 * 1024 * 1024;\n\nexport class MediaStore {\n // Ordem de inserção = ordem de validade, porque o TTL é um só: o mais antigo está na frente.\n readonly #entries = new Map<string, Entry>();\n #bytes = 0;\n readonly #now: () => number;\n readonly #maxBytes: number;\n\n constructor(options: { readonly now?: () => number; readonly maxBytes?: number } = {}) {\n this.#now = options.now ?? Date.now;\n this.#maxBytes = options.maxBytes ?? MEDIA_STORE_MAX_BYTES;\n }\n\n /** Guarda e devolve um ID aleatório de 128 bits, que serve de URL de download. */\n put(media: StoredMedia): string {\n this.#prune();\n const id = randomBytes(16).toString('base64url');\n this.#entries.set(id, { ...media, expiresAt: this.#now() + MEDIA_TTL_MS });\n this.#bytes += media.bytes.length;\n for (const [oldest, entry] of this.#entries) {\n if (this.#bytes <= this.#maxBytes || oldest === id) break;\n this.#delete(oldest, entry);\n }\n return id;\n }\n\n /** O arquivo, se ainda vale. */\n get(id: string): StoredMedia | undefined {\n const entry = this.#entries.get(id);\n if (entry === undefined) return undefined;\n if (entry.expiresAt <= this.#now()) {\n this.#delete(id, entry);\n return undefined;\n }\n return entry;\n }\n\n /**\n * Tira os uploads de `owner` do mapa, todos ou nenhum: a mensagem passa a segurar os bytes, e o\n * mesmo upload não entra em duas mensagens. `undefined` se algum não existe, venceu ou é de\n * outro contato.\n */\n takeAll(ids: readonly string[], owner: string): StoredMedia[] | undefined {\n const now = this.#now();\n const found: Entry[] = [];\n for (const id of ids) {\n const entry = this.#entries.get(id);\n if (entry === undefined || entry.owner !== owner || entry.expiresAt <= now) return undefined;\n found.push(entry);\n }\n // O mesmo ID duas vezes acharia a mesma entrada duas vezes; apagar de novo é inofensivo.\n for (const [index, id] of ids.entries()) {\n if (this.#entries.delete(id)) this.#bytes -= (found[index] as Entry).bytes.length;\n }\n return found;\n }\n\n clear(): void {\n this.#entries.clear();\n this.#bytes = 0;\n }\n\n get size(): number {\n return this.#entries.size;\n }\n\n #prune(): void {\n const now = this.#now();\n for (const [id, entry] of this.#entries) {\n if (entry.expiresAt > now) break;\n this.#delete(id, entry);\n }\n }\n\n #delete(id: string, entry: Entry): void {\n this.#entries.delete(id);\n this.#bytes -= entry.bytes.length;\n }\n}\n","// Buffer de envio para a conversa sem conexão aberta (ADR 0077): até 100 frames por conversa, por\n// 1 h, entregues em ordem no próximo `auth`. Sem timer: a validade é conferida quando o buffer anda.\n\nimport type { ServerFrame } from './protocol.ts';\n\nexport const OUTBOX_MAX_FRAMES = 100;\nexport const OUTBOX_TTL_MS: number = 60 * 60 * 1000;\n\ninterface Pending {\n readonly frame: ServerFrame;\n readonly expiresAt: number;\n}\n\nexport class Outbox {\n // Reinserida a cada frame novo: a conversa parada há mais tempo fica na frente do mapa.\n readonly #chats = new Map<string, Pending[]>();\n readonly #now: () => number;\n\n constructor(now: () => number = Date.now) {\n this.#now = now;\n }\n\n push(chatId: string, frame: ServerFrame): void {\n const now = this.#now();\n this.#prune(now);\n const pending = this.#chats.get(chatId) ?? [];\n this.#chats.delete(chatId);\n pending.push({ frame, expiresAt: now + OUTBOX_TTL_MS });\n if (pending.length > OUTBOX_MAX_FRAMES) pending.shift();\n this.#chats.set(chatId, pending);\n }\n\n /** Tira e devolve o que ainda vale para a conversa, em ordem. */\n drain(chatId: string): ServerFrame[] {\n const pending = this.#chats.get(chatId);\n if (pending === undefined) return [];\n this.#chats.delete(chatId);\n const now = this.#now();\n return pending.filter((item) => item.expiresAt > now).map((item) => item.frame);\n }\n\n clear(): void {\n this.#chats.clear();\n }\n\n get size(): number {\n return this.#chats.size;\n }\n\n /** Solta as conversas cujo frame mais novo já venceu: todos os outros venceram antes. */\n #prune(now: number): void {\n for (const [chatId, pending] of this.#chats) {\n const newest = pending.at(-1);\n if (newest !== undefined && newest.expiresAt > now) break;\n this.#chats.delete(chatId);\n }\n }\n}\n","// Transport do chat web (ADR 0077). O sistema do dono entrega ao widget um JWT; o widget abre o\n// WebSocket do transport, se autentica no primeiro frame e conversa por frames JSON. Mídia vai por\n// HTTP nas rotas do próprio transport. Nada aqui guarda o token.\n\nimport { randomUUID } from 'node:crypto';\nimport type {\n Capability,\n Chat,\n Contact,\n FormattedText,\n GroupMetadata,\n GroupParticipantAction,\n HttpRoutes,\n HttpSocket,\n HttpSocketHandlers,\n Logger,\n MediaInput,\n MessageKey,\n OutgoingContent,\n SendOptions,\n TypingKind,\n} from '@usebridger/core';\nimport { UnsupportedError } from '@usebridger/core';\nimport {\n assertCanSend,\n createMessage,\n groupActionCapability,\n type MediaSource,\n type Transport,\n type TransportEventHandler,\n type TransportEventName,\n type TransportEvents,\n TypedEmitter,\n} from '@usebridger/core/adapter';\nimport { bearerToken, InvalidTokenError, type TokenVerifier, type WebAuth } from './jwt.ts';\nimport { MediaStore, type StoredMedia } from './media-store.ts';\nimport { Outbox } from './outbox.ts';\nimport {\n AUTH_TIMEOUT_MS,\n CLOSE_FORBIDDEN,\n CLOSE_GOING_AWAY,\n CLOSE_INVALID_AUTH,\n CLOSE_TOO_BIG,\n CLOSE_UNAUTHORIZED,\n CONVERSATION_PATTERN,\n DEFAULT_CONVERSATION,\n type ErrorFrame,\n MAX_ATTACHMENTS,\n MAX_FRAME_BYTES,\n type MediaRef,\n type ServerFrame,\n type ServerMessageFrame,\n} from './protocol.ts';\n\nexport interface WebOptions {\n /** Como validar o JWT do sistema do dono: `{ secret }` (HS256) ou `{ jwksUrl }`. */\n readonly auth: WebAuth;\n /**\n * Claim do JWT com o cliente do dono (empresa, escola), que vira `chat.tenantId` (ADR 0072).\n * Configurado, o token sem ele é recusado. Ausente: o transport atende um cliente só.\n */\n readonly tenantClaim?: string;\n /**\n * Origens aceitas no WebSocket e no upload (`https://erp.exemplo.com`). Ausente: qualquer uma,\n * porque quem autoriza é o token. Pedido sem `Origin` (fora do navegador) sempre passa.\n */\n readonly origins?: readonly string[];\n readonly media?: {\n /** Teto de um upload, em bytes. Padrão: 10 MiB. */\n readonly maxBytes?: number;\n };\n}\n\nexport const DEFAULT_MEDIA_MAX_BYTES: number = 10 * 1024 * 1024;\n\nconst CAPABILITIES: readonly Capability[] = [\n 'send.text',\n 'send.image',\n 'send.video',\n 'send.audio',\n 'send.document',\n 'media.download',\n 'actions',\n 'quoted',\n 'typing',\n 'message.edit',\n 'message.delete',\n];\n\nconst SELF: Contact = { id: 'bot', name: null, phone: null, isBot: true };\n\n/** Maior atraso que o `setTimeout` aceita; acima dele, o timer dispara na hora. */\nconst MAX_TIMER_MS = 2 ** 31 - 1;\n\n/** Usuário autenticado numa conexão: o que sobra do token depois da verificação. */\ninterface Identity {\n readonly contact: Contact;\n readonly tenantId?: string;\n readonly expiresAt: number;\n}\n\ninterface Connection {\n socket: HttpSocket | null;\n /** Ausente até o `auth` ser aceito. */\n chat: Chat | null;\n sender: Contact | null;\n authenticating: boolean;\n timer: NodeJS.Timeout | undefined;\n closed: boolean;\n}\n\n/** Token recusado por falta do claim de tenant: válido, mas sem lugar neste transport. */\nclass ForbiddenError extends Error {\n override readonly name = 'ForbiddenError';\n}\n\nexport class WebTransport implements Transport {\n readonly name = 'web';\n readonly capabilities: ReadonlySet<Capability> = new Set(CAPABILITIES);\n readonly native: unknown = null;\n\n readonly #options: WebOptions;\n readonly #verify: TokenVerifier;\n readonly #routes: HttpRoutes;\n readonly #log: Logger;\n readonly #events: TypedEmitter<TransportEvents>;\n readonly #maxBytes: number;\n readonly #connections = new Set<Connection>();\n /** Conexões autenticadas por conversa; várias abas na mesma conversa recebem os mesmos frames. */\n readonly #chats = new Map<string, Set<Connection>>();\n readonly #outbox = new Outbox();\n readonly #media = new MediaStore();\n #connected = false;\n #self: Contact | null = null;\n\n constructor(options: WebOptions, verify: TokenVerifier, routes: HttpRoutes, log: Logger) {\n this.#options = options;\n this.#verify = verify;\n this.#routes = routes;\n this.#log = log;\n this.#maxBytes = options.media?.maxBytes ?? DEFAULT_MEDIA_MAX_BYTES;\n this.#events = new TypedEmitter<TransportEvents>((error, event) =>\n this.#log.error('handler de evento do transport falhou', { err: error, event }),\n );\n // Na fábrica (ADR 0076): a porta abre antes do `connect()`, e as rotas respondem 503 até lá.\n routes.ws('/chat', (request) => this.#accept(request));\n routes.route('POST', '/media', (request) => this.#upload(request));\n routes.route('OPTIONS', '/media', (request) => this.#preflight(request));\n routes.route('GET', '/media/:id', (request, { params }) =>\n this.#download(request, params['id'] ?? ''),\n );\n }\n\n get self(): Contact | null {\n return this.#self;\n }\n\n async connect(): Promise<void> {\n this.#connected = true;\n this.#self = SELF;\n // A porta já está aberta (ADR 0076): dá para receber e enviar a partir de agora.\n this.#events.emit('connection.status', { status: 'open' });\n }\n\n async disconnect(): Promise<void> {\n this.#connected = false;\n for (const connection of this.#connections) {\n clearTimeout(connection.timer);\n connection.closed = true;\n connection.socket?.close(CLOSE_GOING_AWAY, 'transport desconectado');\n }\n this.#connections.clear();\n this.#chats.clear();\n this.#outbox.clear();\n this.#media.clear();\n }\n\n on<E extends TransportEventName>(event: E, handler: TransportEventHandler<E>): () => void {\n return this.#events.on(event, handler);\n }\n\n async send(chatId: string, content: OutgoingContent, options?: SendOptions): Promise<MessageKey> {\n this.#assertConnected();\n assertCanSend(this, content, options);\n const frame = this.#messageFrame(content, options);\n this.#deliver(chatId, frame);\n return { chatId, id: frame.id, fromMe: true, senderId: null };\n }\n\n async react(): Promise<void> {\n throw new UnsupportedError('reactions', this.name);\n }\n\n async edit(key: MessageKey, text: string, formatted?: FormattedText): Promise<void> {\n this.#assertConnected();\n this.#deliver(key.chatId, {\n type: 'edit',\n id: key.id,\n text,\n ...(formatted === undefined ? null : { formatted }),\n });\n }\n\n async delete(key: MessageKey): Promise<void> {\n this.#assertConnected();\n this.#deliver(key.chatId, { type: 'delete', id: key.id });\n }\n\n async sendTyping(chatId: string, kind: TypingKind): Promise<void> {\n this.#assertConnected();\n // Só para quem está olhando: \"digitando\" atrasado não diz nada.\n for (const connection of this.#chats.get(chatId) ?? []) {\n this.#write(connection, { type: 'typing', kind });\n }\n }\n\n async getGroupMetadata(): Promise<GroupMetadata> {\n throw new UnsupportedError('groups', this.name);\n }\n\n async updateGroupParticipants(\n _groupId: string,\n _participantIds: readonly string[],\n action: GroupParticipantAction,\n ): Promise<void> {\n throw new UnsupportedError(groupActionCapability(action), this.name);\n }\n\n // --- WebSocket ---\n\n #accept(request: Request): HttpSocketHandlers | Response {\n if (!this.#connected) return new Response('transport desconectado', { status: 503 });\n if (!this.#originAllowed(request)) return new Response('origem recusada', { status: 403 });\n const connection: Connection = {\n socket: null,\n chat: null,\n sender: null,\n authenticating: false,\n timer: undefined,\n closed: false,\n };\n return {\n onOpen: (socket) => {\n connection.socket = socket;\n this.#connections.add(connection);\n connection.timer = setTimeout(\n () => this.#close(connection, CLOSE_UNAUTHORIZED, 'auth não chegou'),\n AUTH_TIMEOUT_MS,\n );\n },\n onMessage: (_socket, data) => this.#onFrame(connection, data),\n onClose: () => this.#forget(connection),\n };\n }\n\n async #onFrame(connection: Connection, data: string | ArrayBuffer): Promise<void> {\n if (connection.closed) return;\n const size = typeof data === 'string' ? Buffer.byteLength(data) : data.byteLength;\n if (size > MAX_FRAME_BYTES) {\n this.#close(connection, CLOSE_TOO_BIG, 'frame grande demais');\n return;\n }\n const frame = typeof data === 'string' ? parseJson(data) : undefined;\n if (connection.chat === null) {\n await this.#authenticate(connection, frame);\n return;\n }\n if (!isRecord(frame)) {\n this.#reject(connection, 'invalid-frame', 'frame não é um objeto JSON');\n return;\n }\n if (frame['type'] === 'message') this.#onMessage(connection, frame);\n else if (frame['type'] === 'action') this.#onAction(connection, frame);\n else this.#reject(connection, 'invalid-frame', 'tipo de frame desconhecido');\n }\n\n async #authenticate(connection: Connection, frame: unknown): Promise<void> {\n // Um frame só antes do `ready`: o cliente espera a resposta do `auth` antes de falar.\n if (connection.authenticating || !isRecord(frame) || frame['type'] !== 'auth') {\n this.#close(connection, CLOSE_INVALID_AUTH, 'o primeiro frame deve ser `auth`');\n return;\n }\n const { token, conversation = DEFAULT_CONVERSATION } = frame;\n if (\n typeof token !== 'string' ||\n typeof conversation !== 'string' ||\n !CONVERSATION_PATTERN.test(conversation)\n ) {\n this.#close(connection, CLOSE_INVALID_AUTH, '`auth` com token ou conversa inválidos');\n return;\n }\n connection.authenticating = true;\n let identity: Identity;\n try {\n identity = await this.#identify(token);\n } catch (error) {\n this.#refuse(connection, error);\n return;\n }\n // O cliente pode ter saído, ou o transport desconectado, durante a verificação.\n if (connection.closed) return;\n const chatId = `${identity.contact.id}/${conversation}`;\n connection.chat = {\n id: chatId,\n isGroup: false,\n kind: 'dm',\n ...(identity.tenantId === undefined ? null : { tenantId: identity.tenantId }),\n };\n connection.sender = identity.contact;\n connection.authenticating = false;\n clearTimeout(connection.timer);\n this.#expireAt(connection, identity.expiresAt);\n let peers = this.#chats.get(chatId);\n if (peers === undefined) {\n peers = new Set();\n this.#chats.set(chatId, peers);\n }\n peers.add(connection);\n this.#write(connection, { type: 'ready', chatId, userId: identity.contact.id });\n for (const pending of this.#outbox.drain(chatId)) this.#write(connection, pending);\n }\n\n #refuse(connection: Connection, error: unknown): void {\n if (error instanceof ForbiddenError) {\n this.#log.debug('conexão recusada sem o claim de tenant', { reason: error.message });\n this.#close(connection, CLOSE_FORBIDDEN, error.message);\n return;\n }\n if (error instanceof InvalidTokenError) {\n // Debug: token vencido é rotina (aba aberta de ontem), não incidente.\n this.#log.debug('token recusado', { reason: error.message });\n this.#close(connection, CLOSE_UNAUTHORIZED, 'token inválido');\n return;\n }\n // Falha na busca do JWKS, por exemplo: o cliente tenta de novo, e o log diz o que houve.\n this.#log.warn('não deu para verificar o token', { err: error });\n this.#close(connection, CLOSE_UNAUTHORIZED, 'não deu para verificar o token');\n }\n\n /** Fecha a conexão no `exp`; o cliente reconecta com um token novo (ADR 0077). */\n #expireAt(connection: Connection, expiresAt: number): void {\n const remaining = expiresAt - Date.now();\n if (remaining <= 0) {\n this.#close(connection, CLOSE_UNAUTHORIZED, 'token venceu');\n return;\n }\n connection.timer = setTimeout(\n () => this.#expireAt(connection, expiresAt),\n Math.min(remaining, MAX_TIMER_MS),\n );\n }\n\n #onMessage(connection: Connection, frame: Record<string, unknown>): void {\n const { ref, text, attachments = [] } = frame;\n const chat = connection.chat as Chat;\n const sender = connection.sender as Contact;\n if (ref !== undefined && (typeof ref !== 'string' || ref.length > 128)) {\n this.#reject(connection, 'invalid-frame', '`ref` deve ser texto de até 128 caracteres');\n return;\n }\n if (\n (text !== undefined && typeof text !== 'string') ||\n !Array.isArray(attachments) ||\n attachments.length > MAX_ATTACHMENTS ||\n !attachments.every((id) => typeof id === 'string') ||\n ((text ?? '') === '' && attachments.length === 0)\n ) {\n this.#reject(\n connection,\n 'invalid-frame',\n `mensagem precisa de \\`text\\` ou de até ${MAX_ATTACHMENTS} \\`attachments\\``,\n ref,\n );\n return;\n }\n const media = this.#media.takeAll(attachments as string[], sender.id);\n if (media === undefined) {\n this.#reject(\n connection,\n 'media-not-found',\n 'anexo inexistente, vencido ou de outro usuário',\n ref,\n );\n return;\n }\n const id = randomUUID();\n this.#write(connection, { type: 'ack', ref: ref ?? null, id });\n const base = { id, chat, sender, timestamp: Date.now(), fromMe: false };\n const sources = media.map(toSource);\n const first = sources[0];\n const message =\n first === undefined\n ? createMessage({ ...base, type: 'text', text: text as string })\n : createMessage({\n ...base,\n ...mediaType(first.mimetype, first.fileName),\n text: (text as string | undefined) || null,\n media: first,\n attachments: sources,\n });\n this.#events.emit('message', message);\n }\n\n #onAction(connection: Connection, frame: Record<string, unknown>): void {\n const { actionId } = frame;\n if (typeof actionId !== 'string' || actionId === '' || actionId.length > 64) {\n this.#reject(connection, 'invalid-frame', '`actionId` deve ser texto de até 64 caracteres');\n return;\n }\n // O kernel confere se o ID é de um botão enviado a este chat (ADR 0062): forjar não adianta.\n this.#events.emit('interaction', {\n id: randomUUID(),\n chat: connection.chat as Chat,\n sender: connection.sender as Contact,\n timestamp: Date.now(),\n actionId,\n });\n }\n\n #forget(connection: Connection): void {\n clearTimeout(connection.timer);\n connection.closed = true;\n this.#connections.delete(connection);\n const chatId = connection.chat?.id;\n if (chatId === undefined) return;\n const peers = this.#chats.get(chatId);\n peers?.delete(connection);\n if (peers?.size === 0) this.#chats.delete(chatId);\n }\n\n #close(connection: Connection, code: number, reason: string): void {\n if (connection.closed) return;\n // `#forget` sai pelo `onClose`; fechar aqui já tira a conexão de quem recebe frames.\n this.#forget(connection);\n connection.socket?.close(code, reason);\n }\n\n #reject(connection: Connection, code: ErrorFrame['code'], message: string, ref?: unknown): void {\n this.#write(connection, {\n type: 'error',\n code,\n message,\n ...(typeof ref === 'string' ? { ref } : null),\n });\n }\n\n #write(connection: Connection, frame: ServerFrame): void {\n connection.socket?.send(JSON.stringify(frame));\n }\n\n /** Para quem está na conversa, ou para o buffer se ninguém estiver. */\n #deliver(chatId: string, frame: ServerFrame): void {\n const peers = this.#chats.get(chatId);\n if (peers === undefined) {\n this.#outbox.push(chatId, frame);\n return;\n }\n for (const connection of peers) this.#write(connection, frame);\n }\n\n #messageFrame(content: OutgoingContent, options: SendOptions | undefined): ServerMessageFrame {\n const common = {\n type: 'message',\n id: randomUUID(),\n timestamp: Date.now(),\n ...(options?.quoted === undefined ? null : { quotedId: options.quoted.id }),\n } as const;\n switch (content.type) {\n case 'text':\n return {\n ...common,\n kind: 'text',\n text: content.text,\n ...(content.formatted === undefined ? null : { formatted: content.formatted }),\n ...(options?.actions === undefined || options.actions.length === 0\n ? null\n : { actions: options.actions.map(({ id, label }) => ({ id, label })) }),\n };\n case 'image':\n case 'video':\n case 'audio':\n case 'document': {\n const caption = 'caption' in content ? content.caption : undefined;\n const formatted = 'formattedCaption' in content ? content.formattedCaption : undefined;\n return {\n ...common,\n kind: content.type,\n media: this.#mediaRef(content),\n ...(caption === undefined ? null : { text: caption }),\n ...(formatted === undefined ? null : { formatted }),\n };\n }\n default:\n // `assertCanSend` já barrou o que o web não declara; o álbum, a fila divide (ADR 0065).\n throw new UnsupportedError('send.album', this.name);\n }\n }\n\n #mediaRef(content: {\n readonly media: MediaInput;\n readonly mimetype?: string;\n readonly fileName?: string;\n }): MediaRef {\n const named = content.fileName === undefined ? null : { fileName: content.fileName };\n const mimetype = content.mimetype ?? 'application/octet-stream';\n if (!Buffer.isBuffer(content.media)) return { url: content.media.url, mimetype, ...named };\n const id = this.#media.put({ bytes: content.media, mimetype, ...named });\n return { url: `${this.#routes.basePath}/media/${id}`, mimetype, ...named };\n }\n\n // --- HTTP de mídia ---\n\n async #upload(request: Request): Promise<Response> {\n if (!this.#connected) return new Response('transport desconectado', { status: 503 });\n const cors = this.#cors(request);\n if (cors === null) return new Response('origem recusada', { status: 403 });\n const token = bearerToken(request);\n if (token === null) return text('falta o header Authorization: Bearer', 401, cors);\n let identity: Identity;\n try {\n identity = await this.#identify(token);\n } catch (error) {\n if (error instanceof ForbiddenError) return text(error.message, 403, cors);\n if (error instanceof InvalidTokenError) return text('token inválido', 401, cors);\n this.#log.warn('não deu para verificar o token do upload', { err: error });\n return text('não deu para verificar o token', 503, cors);\n }\n const mimetype = request.headers.get('content-type')?.split(';')[0]?.trim();\n if (!mimetype) return text('falta o Content-Type', 415, cors);\n const declared = Number(request.headers.get('content-length') ?? 0);\n if (declared > this.#maxBytes) return text('arquivo grande demais', 413, cors);\n const bytes = await readLimited(request, this.#maxBytes);\n if (bytes === null) return text('arquivo grande demais', 413, cors);\n if (bytes.length === 0) return text('arquivo vazio', 400, cors);\n const name = new URL(request.url).searchParams.get('name');\n const id = this.#media.put({\n bytes,\n mimetype,\n owner: identity.contact.id,\n ...(name ? { fileName: name.slice(0, 255) } : null),\n });\n return Response.json({ id }, { status: 201, headers: cors });\n }\n\n #preflight(request: Request): Response {\n const cors = this.#cors(request);\n if (cors === null) return new Response('origem recusada', { status: 403 });\n return new Response(null, {\n status: 204,\n headers: {\n ...cors,\n 'access-control-allow-methods': 'POST',\n 'access-control-allow-headers': 'authorization, content-type',\n 'access-control-max-age': '600',\n },\n });\n }\n\n #download(request: Request, id: string): Response {\n const cors = this.#cors(request) ?? {};\n const media = this.#media.get(id);\n // Upload do usuário não sai por aqui: só a mídia do bot tem URL pública.\n if (media === undefined || media.owner !== undefined) return text('não encontrado', 404, cors);\n const inline = /^(image|video|audio)\\//.test(media.mimetype);\n const name =\n media.fileName === undefined\n ? ''\n : `; filename*=UTF-8''${encodeURIComponent(media.fileName)}`;\n return new Response(media.bytes, {\n headers: {\n ...cors,\n 'content-type': media.mimetype,\n 'content-disposition': `${inline ? 'inline' : 'attachment'}${name}`,\n // A mídia vem de plugin, que pode repassar o que um usuário mandou: sem sniff e sem script.\n 'x-content-type-options': 'nosniff',\n 'content-security-policy': 'sandbox',\n 'cache-control': 'private, max-age=3600',\n },\n });\n }\n\n // --- Comum ---\n\n async #identify(token: string): Promise<Identity> {\n const verified = await this.#verify(token);\n const claim = this.#options.tenantClaim;\n let tenantId: string | undefined;\n if (claim !== undefined) {\n const value = verified.claims[claim];\n if ((typeof value !== 'string' && typeof value !== 'number') || value === '') {\n throw new ForbiddenError(`token sem o claim de tenant \"${claim}\"`);\n }\n tenantId = String(value);\n }\n // O tenant entra no ID (ADR 0072): dois tenants nunca dividem um contato nem uma conversa.\n const sub = encodeURIComponent(verified.sub);\n const id = tenantId === undefined ? sub : `${encodeURIComponent(tenantId)}:${sub}`;\n const name = verified.claims['name'];\n return {\n contact: {\n id,\n name: typeof name === 'string' ? name : null,\n phone: null,\n claims: verified.claims,\n },\n ...(tenantId === undefined ? null : { tenantId }),\n expiresAt: verified.expiresAt,\n };\n }\n\n #originAllowed(request: Request): boolean {\n const origin = request.headers.get('origin');\n return (\n origin === null ||\n this.#options.origins === undefined ||\n this.#options.origins.includes(origin)\n );\n }\n\n /** Headers de CORS para a origem do pedido; `null` se ela não é aceita. */\n #cors(request: Request): Record<string, string> | null {\n if (!this.#originAllowed(request)) return null;\n const origin = request.headers.get('origin');\n if (origin === null) return {};\n // Sem lista, qualquer origem: o upload exige o token, e o download, a URL secreta.\n return this.#options.origins === undefined\n ? { 'access-control-allow-origin': '*' }\n : { 'access-control-allow-origin': origin, vary: 'Origin' };\n }\n\n #assertConnected(): void {\n if (!this.#connected) throw new Error('web: transport desconectado');\n }\n}\n\nfunction parseJson(data: string): unknown {\n try {\n return JSON.parse(data);\n } catch {\n // JSON inválido é tratado como frame inválido por quem chamou.\n return undefined;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction text(body: string, status: number, headers: Record<string, string>): Response {\n return new Response(body, { status, headers });\n}\n\n/** Corpo inteiro, ou `null` se passar de `max` (o `Content-Length` pode mentir ou faltar). */\nasync function readLimited(request: Request, max: number): Promise<Buffer | null> {\n if (request.body === null) return Buffer.alloc(0);\n const chunks: Uint8Array[] = [];\n let total = 0;\n for await (const chunk of request.body) {\n total += chunk.byteLength;\n if (total > max) return null;\n chunks.push(chunk);\n }\n return Buffer.concat(chunks);\n}\n\nfunction toSource(media: StoredMedia): MediaSource {\n return {\n mimetype: media.mimetype,\n size: media.bytes.length,\n ...(media.fileName === undefined ? null : { fileName: media.fileName }),\n download: async () => media.bytes,\n };\n}\n\n/** Tipo da mensagem pelo mimetype do primeiro anexo (ADR 0065). */\nfunction mediaType(\n mimetype: string,\n fileName: string | undefined,\n):\n | { readonly type: 'image' | 'video' | 'audio' }\n | { readonly type: 'document'; readonly fileName: string | null } {\n if (mimetype.startsWith('image/')) return { type: 'image' };\n if (mimetype.startsWith('video/')) return { type: 'video' };\n if (mimetype.startsWith('audio/')) return { type: 'audio' };\n return { type: 'document', fileName: fileName ?? null };\n}\n","// Entry `@usebridger/transport-web`: a fábrica que o app passa em `createBot({ transport })`\n// (ADR 0037, 0077). A classe fica interna: o transport precisa das rotas HTTP do bot.\n\nimport type { Transport, TransportDeps } from '@usebridger/core/adapter';\nimport { createVerifier } from './jwt.ts';\nimport { type WebOptions, WebTransport } from './transport.ts';\n\nexport type { WebAuth } from './jwt.ts';\nexport * from './protocol.ts';\nexport { DEFAULT_MEDIA_MAX_BYTES, type WebOptions } from './transport.ts';\n\nexport function web(options: WebOptions): (deps: TransportDeps) => Transport {\n // Config errada falha aqui, no app, antes de qualquer conexão.\n const verify = createVerifier(options.auth);\n const maxBytes = options.media?.maxBytes;\n if (maxBytes !== undefined && (!Number.isInteger(maxBytes) || maxBytes <= 0)) {\n throw new RangeError(`web: media.maxBytes deve ser inteiro positivo; recebido ${maxBytes}`);\n }\n if (options.tenantClaim === '') throw new TypeError('web: tenantClaim vazio');\n return (deps) => {\n // Na fábrica, o erro vira `BotConfigError` no `createBot`.\n if (deps.http === undefined) {\n throw new TypeError('web: o transport precisa de `http` no createBot (ver createHttp)');\n }\n return new WebTransport(options, verify, deps.http, deps.log);\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { FormattedText } from "@usebridger/core";
|
|
2
|
+
//#region src/protocol.d.ts
|
|
3
|
+
/** Fechamento: o primeiro frame não é um `auth` válido (ou chegou outro frame antes do `ready`). */
|
|
4
|
+
declare const CLOSE_INVALID_AUTH = 4400;
|
|
5
|
+
/** Fechamento: token ausente, inválido ou vencido (inclusive no meio da conexão). */
|
|
6
|
+
declare const CLOSE_UNAUTHORIZED = 4401;
|
|
7
|
+
/** Fechamento: o token é válido, mas falta o claim de tenant configurado. */
|
|
8
|
+
declare const CLOSE_FORBIDDEN = 4403;
|
|
9
|
+
/** Fechamento: frame acima de `MAX_FRAME_BYTES`. */
|
|
10
|
+
declare const CLOSE_TOO_BIG = 1009;
|
|
11
|
+
/** Fechamento: o transport desconectou (ou o bot parou). */
|
|
12
|
+
declare const CLOSE_GOING_AWAY = 1001;
|
|
13
|
+
/** Teto de um frame. Mídia vai por HTTP, então o frame só leva texto e IDs. */
|
|
14
|
+
declare const MAX_FRAME_BYTES: number;
|
|
15
|
+
/** Prazo para o primeiro frame (`auth`) depois de abrir a conexão. */
|
|
16
|
+
declare const AUTH_TIMEOUT_MS = 1e4;
|
|
17
|
+
/** Conversa quando o `auth` não informa. */
|
|
18
|
+
declare const DEFAULT_CONVERSATION = "default";
|
|
19
|
+
/** Formato do ID de conversa: entra no `chat.id` sem escape. */
|
|
20
|
+
declare const CONVERSATION_PATTERN: RegExp;
|
|
21
|
+
/** Máximo de anexos numa mensagem do cliente. */
|
|
22
|
+
declare const MAX_ATTACHMENTS = 10;
|
|
23
|
+
/** Primeiro frame da conexão. O token não volta em frame nenhum nem fica guardado. */
|
|
24
|
+
interface AuthFrame {
|
|
25
|
+
readonly type: "auth";
|
|
26
|
+
readonly token: string;
|
|
27
|
+
/** Conversa do usuário (`[A-Za-z0-9_-]`, até 64). Padrão: `default`. */
|
|
28
|
+
readonly conversation?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Mensagem do usuário. Precisa de `text` ou de `attachments`. */
|
|
31
|
+
interface ClientMessageFrame {
|
|
32
|
+
readonly type: "message";
|
|
33
|
+
/** ID do lado do cliente, devolvido no `ack` para casar com o ID do servidor. */
|
|
34
|
+
readonly ref?: string;
|
|
35
|
+
readonly text?: string;
|
|
36
|
+
/** IDs devolvidos pelo `POST <base>/media`, na ordem. */
|
|
37
|
+
readonly attachments?: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
/** Clique num botão: o `id` da ação, como veio em `actions`. */
|
|
40
|
+
interface ActionFrame {
|
|
41
|
+
readonly type: "action";
|
|
42
|
+
readonly actionId: string;
|
|
43
|
+
}
|
|
44
|
+
type ClientFrame = AuthFrame | ClientMessageFrame | ActionFrame;
|
|
45
|
+
/** Autenticado; o que estava esperando no buffer vem logo depois. */
|
|
46
|
+
interface ReadyFrame {
|
|
47
|
+
readonly type: "ready";
|
|
48
|
+
readonly chatId: string;
|
|
49
|
+
readonly userId: string;
|
|
50
|
+
}
|
|
51
|
+
/** Confirma uma mensagem do usuário com o ID que o servidor deu a ela. */
|
|
52
|
+
interface AckFrame {
|
|
53
|
+
readonly type: "ack";
|
|
54
|
+
readonly ref: string | null;
|
|
55
|
+
readonly id: string;
|
|
56
|
+
}
|
|
57
|
+
/** Mídia de uma mensagem do bot. `url` relativa é sob a origem do servidor. */
|
|
58
|
+
interface MediaRef {
|
|
59
|
+
readonly url: string;
|
|
60
|
+
readonly mimetype?: string;
|
|
61
|
+
readonly fileName?: string;
|
|
62
|
+
}
|
|
63
|
+
/** Mensagem do bot. */
|
|
64
|
+
interface ServerMessageFrame {
|
|
65
|
+
readonly type: "message";
|
|
66
|
+
readonly id: string;
|
|
67
|
+
/** Epoch em milissegundos. */
|
|
68
|
+
readonly timestamp: number;
|
|
69
|
+
readonly kind: "text" | "image" | "video" | "audio" | "document";
|
|
70
|
+
/** Texto, ou legenda da mídia (o texto visível, quando há `formatted`). */
|
|
71
|
+
readonly text?: string;
|
|
72
|
+
/** Árvore do texto formatado (ADR 0061), para o front renderizar. */
|
|
73
|
+
readonly formatted?: FormattedText;
|
|
74
|
+
readonly media?: MediaRef;
|
|
75
|
+
/** ID da mensagem citada. */
|
|
76
|
+
readonly quotedId?: string;
|
|
77
|
+
readonly actions?: readonly {
|
|
78
|
+
readonly id: string;
|
|
79
|
+
readonly label: string;
|
|
80
|
+
}[];
|
|
81
|
+
}
|
|
82
|
+
interface EditFrame {
|
|
83
|
+
readonly type: "edit";
|
|
84
|
+
readonly id: string;
|
|
85
|
+
readonly text: string;
|
|
86
|
+
readonly formatted?: FormattedText;
|
|
87
|
+
}
|
|
88
|
+
interface DeleteFrame {
|
|
89
|
+
readonly type: "delete";
|
|
90
|
+
readonly id: string;
|
|
91
|
+
}
|
|
92
|
+
/** "Digitando" (some no próximo frame `message` ou depois de alguns segundos, a critério do front). */
|
|
93
|
+
interface TypingFrame {
|
|
94
|
+
readonly type: "typing";
|
|
95
|
+
readonly kind: "text" | "voice";
|
|
96
|
+
}
|
|
97
|
+
/** Frame recusado; a conexão segue aberta. */
|
|
98
|
+
interface ErrorFrame {
|
|
99
|
+
readonly type: "error";
|
|
100
|
+
readonly code: "invalid-frame" | "media-not-found";
|
|
101
|
+
readonly message: string;
|
|
102
|
+
/** O `ref` da mensagem recusada, quando houver. */
|
|
103
|
+
readonly ref?: string;
|
|
104
|
+
}
|
|
105
|
+
type ServerFrame = ReadyFrame | AckFrame | ServerMessageFrame | EditFrame | DeleteFrame | TypingFrame | ErrorFrame;
|
|
106
|
+
//#endregion
|
|
107
|
+
export { TypingFrame as C, ServerMessageFrame as S, MAX_ATTACHMENTS as _, CLOSE_FORBIDDEN as a, ReadyFrame as b, CLOSE_TOO_BIG as c, ClientFrame as d, ClientMessageFrame as f, ErrorFrame as g, EditFrame as h, AuthFrame as i, CLOSE_UNAUTHORIZED as l, DeleteFrame as m, AckFrame as n, CLOSE_GOING_AWAY as o, DEFAULT_CONVERSATION as p, ActionFrame as r, CLOSE_INVALID_AUTH as s, AUTH_TIMEOUT_MS as t, CONVERSATION_PATTERN as u, MAX_FRAME_BYTES as v, ServerFrame as x, MediaRef as y };
|
|
108
|
+
//# sourceMappingURL=protocol-D7nZkQui.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-D7nZkQui.d.mts","names":[],"sources":["../src/protocol.ts"],"mappings":";;;cAOa;;cAEA;;cAEA;;cAEA;;cAEA;;cAGA;;cAEA;;cAEA;;cAEA,sBAAsB;;cAEtB;;UAKI;WACN;WACA;;WAEA;;;UAIM;WACN;;WAEA;WACA;;WAEA;;;UAIM;WACN;WACA;;KAGC,cAAc,YAAY,qBAAqB;;UAK1C;WACN;WACA;WACA;;;UAIM;WACN;WACA;WACA;;;UAIM;WACN;WACA;WACA;;;UAIM;WACN;WACA;;WAEA;WACA;;WAEA;;WAEA,YAAY;WACZ,QAAQ;;WAER;WACA;aAA8B;aAAqB;;;UAG7C;WACN;WACA;WACA;WACA,YAAY;;UAGN;WACN;WACA;;;UAIM;WACN;WACA;;;UAIM;WACN;WACA;WACA;;WAEA;;KAGC,cACR,aACA,WACA,qBACA,YACA,cACA,cACA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/protocol.ts
|
|
2
|
+
/** Fechamento: o primeiro frame não é um `auth` válido (ou chegou outro frame antes do `ready`). */
|
|
3
|
+
const CLOSE_INVALID_AUTH = 4400;
|
|
4
|
+
/** Fechamento: token ausente, inválido ou vencido (inclusive no meio da conexão). */
|
|
5
|
+
const CLOSE_UNAUTHORIZED = 4401;
|
|
6
|
+
/** Fechamento: o token é válido, mas falta o claim de tenant configurado. */
|
|
7
|
+
const CLOSE_FORBIDDEN = 4403;
|
|
8
|
+
/** Fechamento: frame acima de `MAX_FRAME_BYTES`. */
|
|
9
|
+
const CLOSE_TOO_BIG = 1009;
|
|
10
|
+
/** Fechamento: o transport desconectou (ou o bot parou). */
|
|
11
|
+
const CLOSE_GOING_AWAY = 1001;
|
|
12
|
+
/** Teto de um frame. Mídia vai por HTTP, então o frame só leva texto e IDs. */
|
|
13
|
+
const MAX_FRAME_BYTES = 65536;
|
|
14
|
+
/** Prazo para o primeiro frame (`auth`) depois de abrir a conexão. */
|
|
15
|
+
const AUTH_TIMEOUT_MS = 1e4;
|
|
16
|
+
/** Conversa quando o `auth` não informa. */
|
|
17
|
+
const DEFAULT_CONVERSATION = "default";
|
|
18
|
+
/** Formato do ID de conversa: entra no `chat.id` sem escape. */
|
|
19
|
+
const CONVERSATION_PATTERN = /^[\w-]{1,64}$/;
|
|
20
|
+
/** Máximo de anexos numa mensagem do cliente. */
|
|
21
|
+
const MAX_ATTACHMENTS = 10;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { CLOSE_TOO_BIG as a, DEFAULT_CONVERSATION as c, CLOSE_INVALID_AUTH as i, MAX_ATTACHMENTS as l, CLOSE_FORBIDDEN as n, CLOSE_UNAUTHORIZED as o, CLOSE_GOING_AWAY as r, CONVERSATION_PATTERN as s, AUTH_TIMEOUT_MS as t, MAX_FRAME_BYTES as u };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=protocol-a9D3CajG.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-a9D3CajG.mjs","names":[],"sources":["../src/protocol.ts"],"sourcesContent":["// Protocolo v1 do chat web (ADR 0077): frames JSON no WebSocket `GET <base>/chat`. Os tipos são\n// o contrato com o front; o cliente de referência (`./client.ts`) os usa, e o servidor valida o\n// que chega antes de confiar.\n\nimport type { FormattedText } from '@usebridger/core';\n\n/** Fechamento: o primeiro frame não é um `auth` válido (ou chegou outro frame antes do `ready`). */\nexport const CLOSE_INVALID_AUTH = 4400;\n/** Fechamento: token ausente, inválido ou vencido (inclusive no meio da conexão). */\nexport const CLOSE_UNAUTHORIZED = 4401;\n/** Fechamento: o token é válido, mas falta o claim de tenant configurado. */\nexport const CLOSE_FORBIDDEN = 4403;\n/** Fechamento: frame acima de `MAX_FRAME_BYTES`. */\nexport const CLOSE_TOO_BIG = 1009;\n/** Fechamento: o transport desconectou (ou o bot parou). */\nexport const CLOSE_GOING_AWAY = 1001;\n\n/** Teto de um frame. Mídia vai por HTTP, então o frame só leva texto e IDs. */\nexport const MAX_FRAME_BYTES: number = 64 * 1024;\n/** Prazo para o primeiro frame (`auth`) depois de abrir a conexão. */\nexport const AUTH_TIMEOUT_MS = 10_000;\n/** Conversa quando o `auth` não informa. */\nexport const DEFAULT_CONVERSATION = 'default';\n/** Formato do ID de conversa: entra no `chat.id` sem escape. */\nexport const CONVERSATION_PATTERN: RegExp = /^[\\w-]{1,64}$/;\n/** Máximo de anexos numa mensagem do cliente. */\nexport const MAX_ATTACHMENTS = 10;\n\n// --- Cliente → servidor ---\n\n/** Primeiro frame da conexão. O token não volta em frame nenhum nem fica guardado. */\nexport interface AuthFrame {\n readonly type: 'auth';\n readonly token: string;\n /** Conversa do usuário (`[A-Za-z0-9_-]`, até 64). Padrão: `default`. */\n readonly conversation?: string;\n}\n\n/** Mensagem do usuário. Precisa de `text` ou de `attachments`. */\nexport interface ClientMessageFrame {\n readonly type: 'message';\n /** ID do lado do cliente, devolvido no `ack` para casar com o ID do servidor. */\n readonly ref?: string;\n readonly text?: string;\n /** IDs devolvidos pelo `POST <base>/media`, na ordem. */\n readonly attachments?: readonly string[];\n}\n\n/** Clique num botão: o `id` da ação, como veio em `actions`. */\nexport interface ActionFrame {\n readonly type: 'action';\n readonly actionId: string;\n}\n\nexport type ClientFrame = AuthFrame | ClientMessageFrame | ActionFrame;\n\n// --- Servidor → cliente ---\n\n/** Autenticado; o que estava esperando no buffer vem logo depois. */\nexport interface ReadyFrame {\n readonly type: 'ready';\n readonly chatId: string;\n readonly userId: string;\n}\n\n/** Confirma uma mensagem do usuário com o ID que o servidor deu a ela. */\nexport interface AckFrame {\n readonly type: 'ack';\n readonly ref: string | null;\n readonly id: string;\n}\n\n/** Mídia de uma mensagem do bot. `url` relativa é sob a origem do servidor. */\nexport interface MediaRef {\n readonly url: string;\n readonly mimetype?: string;\n readonly fileName?: string;\n}\n\n/** Mensagem do bot. */\nexport interface ServerMessageFrame {\n readonly type: 'message';\n readonly id: string;\n /** Epoch em milissegundos. */\n readonly timestamp: number;\n readonly kind: 'text' | 'image' | 'video' | 'audio' | 'document';\n /** Texto, ou legenda da mídia (o texto visível, quando há `formatted`). */\n readonly text?: string;\n /** Árvore do texto formatado (ADR 0061), para o front renderizar. */\n readonly formatted?: FormattedText;\n readonly media?: MediaRef;\n /** ID da mensagem citada. */\n readonly quotedId?: string;\n readonly actions?: readonly { readonly id: string; readonly label: string }[];\n}\n\nexport interface EditFrame {\n readonly type: 'edit';\n readonly id: string;\n readonly text: string;\n readonly formatted?: FormattedText;\n}\n\nexport interface DeleteFrame {\n readonly type: 'delete';\n readonly id: string;\n}\n\n/** \"Digitando\" (some no próximo frame `message` ou depois de alguns segundos, a critério do front). */\nexport interface TypingFrame {\n readonly type: 'typing';\n readonly kind: 'text' | 'voice';\n}\n\n/** Frame recusado; a conexão segue aberta. */\nexport interface ErrorFrame {\n readonly type: 'error';\n readonly code: 'invalid-frame' | 'media-not-found';\n readonly message: string;\n /** O `ref` da mensagem recusada, quando houver. */\n readonly ref?: string;\n}\n\nexport type ServerFrame =\n | ReadyFrame\n | AckFrame\n | ServerMessageFrame\n | EditFrame\n | DeleteFrame\n | TypingFrame\n | ErrorFrame;\n"],"mappings":";;AAOA,MAAa,qBAAqB;;AAElC,MAAa,qBAAqB;;AAElC,MAAa,kBAAkB;;AAE/B,MAAa,gBAAgB;;AAE7B,MAAa,mBAAmB;;AAGhC,MAAa,kBAA0B;;AAEvC,MAAa,kBAAkB;;AAE/B,MAAa,uBAAuB;;AAEpC,MAAa,uBAA+B;;AAE5C,MAAa,kBAAkB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebridger/transport-web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Transport do Bridger para chat web: WebSocket com JWT, conversas e mídia por HTTP.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/murillous/usebridger.git",
|
|
9
|
+
"directory": "packages/transport-web"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/murillous/usebridger/tree/develop/packages/transport-web/docs#readme",
|
|
12
|
+
"bugs": "https://github.com/murillous/usebridger/issues",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"imports": {
|
|
15
|
+
"#*": "./src/*"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"@usebridger/source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/index.d.mts",
|
|
21
|
+
"default": "./dist/index.mjs"
|
|
22
|
+
},
|
|
23
|
+
"./client": {
|
|
24
|
+
"@usebridger/source": "./src/client.ts",
|
|
25
|
+
"types": "./dist/client.d.mts",
|
|
26
|
+
"default": "./dist/client.mjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@usebridger/core": "^0.2.0",
|
|
34
|
+
"jose": "^6.2.12"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@usebridger/testing": "^0.2.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown"
|
|
44
|
+
}
|
|
6
45
|
}
|
package/README.md
DELETED