@poolse/sdk 0.1.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.
@@ -0,0 +1,775 @@
1
+ import { Channel } from 'phoenix';
2
+
3
+ /**
4
+ * SDK configuration passed to `new Poolse(config)`.
5
+ */
6
+ interface PoolseConfig {
7
+ /**
8
+ * Base URL of the poolse REST API, e.g. `https://chat.example.com`.
9
+ * MUST NOT include the `/v1` path — the SDK adds that itself.
10
+ */
11
+ apiUrl: string;
12
+ /**
13
+ * Async hook the SDK calls every time it needs an `Authorization:
14
+ * Bearer <jwt>` header. Most apps refresh the JWT from their own
15
+ * backend here — the SDK never talks to poolse's `POST
16
+ * /v1/users/:user_id/tokens` itself (that endpoint is API-key-authed
17
+ * and lives on the Customer's BACKEND, not the End User's device).
18
+ *
19
+ * Return `null` to deliberately make an unauthenticated request — the
20
+ * server will reject it, but the SDK won't error inside `getToken`.
21
+ */
22
+ getToken: () => Promise<string | null> | string | null;
23
+ /**
24
+ * Optional fetch override. Browsers and Node 22+ both ship a global
25
+ * `fetch`, but tests can inject a mock here; bundlers in restricted
26
+ * environments can supply a polyfill.
27
+ */
28
+ fetch?: typeof globalThis.fetch;
29
+ /**
30
+ * Retry budget for transient failures (network + 5xx + 429). Defaults
31
+ * to 3 attempts after the initial request. Set to 0 to disable.
32
+ */
33
+ maxRetries?: number;
34
+ /**
35
+ * Base for the exponential backoff, in milliseconds. Each retry waits
36
+ * `min(maxBackoffMs, baseBackoffMs * 2^attempt)` plus jitter, OR honours
37
+ * the `Retry-After` header if present. Default 250 ms.
38
+ */
39
+ baseBackoffMs?: number;
40
+ /** Hard cap on a single retry delay. Default 30_000 ms. */
41
+ maxBackoffMs?: number;
42
+ /**
43
+ * Override the idempotency-key generator. Defaults to
44
+ * `crypto.randomUUID()`. Most apps don't need to override this — the
45
+ * generator is exposed mainly for deterministic tests.
46
+ */
47
+ generateIdempotencyKey?: () => string;
48
+ /**
49
+ * Override the WebSocket URL. Defaults to `apiUrl` with `http(s)://`
50
+ * swapped to `ws(s)://`, suitable when the realtime gateway shares
51
+ * its origin with the REST API. Set explicitly for split-host
52
+ * deployments (`https://api.example.com` REST + `wss://realtime.example.com` WS).
53
+ */
54
+ wsUrl?: string;
55
+ /**
56
+ * Path the WebSocket is mounted on. Defaults to `/socket` — matches
57
+ * `CaasRealtimeWeb.UserSocket`'s mount point.
58
+ */
59
+ socketPath?: string;
60
+ /**
61
+ * Called when the underlying socket encounters a non-fatal error
62
+ * (Phoenix retries internally). Useful for surfacing reconnect
63
+ * banners in the UI without coupling to socket internals.
64
+ */
65
+ onSocketError?: (err: Error) => void;
66
+ }
67
+ /** Internal resolved config — all the defaults filled in. */
68
+ interface ResolvedConfig {
69
+ apiUrl: string;
70
+ getToken: PoolseConfig['getToken'];
71
+ fetch: typeof globalThis.fetch;
72
+ maxRetries: number;
73
+ baseBackoffMs: number;
74
+ maxBackoffMs: number;
75
+ generateIdempotencyKey: () => string;
76
+ wsUrl: string | undefined;
77
+ socketPath: string;
78
+ onSocketError: ((err: Error) => void) | undefined;
79
+ }
80
+
81
+ type Fetcher = PoolseConfig['getToken'];
82
+ interface GetTokenOptions {
83
+ /** Bypass the cache and force a fresh call to the consumer's `getToken`. */
84
+ forceRefresh?: boolean;
85
+ }
86
+ declare class TokenCache {
87
+ private readonly fetcher;
88
+ private token;
89
+ private expMs;
90
+ private inFlight;
91
+ constructor(fetcher: Fetcher);
92
+ /**
93
+ * Synchronously return the cached token without triggering a fetch.
94
+ * Returns `null` if the cache is empty OR if the cached token is
95
+ * within the refresh window (treating near-expiry tokens as stale
96
+ * keeps the realtime layer from handshaking with an about-to-expire
97
+ * JWT when a refresh is already due).
98
+ *
99
+ * Exists for callers like Phoenix.js's `params` callback that the
100
+ * library invokes synchronously and does NOT await — see
101
+ * `phoenix/priv/static/phoenix.mjs::endPointURL()`.
102
+ */
103
+ peekToken(): string | null;
104
+ getToken(opts?: GetTokenOptions): Promise<string | null>;
105
+ invalidate(): void;
106
+ private fetchAndStore;
107
+ }
108
+
109
+ type Uuid = string;
110
+ type IsoDateTime = string;
111
+ interface Me {
112
+ id: Uuid;
113
+ tenant_id: Uuid;
114
+ external_id: string;
115
+ display_name: string | null;
116
+ custom_data: Record<string, unknown>;
117
+ is_blocked: boolean;
118
+ inserted_at: IsoDateTime;
119
+ updated_at: IsoDateTime;
120
+ }
121
+ type ConversationType = 'direct' | 'group';
122
+ interface Conversation {
123
+ id: Uuid;
124
+ tenant_id: Uuid;
125
+ type: ConversationType;
126
+ /** Display name (for group chats); null for unnamed conversations. */
127
+ name: string | null;
128
+ /** Public avatar URL; null when none uploaded. */
129
+ avatar_url: string | null;
130
+ /** User that created the conversation; null for system-created rows. */
131
+ created_by_user_id: Uuid | null;
132
+ /** Hard cap on memberships; null = unlimited (tenant default). */
133
+ member_limit: number | null;
134
+ /**
135
+ * Customer-supplied free-form data — anything you want to attach to
136
+ * the conversation that doesn't fit the schema (UI flags, tags, etc).
137
+ * Server treats it as opaque JSON.
138
+ */
139
+ custom_data: Record<string, unknown>;
140
+ /**
141
+ * Server-defined behavioral knobs (notification rules, retention,
142
+ * etc). Defaults to `{}` and is updateable.
143
+ */
144
+ settings: Record<string, unknown>;
145
+ /** Most recent message's `inserted_at`; null until first message. */
146
+ last_message_at: IsoDateTime | null;
147
+ /** Monotonic per-conversation sequence counter (last message's `sequence`). */
148
+ last_sequence: number;
149
+ inserted_at: IsoDateTime;
150
+ updated_at: IsoDateTime;
151
+ }
152
+ interface ConversationList {
153
+ data: Conversation[];
154
+ }
155
+ interface ConversationCreateRequest {
156
+ type: ConversationType;
157
+ name?: string | null;
158
+ avatar_url?: string | null;
159
+ member_limit?: number | null;
160
+ /**
161
+ * Customer-side user IDs to add as members on creation — saves a
162
+ * round-trip vs creating then calling `addMembers`.
163
+ */
164
+ member_external_ids?: string[];
165
+ custom_data?: Record<string, unknown>;
166
+ settings?: Record<string, unknown>;
167
+ }
168
+ interface ConversationUpdateRequest {
169
+ name?: string | null;
170
+ avatar_url?: string | null;
171
+ member_limit?: number | null;
172
+ custom_data?: Record<string, unknown>;
173
+ settings?: Record<string, unknown>;
174
+ }
175
+ type MemberRole = 'owner' | 'admin' | 'member';
176
+ interface Membership {
177
+ id: Uuid;
178
+ conversation_id: Uuid;
179
+ user_id: Uuid;
180
+ role: MemberRole;
181
+ last_read_message_id: Uuid | null;
182
+ last_read_at: IsoDateTime | null;
183
+ inserted_at: IsoDateTime;
184
+ updated_at: IsoDateTime;
185
+ }
186
+ interface MembershipList {
187
+ data: Membership[];
188
+ }
189
+ /**
190
+ * Server accepts a batch of `external_ids` (the customer's stable user
191
+ * identifiers — what was passed to `POST /v1/users`). The server
192
+ * resolves each to its internal user_id and creates a membership row
193
+ * per external_id, all in one round-trip. Optional `role` defaults to
194
+ * `"member"` server-side.
195
+ *
196
+ * Most callers use the higher-level
197
+ * `chat.conversations.one(id).addMember(externalId)` /
198
+ * `addMembers([externalIds])` methods which build this shape for you.
199
+ */
200
+ interface MembershipCreateRequest {
201
+ external_ids: string[];
202
+ role?: MemberRole;
203
+ }
204
+ type MessageType = 'text' | 'system' | 'custom';
205
+ interface Message {
206
+ id: Uuid;
207
+ tenant_id: Uuid;
208
+ conversation_id: Uuid;
209
+ sender_id: Uuid | null;
210
+ type: MessageType;
211
+ body: string | null;
212
+ reply_to_id: Uuid | null;
213
+ thread_root_id: Uuid | null;
214
+ mentions: Uuid[];
215
+ reactions: Record<string, Uuid[]>;
216
+ edited_at: IsoDateTime | null;
217
+ deleted_at: IsoDateTime | null;
218
+ sequence: number;
219
+ inserted_at: IsoDateTime;
220
+ updated_at: IsoDateTime;
221
+ }
222
+ interface MessageList {
223
+ data: Message[];
224
+ }
225
+ interface MessageCreateRequest {
226
+ body: string;
227
+ type?: MessageType;
228
+ reply_to_id?: Uuid;
229
+ mentions?: Uuid[];
230
+ id?: Uuid;
231
+ }
232
+ interface MessageUpdateRequest {
233
+ body: string;
234
+ }
235
+ interface ReadRequest {
236
+ message_id: Uuid;
237
+ }
238
+ interface ReactionRequest {
239
+ emoji: string;
240
+ }
241
+ type AttachmentStatus = 'pending' | 'ready';
242
+ interface Attachment {
243
+ id: Uuid;
244
+ tenant_id: Uuid;
245
+ /** Linked message id; null while the attachment is still `:pending`. */
246
+ message_id: Uuid | null;
247
+ /** Uploader; null for system-created attachments. */
248
+ sender_id: Uuid | null;
249
+ content_type: string;
250
+ byte_size: number;
251
+ /** Server-computed SHA-256 of the uploaded bytes; populated after the PUT completes. */
252
+ sha256: string | null;
253
+ original_filename: string | null;
254
+ status: AttachmentStatus;
255
+ inserted_at: IsoDateTime;
256
+ updated_at: IsoDateTime;
257
+ }
258
+ interface AttachmentUploadRequest {
259
+ content_type: string;
260
+ byte_size: number;
261
+ original_filename?: string;
262
+ }
263
+ /** Presigned PUT payload returned by `POST /v1/attachments/upload-url`. */
264
+ interface AttachmentUploadResponse {
265
+ attachment: Attachment;
266
+ upload: {
267
+ url: string;
268
+ method: 'put';
269
+ /** Headers the client MUST include on the PUT (signed into the URL). */
270
+ headers: Record<string, string>;
271
+ };
272
+ }
273
+ /** Presigned GET payload returned by `GET /v1/attachments/:id/download-url`. */
274
+ interface AttachmentDownloadResponse {
275
+ url: string;
276
+ method: 'get';
277
+ }
278
+ interface ErrorEnvelope {
279
+ error: {
280
+ code: string;
281
+ message: string;
282
+ doc_url: string;
283
+ details?: Record<string, unknown>;
284
+ };
285
+ }
286
+
287
+ /** `message:new` / `message:updated` push payloads. */
288
+ type MessageNewEvent = Message;
289
+ type MessageUpdatedEvent = Message;
290
+ /** `message:deleted` push payload (just the tombstone, no body). */
291
+ interface MessageDeletedEvent {
292
+ id: Uuid;
293
+ conversation_id: Uuid;
294
+ deleted_at: string | null;
295
+ }
296
+ /** `typing:start` / `typing:stop`. */
297
+ interface TypingEvent {
298
+ user_id: Uuid;
299
+ }
300
+ /** `reaction:added` / `reaction:removed`. */
301
+ interface ReactionEvent {
302
+ message_id: Uuid;
303
+ conversation_id: Uuid;
304
+ emoji: string;
305
+ user_id: Uuid;
306
+ }
307
+ /** Per-user mention push on the `user:<id>` channel. */
308
+ interface MentionEvent {
309
+ message_id: Uuid;
310
+ conversation_id: Uuid;
311
+ sender_id: Uuid | null;
312
+ }
313
+ /**
314
+ * Pushed on `user:<id>` when the user is added to a conversation —
315
+ * either as creator (via `POST /v1/conversations`) or as additional
316
+ * member (via `POST /v1/conversations/:id/members`). Payload is the
317
+ * full conversation row.
318
+ */
319
+ type ConversationCreatedEvent = Conversation;
320
+ /** Phoenix Presence list shape. */
321
+ type PresenceSnapshot = Record<Uuid, {
322
+ metas: Array<{
323
+ phx_ref: string;
324
+ online_at: number;
325
+ external_id?: string;
326
+ }>;
327
+ }>;
328
+ /**
329
+ * Connection status. Reflects the underlying socket lifecycle plus
330
+ * channel join state — easier for UIs than tracking both separately.
331
+ */
332
+ type RealtimeStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'closed';
333
+ /** Unsubscribe handle returned by `onX` listener registrations. */
334
+ type Unsubscribe = () => void;
335
+
336
+ interface RealtimeOptions {
337
+ /**
338
+ * WebSocket endpoint path (mounted in caas_realtime). Defaults to
339
+ * `'/socket'` — matches the path served by `CaasRealtimeWeb.UserSocket`.
340
+ */
341
+ socketPath?: string;
342
+ /**
343
+ * Override the Phoenix-derived WebSocket URL. Use when the realtime
344
+ * gateway lives on a different host than the REST API (most common
345
+ * in prod: `https://api.example.com` for REST, `wss://realtime.example.com`
346
+ * for WebSocket). When unset, the WS URL is derived from `apiUrl` by
347
+ * swapping http(s) → ws(s).
348
+ */
349
+ wsUrl?: string;
350
+ }
351
+ declare class PoolseRealtime {
352
+ private readonly config;
353
+ private readonly tokenCache;
354
+ private readonly socketPath;
355
+ private readonly wsUrl;
356
+ private socket;
357
+ private readonly conversations;
358
+ private userChannel;
359
+ private status;
360
+ private readonly statusListeners;
361
+ constructor(config: ResolvedConfig, tokenCache: TokenCache, opts?: RealtimeOptions);
362
+ /** Current connection status. */
363
+ getStatus(): RealtimeStatus;
364
+ /** Subscribe to connection-status changes. */
365
+ onStatus(listener: (status: RealtimeStatus) => void): Unsubscribe;
366
+ /**
367
+ * Open the socket (idempotent). Synchronous construction so callers
368
+ * can join a channel on the next line; the underlying WebSocket
369
+ * connects on the next tick once the JWT pre-fetch resolves.
370
+ *
371
+ * Phoenix.js invokes the `params` callback SYNCHRONOUSLY on every
372
+ * (re)connect and does NOT await its return value — see
373
+ * `phoenix/priv/static/phoenix.mjs::endPointURL`. So `params` has
374
+ * to read a token that's already in hand. We:
375
+ *
376
+ * 1. Construct the Socket immediately (sets `this.socket` so
377
+ * concurrent `conversation()` / `user()` callers can attach
378
+ * channels — Phoenix buffers joins until the socket opens).
379
+ * 2. Pre-fetch the JWT through `TokenCache`, which fills its
380
+ * internal cache.
381
+ * 3. Call `socket.connect()` so phoenix.js's first handshake reads
382
+ * a primed `peekToken()`.
383
+ *
384
+ * On reconnect, Phoenix calls `params()` again; the cache is still
385
+ * warm (default JWT exp ~1h, refresh window 30s) so `peekToken()`
386
+ * returns the live token. When the token genuinely expires, our
387
+ * REST 401 path invalidates the cache; the next reconnect's
388
+ * `peekToken()` is `null` and the handshake intentionally fails so
389
+ * the cache can re-fill on the next iteration.
390
+ */
391
+ connect(): void;
392
+ /** Close the socket and tear down every joined channel. */
393
+ disconnect(): void;
394
+ /**
395
+ * Subscribe to a conversation. Returns a typed handle with
396
+ * `onMessage`, `onTyping`, etc. Reusing the same `id` returns the
397
+ * same handle — re-subscribing doesn't open a second channel.
398
+ */
399
+ conversation(conversationId: string): ConversationChannel;
400
+ /**
401
+ * Subscribe to the current user's `user:<id>` channel. Only the user
402
+ * matching the JWT can join — poolse's UserChannel enforces this.
403
+ */
404
+ user(userId: string): UserChannel;
405
+ /** Drop a conversation handle and leave the channel. */
406
+ leave(conversationId: string): void;
407
+ private setStatus;
408
+ }
409
+ declare class ConversationChannel {
410
+ readonly conversationId: string;
411
+ private readonly channel;
412
+ private readonly listeners;
413
+ constructor(conversationId: string, channel: Channel);
414
+ /** New message pushed to the conversation. */
415
+ onMessage(fn: (msg: MessageNewEvent) => void): Unsubscribe;
416
+ /** Existing message edited by its sender. */
417
+ onMessageUpdated(fn: (msg: MessageUpdatedEvent) => void): Unsubscribe;
418
+ /** Tombstone for a soft-deleted message. */
419
+ onMessageDeleted(fn: (evt: MessageDeletedEvent) => void): Unsubscribe;
420
+ onTypingStart(fn: (evt: TypingEvent) => void): Unsubscribe;
421
+ onTypingStop(fn: (evt: TypingEvent) => void): Unsubscribe;
422
+ onReactionAdded(fn: (evt: ReactionEvent) => void): Unsubscribe;
423
+ onReactionRemoved(fn: (evt: ReactionEvent) => void): Unsubscribe;
424
+ onPresenceState(fn: (state: PresenceSnapshot) => void): Unsubscribe;
425
+ onPresenceDiff(fn: (diff: PresenceSnapshot) => void): Unsubscribe;
426
+ /** Send a typing ping to the server. Debounced server-side. */
427
+ sendTyping(): void;
428
+ /** @internal — called by `PoolseRealtime.conversation/1`. */
429
+ _join(): void;
430
+ /** @internal — called when the consumer leaves this conversation. */
431
+ _destroy(): void;
432
+ private subscribe;
433
+ }
434
+ declare class UserChannel {
435
+ readonly userId: string;
436
+ private readonly channel;
437
+ private readonly mentionListeners;
438
+ private readonly conversationCreatedListeners;
439
+ private mentionBound;
440
+ private conversationCreatedBound;
441
+ constructor(userId: string, channel: Channel);
442
+ onMention(fn: (evt: MentionEvent) => void): Unsubscribe;
443
+ /**
444
+ * Subscribe to "you've been added to a conversation" notifications.
445
+ * Fires once per new membership — either because you created the
446
+ * conversation, or because someone added you to an existing one.
447
+ *
448
+ * Payload is the full {@link Conversation} row so consumers can
449
+ * prepend it to a local list without a refetch.
450
+ */
451
+ onConversationCreated(fn: (conv: ConversationCreatedEvent) => void): Unsubscribe;
452
+ /** @internal */
453
+ _join(): void;
454
+ /** @internal */
455
+ _destroy(): void;
456
+ }
457
+
458
+ type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
459
+ interface RequestOptions {
460
+ method: HttpMethod;
461
+ path: string;
462
+ /** JSON-serialisable body for non-GETs. Omit for GET/DELETE. */
463
+ body?: unknown;
464
+ /** Query string params; values are stringified, nullish entries dropped. */
465
+ query?: Record<string, string | number | boolean | null | undefined>;
466
+ /**
467
+ * Idempotency-Key override. Defaults to a fresh UUID for non-GETs.
468
+ * Pass `null` to deliberately omit the header (e.g. when the caller's
469
+ * own retry logic supplies a deterministic one).
470
+ */
471
+ idempotencyKey?: string | null;
472
+ /**
473
+ * Per-request override for total retry budget. Defaults to
474
+ * `config.maxRetries`.
475
+ */
476
+ maxRetries?: number;
477
+ /** AbortSignal for caller-driven cancellation. */
478
+ signal?: AbortSignal;
479
+ }
480
+ declare class RestClient {
481
+ private readonly config;
482
+ private readonly tokenCache;
483
+ constructor(config: ResolvedConfig, tokenCache: TokenCache);
484
+ request<T>(opts: RequestOptions): Promise<T>;
485
+ private buildUrl;
486
+ private buildHeaders;
487
+ private resolveIdempotencyKey;
488
+ private backoffDelay;
489
+ }
490
+
491
+ /** Input accepted by {@link AttachmentsResource.upload}. */
492
+ interface AttachmentUploadInput {
493
+ /**
494
+ * The bytes to upload. Browser: pass a `File` or `Blob`. Node /
495
+ * Workers / Deno: a `Uint8Array`, `ArrayBuffer`, or any
496
+ * `BodyInit`-compatible value the runtime's `fetch` accepts as a
497
+ * PUT body.
498
+ */
499
+ body: BodyInit;
500
+ /**
501
+ * MIME type. MUST match what you pass as `content_type` on the
502
+ * upload-URL request (the storage backend signs it into the URL —
503
+ * a mismatch makes the PUT fail with 403).
504
+ */
505
+ contentType: string;
506
+ /** Total bytes — must match the bytes you actually PUT. */
507
+ byteSize: number;
508
+ /** Surfaced in download UX so saved files keep a sensible name. */
509
+ filename?: string;
510
+ }
511
+ /** Options accepted by every attachment method. */
512
+ interface AttachmentOptions {
513
+ signal?: AbortSignal;
514
+ }
515
+ /** Top-level `/v1/attachments` collection. */
516
+ declare class AttachmentsResource {
517
+ private readonly client;
518
+ private readonly fetchFn;
519
+ /**
520
+ * The PUT to the presigned URL bypasses the SDK's authenticated
521
+ * REST client (presigned URLs encode their own auth and MUST NOT
522
+ * receive an `Authorization` header). It still respects
523
+ * `config.fetch` if the customer provided one — required for tests
524
+ * with a mock fetch, and for runtimes where `globalThis.fetch` is
525
+ * not the right transport.
526
+ */
527
+ constructor(client: RestClient, fetchFn: typeof globalThis.fetch);
528
+ /**
529
+ * Step 1 of an upload — request a presigned PUT URL. Use this when
530
+ * you want to drive the PUT yourself (e.g. resumable uploads,
531
+ * React Native FileSystem). For the common case prefer
532
+ * {@link upload}, which does both steps for you.
533
+ */
534
+ requestUpload(attrs: AttachmentUploadRequest, opts?: AttachmentOptions): Promise<AttachmentUploadResponse>;
535
+ /**
536
+ * One-call upload: request a presigned URL, PUT the bytes to it,
537
+ * return the attachment row. After this resolves the attachment is
538
+ * ready to be referenced from a message send.
539
+ *
540
+ * ```ts
541
+ * // Browser <input type="file">:
542
+ * const file = inputEl.files![0]!;
543
+ * const att = await chat.attachments.upload({
544
+ * body: file,
545
+ * contentType: file.type,
546
+ * byteSize: file.size,
547
+ * filename: file.name,
548
+ * });
549
+ * await chat.conversations.one(convId).messages.send({
550
+ * body: 'Look at this!',
551
+ * custom_data: { attachment_id: att.id },
552
+ * });
553
+ * ```
554
+ *
555
+ * Note: the PUT uses the runtime's bare `fetch` (NOT the SDK's
556
+ * authenticated REST client) — presigned URLs already encode their
557
+ * own auth and MUST NOT receive an `Authorization` header.
558
+ */
559
+ upload(input: AttachmentUploadInput, opts?: AttachmentOptions): Promise<Attachment>;
560
+ /** Returns a handle for further operations on a single attachment. */
561
+ one(id: Uuid): AttachmentHandle;
562
+ }
563
+ /** Wraps an attachment id for download-url + delete. */
564
+ declare class AttachmentHandle {
565
+ private readonly client;
566
+ readonly id: Uuid;
567
+ constructor(client: RestClient, id: Uuid);
568
+ /**
569
+ * Request a presigned GET URL (~1h TTL). Conversation-member-gated
570
+ * server-side. Useful when rendering files in chat: cache the URL
571
+ * client-side until close to expiry, then re-fetch.
572
+ */
573
+ downloadUrl(opts?: AttachmentOptions): Promise<AttachmentDownloadResponse>;
574
+ /**
575
+ * Delete the attachment row + best-effort bucket object delete.
576
+ * Authz: uploader (while still `:pending`) or message-sender / conv
577
+ * owner-admin (once linked).
578
+ */
579
+ delete(opts?: AttachmentOptions): Promise<void>;
580
+ }
581
+
582
+ /** Per-conversation message collection: send, list, mark-read. */
583
+ declare class ConversationMessages {
584
+ private readonly client;
585
+ private readonly conversationId;
586
+ constructor(client: RestClient, conversationId: Uuid);
587
+ list(opts?: {
588
+ limit?: number;
589
+ before?: number;
590
+ }, signal?: AbortSignal): Promise<MessageList>;
591
+ /**
592
+ * Send a message to this conversation.
593
+ *
594
+ * If `attrs.id` is omitted the SDK generates a v4 UUID and uses it
595
+ * as both the wire-level idempotency key AND the literal message.id
596
+ * the server stores. Two side-effects that make a real-time UI
597
+ * trivial:
598
+ *
599
+ * * Resending the same `id` (e.g. a network-retry) returns the
600
+ * ORIGINAL message instead of inserting a duplicate.
601
+ * * The realtime `message:new` broadcast carries this same id,
602
+ * so an optimistic UI can pre-render the row under the final id
603
+ * and dedup by id alone — no client/server id swap needed.
604
+ *
605
+ * Pass an explicit `attrs.id` only when you generated it yourself
606
+ * upstream (e.g. you already render an optimistic row in your hook
607
+ * and want the server to confirm under the same key).
608
+ */
609
+ send(attrs: MessageCreateRequest, signal?: AbortSignal): Promise<Message>;
610
+ markRead(messageId: Uuid, signal?: AbortSignal): Promise<void>;
611
+ }
612
+ /** Per-message operations: edit, delete, react, list replies. */
613
+ declare class MessageHandle {
614
+ private readonly client;
615
+ readonly id: Uuid;
616
+ constructor(client: RestClient, id: Uuid);
617
+ update(attrs: MessageUpdateRequest, signal?: AbortSignal): Promise<Message>;
618
+ delete(signal?: AbortSignal): Promise<void>;
619
+ replies(opts?: {
620
+ limit?: number;
621
+ after?: number;
622
+ }, signal?: AbortSignal): Promise<MessageList>;
623
+ addReaction(emoji: string, signal?: AbortSignal): Promise<Message>;
624
+ removeReaction(emoji: string, signal?: AbortSignal): Promise<Message>;
625
+ }
626
+ /** Top-level `/v1/messages` namespace — accessed via `chat.messages(id)`. */
627
+ declare class MessagesResource {
628
+ private readonly client;
629
+ constructor(client: RestClient);
630
+ one(id: Uuid): MessageHandle;
631
+ }
632
+
633
+ /** Optional knobs accepted by every member-add call. */
634
+ interface AddMemberOptions {
635
+ /** Membership role — defaults to `"member"` server-side. */
636
+ role?: MemberRole;
637
+ /** AbortSignal for caller-driven cancellation. */
638
+ signal?: AbortSignal;
639
+ }
640
+ /** Wraps a single conversation by id — used as the entry point for sub-resources. */
641
+ declare class ConversationHandle {
642
+ private readonly client;
643
+ readonly id: Uuid;
644
+ /**
645
+ * Message ops scoped to this conversation: `list`, `send`, `markRead`.
646
+ * Lazy: constructed on first access so an idle handle stays cheap.
647
+ */
648
+ readonly messages: ConversationMessages;
649
+ constructor(client: RestClient, id: Uuid);
650
+ show(signal?: AbortSignal): Promise<Conversation>;
651
+ update(attrs: ConversationUpdateRequest, signal?: AbortSignal): Promise<Conversation>;
652
+ listMembers(signal?: AbortSignal): Promise<MembershipList>;
653
+ /**
654
+ * Add multiple users to this conversation in one round-trip.
655
+ *
656
+ * `externalIds` are the stable customer-side identifiers you passed
657
+ * to `POST /v1/users` when creating each user — the server resolves
658
+ * them to internal user_ids and creates one membership row per id.
659
+ *
660
+ * Requires `:manage_members` on this conversation (owner or admin).
661
+ *
662
+ * ```ts
663
+ * await chat.conversations.one(convId).addMembers(['alice', 'bob']);
664
+ * ```
665
+ */
666
+ addMembers(externalIds: string[], opts?: AddMemberOptions): Promise<MembershipList>;
667
+ /**
668
+ * Add a single user. Convenience wrapper around {@link addMembers}
669
+ * that unwraps the returned list to the single membership row.
670
+ *
671
+ * ```ts
672
+ * const m = await chat.conversations.one(convId).addMember('alice');
673
+ * ```
674
+ */
675
+ addMember(externalId: string, opts?: AddMemberOptions): Promise<Membership>;
676
+ removeMember(userId: Uuid, signal?: AbortSignal): Promise<void>;
677
+ }
678
+ /** Top-level `/v1/conversations` collection. */
679
+ declare class ConversationsResource {
680
+ private readonly client;
681
+ constructor(client: RestClient);
682
+ list(signal?: AbortSignal): Promise<ConversationList>;
683
+ create(attrs: ConversationCreateRequest, signal?: AbortSignal): Promise<Conversation>;
684
+ /** Returns a handle for further operations on a single conversation. */
685
+ one(id: Uuid): ConversationHandle;
686
+ }
687
+
688
+ /** `/v1/me` — the End-User identity behind the presented JWT. */
689
+ declare class MeResource {
690
+ private readonly client;
691
+ constructor(client: RestClient);
692
+ /** GET /v1/me */
693
+ show(signal?: AbortSignal): Promise<Me>;
694
+ }
695
+
696
+ declare class Poolse {
697
+ /** `/v1/me` — current End User. */
698
+ readonly me: MeResource;
699
+ /** `/v1/conversations` collection + per-conversation handle factory. */
700
+ readonly conversations: ConversationsResource;
701
+ /** `/v1/messages/:id/*` — accessed via `chat.messages.one(id)`. */
702
+ readonly messages: MessagesResource;
703
+ /** `/v1/attachments/*` — presigned-URL uploads/downloads. */
704
+ readonly attachments: AttachmentsResource;
705
+ /**
706
+ * Low-level REST client. Exposed for advanced use cases (custom endpoints,
707
+ * raw retry/headers control). Most callers should use the resources above.
708
+ */
709
+ readonly rest: RestClient;
710
+ /**
711
+ * WebSocket / Phoenix Channels client. Lazily connects on the first
712
+ * `poolse.realtime.conversation(id)` / `poolse.realtime.user(id)`
713
+ * call — passing `config.apiUrl` (with `http(s)://` swapped to
714
+ * `ws(s)://`) for the socket URL by default, overridable via
715
+ * `config.wsUrl`.
716
+ */
717
+ readonly realtime: PoolseRealtime;
718
+ private readonly resolved;
719
+ private readonly tokenCache;
720
+ constructor(config: PoolseConfig);
721
+ /**
722
+ * Tear down the SDK: close the WebSocket, drop all channels.
723
+ * No-op for REST — fetch() doesn't keep persistent state.
724
+ * Call this when the user signs out or the SDK instance is
725
+ * being replaced.
726
+ */
727
+ destroy(): void;
728
+ }
729
+
730
+ /** Base for any error originating in the SDK. */
731
+ declare class PoolseError extends Error {
732
+ readonly name: string;
733
+ constructor(message: string);
734
+ }
735
+ /** fetch() rejected or the server returned no response at all. */
736
+ declare class NetworkError extends PoolseError {
737
+ readonly name: string;
738
+ readonly cause: unknown;
739
+ constructor(message: string, cause: unknown);
740
+ }
741
+ /**
742
+ * Server returned a non-2xx status with the canonical error envelope.
743
+ * `code` is poolse's snake_case error code (e.g. `"invalid_user_token"`).
744
+ */
745
+ declare class ApiError extends PoolseError {
746
+ readonly name: string;
747
+ readonly status: number;
748
+ readonly code: string;
749
+ readonly docUrl: string;
750
+ readonly details: Record<string, unknown> | undefined;
751
+ constructor(status: number, envelope: ErrorEnvelope['error']);
752
+ }
753
+ /**
754
+ * 429 specifically. Surfaced as its own subclass so callers can back off
755
+ * politely without parsing the generic ApiError.
756
+ */
757
+ declare class RateLimitedError extends ApiError {
758
+ readonly name: string;
759
+ readonly retryAfterMs: number;
760
+ constructor(envelope: ErrorEnvelope['error'], retryAfterMs: number);
761
+ }
762
+ /**
763
+ * Auth failure (401, including expired/revoked tokens). Callers should
764
+ * refresh the JWT (via `getToken`) and retry — the SDK does NOT do this
765
+ * automatically, because the failure could also mean "the user signed
766
+ * out on another tab" and silent re-auth would hide that.
767
+ */
768
+ declare class AuthError extends ApiError {
769
+ readonly name: string;
770
+ constructor(envelope: ErrorEnvelope['error']);
771
+ }
772
+
773
+ declare const version = "0.0.1";
774
+
775
+ export { type AddMemberOptions, ApiError, type Attachment, type AttachmentDownloadResponse, AttachmentHandle, type AttachmentOptions, type AttachmentStatus, type AttachmentUploadInput, type AttachmentUploadRequest, type AttachmentUploadResponse, AttachmentsResource, AuthError, type Conversation, ConversationChannel, type ConversationCreateRequest, type ConversationCreatedEvent, ConversationHandle, type ConversationList, ConversationMessages, type ConversationType, type ConversationUpdateRequest, ConversationsResource, type ErrorEnvelope, type IsoDateTime, type Me, MeResource, type MemberRole, type Membership, type MembershipCreateRequest, type MembershipList, type MentionEvent, type Message, type MessageCreateRequest, type MessageDeletedEvent, MessageHandle, type MessageList, type MessageNewEvent, type MessageType, type MessageUpdateRequest, type MessageUpdatedEvent, MessagesResource, NetworkError, Poolse, type PoolseConfig, PoolseError, PoolseRealtime, type PresenceSnapshot, RateLimitedError, type ReactionEvent, type ReactionRequest, type ReadRequest, type RealtimeStatus, type TypingEvent, type Unsubscribe, UserChannel, type Uuid, version };