@portalsdk/wire-protocol 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 +21 -0
- package/README.md +76 -0
- package/dist/index.cjs +189 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +672 -0
- package/dist/index.d.ts +672 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol version carried by every upgrade as `?v=` (§1.1, §6).
|
|
3
|
+
*
|
|
4
|
+
* An unknown version is refused at the upgrade with HTTP 426 `unsupported_version`.
|
|
5
|
+
* Within v1 the protocol evolves additively only; a breaking change bumps this.
|
|
6
|
+
*/
|
|
7
|
+
declare const PROTOCOL_VERSION = 1;
|
|
8
|
+
/**
|
|
9
|
+
* Upgrade query-parameter names (§1.1). Build upgrade URLs from these rather than
|
|
10
|
+
* string literals, so a rename is a compile error rather than a silent 4xx.
|
|
11
|
+
*
|
|
12
|
+
* - `version` — required on every upgrade; unknown → 426.
|
|
13
|
+
* - `token` — the signed JWT. Identifies the user; the apiKey is resolved from it.
|
|
14
|
+
* - `leaf` — opaque reconnect token; echo back what `ready` gave you, unchanged.
|
|
15
|
+
* - `meta` — initial presence metadata, base64 JSON (standard channels; ≤1KB decoded).
|
|
16
|
+
* - `last` — highest contiguous seq held, sent on reconnect to request replay (§1.4).
|
|
17
|
+
*/
|
|
18
|
+
declare const UPGRADE_PARAMS: {
|
|
19
|
+
readonly version: "v";
|
|
20
|
+
readonly token: "token";
|
|
21
|
+
readonly leaf: "leaf";
|
|
22
|
+
readonly meta: "meta";
|
|
23
|
+
readonly last: "last";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Response header carrying the refusal code on a refused upgrade (§1.1).
|
|
27
|
+
*
|
|
28
|
+
* Duplicates `code` from the body so a client behind a body-eating proxy can still
|
|
29
|
+
* tell why the socket never opened.
|
|
30
|
+
*/
|
|
31
|
+
declare const PORTAL_ERROR_HEADER = "x-portal-error";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Why an upgrade was refused (§1.1).
|
|
35
|
+
*
|
|
36
|
+
* Refusals happen at the HTTP upgrade — **the socket never opens**. They are therefore
|
|
37
|
+
* disjoint from {@link PublishErrorCode} (HTTP publish rejections) and from the
|
|
38
|
+
* in-session `error` frame, which both presuppose a working connection.
|
|
39
|
+
*/
|
|
40
|
+
type RefusalCode = "invalid_token" | "token_expired" | "invalid_api_key" | "not_member" | "banned" | "anonymous_not_allowed" | "unknown_channel" | "unsupported_version" | "channel_at_capacity";
|
|
41
|
+
/**
|
|
42
|
+
* The HTTP status each refusal is delivered with (§1.1 table).
|
|
43
|
+
*
|
|
44
|
+
* The mapping is many-to-one: status alone does not identify the cause, so read
|
|
45
|
+
* `code` from the body (or the `x-portal-error` header) rather than branching on status.
|
|
46
|
+
*/
|
|
47
|
+
declare const REFUSAL_STATUS: {
|
|
48
|
+
readonly invalid_token: 401;
|
|
49
|
+
readonly token_expired: 401;
|
|
50
|
+
readonly invalid_api_key: 403;
|
|
51
|
+
readonly not_member: 403;
|
|
52
|
+
readonly banned: 403;
|
|
53
|
+
readonly anonymous_not_allowed: 403;
|
|
54
|
+
readonly unknown_channel: 404;
|
|
55
|
+
readonly unsupported_version: 426;
|
|
56
|
+
readonly channel_at_capacity: 429;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Whether a value is a refusal code this version knows (§1.1).
|
|
60
|
+
*
|
|
61
|
+
* A refusal body arriving with an unrecognised code is not a refusal this client can
|
|
62
|
+
* reason about — treat it as an opaque failure rather than coercing it.
|
|
63
|
+
*/
|
|
64
|
+
declare const isRefusalCode: (value: unknown) => value is RefusalCode;
|
|
65
|
+
/** Body of a refused upgrade (§1.1). */
|
|
66
|
+
type RefusalBody = {
|
|
67
|
+
code: RefusalCode;
|
|
68
|
+
reason?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Seconds to wait before retrying. Documented for `channel_at_capacity` (429) only
|
|
71
|
+
* (§1.1); absent on every other refusal.
|
|
72
|
+
*/
|
|
73
|
+
retryAfter?: number;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Why an HTTP publish was rejected (§3.1).
|
|
77
|
+
*
|
|
78
|
+
* Deliberately NOT merged into {@link RefusalCode}: these arrive on a live connection
|
|
79
|
+
* in response to `POST /v1/channels/{id}/messages`, and a client reacts to them per
|
|
80
|
+
* send, not per connection.
|
|
81
|
+
*
|
|
82
|
+
* `blocked_by_middleware` carries user-visible copy in `reason`.
|
|
83
|
+
*/
|
|
84
|
+
type PublishErrorCode = "not_permitted" | "blocked_by_middleware" | "content_too_large" | "rate_limited";
|
|
85
|
+
/**
|
|
86
|
+
* Body of a rejected publish (§3.1).
|
|
87
|
+
*
|
|
88
|
+
* SPEC: §3.1 specifies the shape as `4xx { code, reason? }` but does not map each code
|
|
89
|
+
* to a status, so no status record is exported here (unlike {@link REFUSAL_STATUS}).
|
|
90
|
+
*/
|
|
91
|
+
type PublishErrorBody = {
|
|
92
|
+
code: PublishErrorCode;
|
|
93
|
+
reason?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** A mention as declared by the sender and verified by the platform (§2.1). */
|
|
97
|
+
type Mention = {
|
|
98
|
+
userId: string;
|
|
99
|
+
};
|
|
100
|
+
/** Who sent a {@link WireMessage} (§2.1). */
|
|
101
|
+
type WireSender = {
|
|
102
|
+
id: string;
|
|
103
|
+
anon: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Populated on broadcast channels only — they have no roster to join against.
|
|
106
|
+
* On standard channels display data is joined app-side by `id`.
|
|
107
|
+
*/
|
|
108
|
+
username?: string;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The message envelope as it travels on the wire (§2.1).
|
|
112
|
+
*
|
|
113
|
+
* This is the transport form, one layer BELOW the SDK's public `Message`. Notably it
|
|
114
|
+
* keeps `seq`: ordering, dedup, and gap-fill are expressed in terms of it. Stripping
|
|
115
|
+
* `seq` and deriving `unread`/`status` is the client runtime's job, not this package's.
|
|
116
|
+
*/
|
|
117
|
+
type WireMessage = {
|
|
118
|
+
/** Platform-assigned. The dedup and mutation key. */
|
|
119
|
+
id: string;
|
|
120
|
+
/**
|
|
121
|
+
* Per-channel, assigned at persist; contiguous within a connection's delivery stream.
|
|
122
|
+
*
|
|
123
|
+
* `null` for ephemeral messages, which are not persisted and carry no ordering or
|
|
124
|
+
* gap guarantees (§4).
|
|
125
|
+
*/
|
|
126
|
+
seq: number | null;
|
|
127
|
+
/** Userland discriminator; defaults to `"message"`. Opaque to the platform. */
|
|
128
|
+
type: string;
|
|
129
|
+
/**
|
|
130
|
+
* Envelope content class; `"text"` throughout v1. Media kinds are reserved and will
|
|
131
|
+
* not appear in v1 (§7).
|
|
132
|
+
*
|
|
133
|
+
* SPEC: §2.1 shows `"kind": "text"` by example but never enumerates the field, so
|
|
134
|
+
* this is `string` rather than a closed union — a future kind must not cause a v1
|
|
135
|
+
* parser to drop the frame (§6 forward compatibility).
|
|
136
|
+
*/
|
|
137
|
+
kind: string;
|
|
138
|
+
/** Customer payload, ≤2KB. Opaque to the platform and to this package. */
|
|
139
|
+
content: unknown;
|
|
140
|
+
sender: WireSender;
|
|
141
|
+
/** Epoch milliseconds. */
|
|
142
|
+
timestamp: number;
|
|
143
|
+
/** Targeted-delivery recipient; the message skips fan-out (§2.1). */
|
|
144
|
+
to?: string;
|
|
145
|
+
mentions?: Mention[];
|
|
146
|
+
/** Flips in place via a `retract` frame; content is stripped per policy. */
|
|
147
|
+
retracted: boolean;
|
|
148
|
+
/** Ephemeral messages are not persisted and have `seq: null`. */
|
|
149
|
+
ephemeral: boolean;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Channel mode. Decides presence shape and whether `sender.username` is populated. */
|
|
153
|
+
type ChannelMode = "standard" | "broadcast";
|
|
154
|
+
/** The channel, as described by the connect snapshot (§1.2). */
|
|
155
|
+
type ChannelInfo = {
|
|
156
|
+
id: string;
|
|
157
|
+
mode: ChannelMode;
|
|
158
|
+
name?: string;
|
|
159
|
+
meta?: Record<string, unknown>;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* What this connection is allowed to do (§1.2).
|
|
163
|
+
*
|
|
164
|
+
* Open-ended by design: the index signature lets the platform add capabilities
|
|
165
|
+
* additively (§6) without breaking older clients. Named keys are the ones observed;
|
|
166
|
+
* absent means not granted.
|
|
167
|
+
*
|
|
168
|
+
* SPEC: §1.2 shows only `publish`, but real `ready` frames also carry `sendDirect`
|
|
169
|
+
* (fixture `channel_ready.me.capabilities`). The full key set is not enumerated in the
|
|
170
|
+
* doc.
|
|
171
|
+
*/
|
|
172
|
+
type Capabilities = {
|
|
173
|
+
/** May publish persistent messages via `POST /v1/channels/{id}/messages` (§3.1). */
|
|
174
|
+
publish?: boolean;
|
|
175
|
+
/** May send targeted `to:` messages. Observed in fixtures; not in §1.2. */
|
|
176
|
+
sendDirect?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Unrecognised capabilities are `unknown`, not `boolean`: §1.2 neither enumerates the
|
|
179
|
+
* keys nor promises the values stay boolean, and a stricter type would force the
|
|
180
|
+
* parser to reject a whole `ready` frame over one unrecognised capability.
|
|
181
|
+
*/
|
|
182
|
+
[capability: string]: unknown;
|
|
183
|
+
};
|
|
184
|
+
/** The connected user's own verified identity (§1.2). */
|
|
185
|
+
type MeInfo = {
|
|
186
|
+
id: string;
|
|
187
|
+
anon: boolean;
|
|
188
|
+
/** Whatever the token signer signed. Never assembled client-side. */
|
|
189
|
+
claims: Record<string, unknown>;
|
|
190
|
+
capabilities: Capabilities;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* A participant as it appears **on the wire** (§1.2 snapshot, §2.1 deltas).
|
|
194
|
+
*
|
|
195
|
+
* SPEC: provisional. `{ userId, claims }` is what the current fixtures prove the wire
|
|
196
|
+
* carries (`channel_ready` presence, and the `presence` frame's `joined`), so it is what
|
|
197
|
+
* this type says — the type follows recorded evidence, never an anticipated change.
|
|
198
|
+
*
|
|
199
|
+
* This shape may change in a future version of the package; treat `claims` as unstable
|
|
200
|
+
* and do not build against it.
|
|
201
|
+
*/
|
|
202
|
+
type WirePresenceParticipant = {
|
|
203
|
+
userId: string;
|
|
204
|
+
claims: Record<string, unknown>;
|
|
205
|
+
};
|
|
206
|
+
/** Presence snapshot on a standard channel, carried by `ready` (§1.2). */
|
|
207
|
+
type DetailedPresenceSnapshot = {
|
|
208
|
+
mode: "detailed";
|
|
209
|
+
participants: WirePresenceParticipant[];
|
|
210
|
+
count: number;
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Presence snapshot on a broadcast channel, carried by `ready` (§1.2).
|
|
214
|
+
*
|
|
215
|
+
* SPEC: §1.2 defers the snapshot shape to "detailed | aggregate shape, per mode" and no
|
|
216
|
+
* fixture covers aggregate mode, so this is modelled on the §2.1 aggregate frame with
|
|
217
|
+
* `recent` optional. Unverified against a real broadcast `ready`.
|
|
218
|
+
*/
|
|
219
|
+
type AggregatePresenceSnapshot = {
|
|
220
|
+
mode: "aggregate";
|
|
221
|
+
count: number;
|
|
222
|
+
/** SPEC: element shape is elided as `[...]` in §2.1. See {@link AggregatePresenceFrame}. */
|
|
223
|
+
recent?: unknown[];
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* The `presence` value inside `ready` (§1.2), discriminated on `mode`.
|
|
227
|
+
*
|
|
228
|
+
* Distinct from {@link PresenceFrame}: the snapshot is the full roster
|
|
229
|
+
* (`participants`), while the frame is a delta (`joined`/`left`).
|
|
230
|
+
*/
|
|
231
|
+
type ReadyPresence = DetailedPresenceSnapshot | AggregatePresenceSnapshot;
|
|
232
|
+
/**
|
|
233
|
+
* First frame on a channel socket, exactly once (§1.2).
|
|
234
|
+
*
|
|
235
|
+
* One fat snapshot — there is no staged handshake. Initial history is NOT included;
|
|
236
|
+
* the client fetches `GET /history` (§3.2) in parallel with the upgrade.
|
|
237
|
+
*/
|
|
238
|
+
type ChannelReadyFrame = {
|
|
239
|
+
t: "ready";
|
|
240
|
+
channel: ChannelInfo;
|
|
241
|
+
me: MeInfo;
|
|
242
|
+
/** Channel head at snapshot time. The gap-fill baseline (§4). */
|
|
243
|
+
seq: number;
|
|
244
|
+
/** Opaque reconnect token; send it back unchanged as `?leaf=` on the next connect. */
|
|
245
|
+
leaf: string;
|
|
246
|
+
presence: ReadyPresence;
|
|
247
|
+
/** This user's read position. Absent when watermarks are off (§1.2). */
|
|
248
|
+
watermark?: number;
|
|
249
|
+
/**
|
|
250
|
+
* Cached extension snapshots, keyed by namespace. An unavailable extension is
|
|
251
|
+
* key-absent rather than null.
|
|
252
|
+
*
|
|
253
|
+
* SPEC: §1.2 shows `ext` unconditionally, but real `ready` frames omit it entirely
|
|
254
|
+
* (fixture `channel_ready`), so it is optional here.
|
|
255
|
+
*/
|
|
256
|
+
ext?: Record<string, unknown>;
|
|
257
|
+
/**
|
|
258
|
+
* Extension namespace → transport, for routing `send()` (§1.2).
|
|
259
|
+
*
|
|
260
|
+
* SPEC: shown unconditionally in §1.2 but absent from the fixture; optional here.
|
|
261
|
+
*/
|
|
262
|
+
bindings?: Record<string, string>;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* THE data frame (§2.1). Messages are coalesced per window; `msgs` is ordered and `seq`
|
|
266
|
+
* is contiguous within a connection's delivery stream.
|
|
267
|
+
*/
|
|
268
|
+
type BatchFrame = {
|
|
269
|
+
t: "batch";
|
|
270
|
+
msgs: WireMessage[];
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* A message was retracted (§2.1).
|
|
274
|
+
*
|
|
275
|
+
* May reference a `seq` the client does not hold yet (the retraction can outrun its
|
|
276
|
+
* message): keep a tombstone set and apply on arrival (§4).
|
|
277
|
+
*/
|
|
278
|
+
type RetractFrame = {
|
|
279
|
+
t: "retract";
|
|
280
|
+
id: string;
|
|
281
|
+
seq: number;
|
|
282
|
+
reason?: string;
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* Presence delta on a standard channel (§2.1).
|
|
286
|
+
*
|
|
287
|
+
* SPEC: §2.1 elides the `joined`/`left` element shapes as `[...]`. The fixture proves
|
|
288
|
+
* `joined` carries {@link WirePresenceParticipant}; its `left` is empty, so `left`'s
|
|
289
|
+
* element shape is assumed symmetric and remains **unverified**.
|
|
290
|
+
*/
|
|
291
|
+
type DetailedPresenceFrame = {
|
|
292
|
+
t: "presence";
|
|
293
|
+
mode: "detailed";
|
|
294
|
+
joined: WirePresenceParticipant[];
|
|
295
|
+
left: WirePresenceParticipant[];
|
|
296
|
+
count: number;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Presence delta on a broadcast channel (§2.1).
|
|
300
|
+
*
|
|
301
|
+
* SPEC: `recent` is elided as `[...]` in §2.1 and no fixture covers it, so its element
|
|
302
|
+
* shape is unproven. It stays `unknown[]` rather than guessing: narrowing it here would
|
|
303
|
+
* make an invented shape permanent public surface. It awaits a confirmed shape.
|
|
304
|
+
*/
|
|
305
|
+
type AggregatePresenceFrame = {
|
|
306
|
+
t: "presence";
|
|
307
|
+
mode: "aggregate";
|
|
308
|
+
count: number;
|
|
309
|
+
recent: unknown[];
|
|
310
|
+
};
|
|
311
|
+
/** Presence delta (§2.1), discriminated on `mode`. */
|
|
312
|
+
type PresenceFrame = DetailedPresenceFrame | AggregatePresenceFrame;
|
|
313
|
+
/**
|
|
314
|
+
* Transient per-user activity — typing, thinking, uploading (§2.1).
|
|
315
|
+
*
|
|
316
|
+
* Never echoed for yourself. Peers expire by absence (~5s client-side); there is no
|
|
317
|
+
* explicit "stopped" frame.
|
|
318
|
+
*/
|
|
319
|
+
type ActivityFrame = {
|
|
320
|
+
t: "activity";
|
|
321
|
+
userId: string;
|
|
322
|
+
kind: string;
|
|
323
|
+
/** Epoch milliseconds the activity started. */
|
|
324
|
+
since: number;
|
|
325
|
+
};
|
|
326
|
+
/** Delivery to THIS connection only — `to:`-sends and targeted pushes (§2.1). */
|
|
327
|
+
type DirectFrame = {
|
|
328
|
+
t: "direct";
|
|
329
|
+
msg: WireMessage;
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Connection reassignment (§2.1). Close and reconnect, sending the new token back
|
|
333
|
+
* as `?leaf=`.
|
|
334
|
+
*/
|
|
335
|
+
type ReassignFrame = {
|
|
336
|
+
t: "reassign";
|
|
337
|
+
leaf: string;
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* An in-session error (§2.1). `ref` echoes the `cl` tag of the client frame it answers,
|
|
341
|
+
* so a rejected `ephemeral` can be matched back to its send.
|
|
342
|
+
*
|
|
343
|
+
* SPEC: §2.1 shows `"code": "not_permitted"` by example but never enumerates in-session
|
|
344
|
+
* error codes, so `code` is `string`. It is NOT typed as {@link PublishErrorCode}:
|
|
345
|
+
* §2.2 says upstream gates are "identical" to publish gates, but the doc never states
|
|
346
|
+
* the two code sets are the same, and inventing that equivalence here would make it
|
|
347
|
+
* permanent protocol surface.
|
|
348
|
+
*/
|
|
349
|
+
type ErrorFrame = {
|
|
350
|
+
t: "error";
|
|
351
|
+
code: string;
|
|
352
|
+
reason?: string;
|
|
353
|
+
ref?: string;
|
|
354
|
+
};
|
|
355
|
+
/** Keepalive response (§1.3). Shared with the inbox socket. */
|
|
356
|
+
type PongFrame = {
|
|
357
|
+
t: "pong";
|
|
358
|
+
};
|
|
359
|
+
/** Every frame the platform can send on a channel socket (§1.2, §2.1). */
|
|
360
|
+
type ChannelServerFrame = ChannelReadyFrame | BatchFrame | RetractFrame | PresenceFrame | ActivityFrame | DirectFrame | ReassignFrame | ErrorFrame | PongFrame;
|
|
361
|
+
/**
|
|
362
|
+
* The ephemeral lane (§2.2): no persistence, no `seq`, no history. Cursors, transient
|
|
363
|
+
* signals, and ws-transport extension traffic all ride this.
|
|
364
|
+
*/
|
|
365
|
+
type EphemeralFrame = {
|
|
366
|
+
t: "ephemeral";
|
|
367
|
+
/** Client tag. An `error` frame answering this send echoes it as `ref`. */
|
|
368
|
+
cl: string;
|
|
369
|
+
type: string;
|
|
370
|
+
content: unknown;
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Announce own activity (§2.2). Throttled client-side (~3s).
|
|
374
|
+
*
|
|
375
|
+
* Distinct from the S→C {@link ActivityFrame}, which adds `userId` and `since`: same
|
|
376
|
+
* `t`, different shape, different direction.
|
|
377
|
+
*/
|
|
378
|
+
type ActivityUpFrame = {
|
|
379
|
+
t: "activity";
|
|
380
|
+
kind: string;
|
|
381
|
+
};
|
|
382
|
+
/** Advance this user's read position (§2.2). Independent of inbox read state (§5). */
|
|
383
|
+
type WatermarkFrame = {
|
|
384
|
+
t: "watermark";
|
|
385
|
+
seq: number;
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* Replace this session's presence metadata mid-session (§2.2); the change is
|
|
389
|
+
* re-announced to other participants via presence deltas.
|
|
390
|
+
*
|
|
391
|
+
* `metadata` is client-supplied and presentation-only — it never feeds authorization,
|
|
392
|
+
* which comes from signed token claims. Sends the full replacement bag, not a patch.
|
|
393
|
+
*/
|
|
394
|
+
type MetaFrame = {
|
|
395
|
+
t: "meta";
|
|
396
|
+
metadata: Record<string, unknown>;
|
|
397
|
+
};
|
|
398
|
+
/** Keepalive (§1.3). Shared with the inbox socket. */
|
|
399
|
+
type PingFrame = {
|
|
400
|
+
t: "ping";
|
|
401
|
+
};
|
|
402
|
+
/**
|
|
403
|
+
* The complete upstream set for a channel socket (§2.2).
|
|
404
|
+
*
|
|
405
|
+
* Persistent publishes are NOT here — they go over HTTP (§3.1).
|
|
406
|
+
*/
|
|
407
|
+
type ChannelClientFrame = EphemeralFrame | ActivityUpFrame | WatermarkFrame | MetaFrame | PingFrame;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* One conversation row in the inbox (§5).
|
|
411
|
+
*
|
|
412
|
+
* `muted` silences aggregation, not data: a muted entry keeps updating and stops
|
|
413
|
+
* contributing to the counter, but items addressed to you still land.
|
|
414
|
+
*/
|
|
415
|
+
type InboxEntryWire = {
|
|
416
|
+
/** The channel id this row tracks. */
|
|
417
|
+
id: string;
|
|
418
|
+
name?: string;
|
|
419
|
+
meta?: Record<string, unknown>;
|
|
420
|
+
/** Preview of the most recent message. Absent on large channels (seq-only tier). */
|
|
421
|
+
latest?: {
|
|
422
|
+
text: string;
|
|
423
|
+
sender: {
|
|
424
|
+
id: string;
|
|
425
|
+
};
|
|
426
|
+
at: number;
|
|
427
|
+
};
|
|
428
|
+
unread: number;
|
|
429
|
+
muted: boolean;
|
|
430
|
+
/** Recency, epoch milliseconds. The sort key. */
|
|
431
|
+
at: number;
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* A targeted item: a mention, a `to:`-send, or a notify descriptor (§5).
|
|
435
|
+
*
|
|
436
|
+
* Items carry per-item read state, unlike channels which are positional (watermark).
|
|
437
|
+
*/
|
|
438
|
+
type InboxItemWire = {
|
|
439
|
+
/** Event id; the idempotency key. */
|
|
440
|
+
id: string;
|
|
441
|
+
/** Userland: `"mention"`, `"ticket.assigned"`, … */
|
|
442
|
+
type: string;
|
|
443
|
+
title?: string;
|
|
444
|
+
/** Userland payload. Opaque to the platform and to this package. */
|
|
445
|
+
data: unknown;
|
|
446
|
+
/** Present when the item originated in a channel (mention, `to:`-send). */
|
|
447
|
+
channelId?: string;
|
|
448
|
+
at: number;
|
|
449
|
+
read: boolean;
|
|
450
|
+
};
|
|
451
|
+
/**
|
|
452
|
+
* First frame on an inbox socket (§5).
|
|
453
|
+
*
|
|
454
|
+
* Anonymous tokens never get here — they are refused at the upgrade with 403
|
|
455
|
+
* `anonymous_not_allowed`, because no inbox exists for them.
|
|
456
|
+
*/
|
|
457
|
+
type InboxReadyFrame = {
|
|
458
|
+
t: "ready";
|
|
459
|
+
entries: InboxEntryWire[];
|
|
460
|
+
items: InboxItemWire[];
|
|
461
|
+
counter: number;
|
|
462
|
+
};
|
|
463
|
+
/** A row upsert — preview, unread, or mute changed (§5). */
|
|
464
|
+
type InboxEntryFrame = {
|
|
465
|
+
t: "entry";
|
|
466
|
+
entry: InboxEntryWire;
|
|
467
|
+
};
|
|
468
|
+
/** A targeted item arrived (§5). */
|
|
469
|
+
type InboxItemFrame = {
|
|
470
|
+
t: "item";
|
|
471
|
+
item: InboxItemWire;
|
|
472
|
+
};
|
|
473
|
+
/** The global badge changed (§5). Pushed on change. */
|
|
474
|
+
type InboxCounterFrame = {
|
|
475
|
+
t: "counter";
|
|
476
|
+
n: number;
|
|
477
|
+
};
|
|
478
|
+
/** Every frame the platform can send on an inbox socket (§5). */
|
|
479
|
+
type InboxServerFrame = InboxReadyFrame | InboxEntryFrame | InboxItemFrame | InboxCounterFrame | PongFrame;
|
|
480
|
+
/**
|
|
481
|
+
* Advance the inbox position for one channel — clears its sidebar badge (§5).
|
|
482
|
+
*
|
|
483
|
+
* NOT the channel watermark: the inbox tracks *noticing*, the channel tracks *reading*,
|
|
484
|
+
* and the two may legitimately disagree.
|
|
485
|
+
*/
|
|
486
|
+
type InboxReadFrame = {
|
|
487
|
+
t: "read";
|
|
488
|
+
channelId: string;
|
|
489
|
+
};
|
|
490
|
+
/** Flip one item's read flag (§5). Never cascades to older items. */
|
|
491
|
+
type InboxItemReadFrame = {
|
|
492
|
+
t: "item.read";
|
|
493
|
+
id: string;
|
|
494
|
+
};
|
|
495
|
+
/** Mark ALL items read (§5). Global and zero-arg — it ignores any client-side filter. */
|
|
496
|
+
type InboxReadAllFrame = {
|
|
497
|
+
t: "read.all";
|
|
498
|
+
};
|
|
499
|
+
/** Set the durable per-user-per-channel mute preference (§5). */
|
|
500
|
+
type InboxMuteFrame = {
|
|
501
|
+
t: "mute";
|
|
502
|
+
channelId: string;
|
|
503
|
+
muted: boolean;
|
|
504
|
+
};
|
|
505
|
+
/** The complete upstream set for an inbox socket (§5). */
|
|
506
|
+
type InboxClientFrame = InboxReadFrame | InboxItemReadFrame | InboxReadAllFrame | InboxMuteFrame | PingFrame;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Body of `POST /v1/channels/{channelId}/messages` (§3.1).
|
|
510
|
+
*
|
|
511
|
+
* Persistent publishes go over HTTP, never the socket (§2.2).
|
|
512
|
+
*/
|
|
513
|
+
type PublishBody = {
|
|
514
|
+
/** Userland discriminator; defaults to `"message"`. */
|
|
515
|
+
type?: string;
|
|
516
|
+
/** ≤2KB, opaque. */
|
|
517
|
+
content: unknown;
|
|
518
|
+
/** Defaults to `"text"`. Media kinds are rejected in v1 (§7). */
|
|
519
|
+
kind?: string;
|
|
520
|
+
/**
|
|
521
|
+
* Delivery instruction: skip fan-out and deliver to this member only, writing their
|
|
522
|
+
* inbox item. A field named `to` inside `content` routes nothing.
|
|
523
|
+
*/
|
|
524
|
+
to?: string;
|
|
525
|
+
/** Declared by the sender; the platform verifies, dedupes, and caps them. */
|
|
526
|
+
mentions?: Mention[];
|
|
527
|
+
};
|
|
528
|
+
/**
|
|
529
|
+
* A successful publish (§3.1) — the wire form of the SendAck.
|
|
530
|
+
*
|
|
531
|
+
* Named `…Wire` deliberately: the SDK's *public* `SendAck` is `{ id, timestamp }` with
|
|
532
|
+
* no `seq`, because `seq` is transport and gets stripped at the SDK edge. Same concept,
|
|
533
|
+
* different layer — do not treat the two as interchangeable.
|
|
534
|
+
*
|
|
535
|
+
* An ack means accepted and durable; it does not mean permanent (a retraction may
|
|
536
|
+
* follow).
|
|
537
|
+
*/
|
|
538
|
+
type SendAckWire = {
|
|
539
|
+
id: string;
|
|
540
|
+
seq: number;
|
|
541
|
+
timestamp: number;
|
|
542
|
+
};
|
|
543
|
+
/**
|
|
544
|
+
* `GET /v1/channels/{channelId}/history` (§3.2).
|
|
545
|
+
*
|
|
546
|
+
* One endpoint serves initial backfill, scroll-up paging (`?before=&limit=`), and
|
|
547
|
+
* gap-fill ranges (`?from=&to=`). Retracted messages come back as tombstoned envelopes,
|
|
548
|
+
* consistent with live rendering.
|
|
549
|
+
*/
|
|
550
|
+
type HistoryResponse = {
|
|
551
|
+
msgs: WireMessage[];
|
|
552
|
+
hasMore: boolean;
|
|
553
|
+
};
|
|
554
|
+
/** One row of the member directory (§3.3). */
|
|
555
|
+
type MemberRow = {
|
|
556
|
+
userId: string;
|
|
557
|
+
online: boolean;
|
|
558
|
+
claims: Record<string, unknown>;
|
|
559
|
+
};
|
|
560
|
+
/**
|
|
561
|
+
* `GET /v1/channels/{channelId}/members` (§3.3). Standard channels only.
|
|
562
|
+
*
|
|
563
|
+
* A fetched directory including offline members — not live presence state. `cursor` is
|
|
564
|
+
* absent on the last page.
|
|
565
|
+
*/
|
|
566
|
+
type MembersResponse = {
|
|
567
|
+
members: MemberRow[];
|
|
568
|
+
cursor?: string;
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* A well-formed frame whose `t` this version does not know (§6).
|
|
573
|
+
*
|
|
574
|
+
* v1 evolves additively: the platform may introduce new frame types, and an older
|
|
575
|
+
* client MUST ignore them. Ignorable is not the same as droppable — the parser hands
|
|
576
|
+
* the frame back intact so it survives a parse → serialize round-trip and can be
|
|
577
|
+
* logged, forwarded, or inspected rather than silently vanishing.
|
|
578
|
+
*/
|
|
579
|
+
type UnknownFrame = {
|
|
580
|
+
t: string;
|
|
581
|
+
[field: string]: unknown;
|
|
582
|
+
};
|
|
583
|
+
/**
|
|
584
|
+
* What {@link parseChannelFrame} yields: a known S→C channel frame, or an
|
|
585
|
+
* {@link UnknownFrame} passthrough.
|
|
586
|
+
*/
|
|
587
|
+
type ParsedChannelFrame = ChannelServerFrame | UnknownFrame;
|
|
588
|
+
/**
|
|
589
|
+
* What {@link parseInboxFrame} yields: a known S→C inbox frame, or an
|
|
590
|
+
* {@link UnknownFrame} passthrough.
|
|
591
|
+
*/
|
|
592
|
+
type ParsedInboxFrame = InboxServerFrame | UnknownFrame;
|
|
593
|
+
/**
|
|
594
|
+
* What {@link parseChannelClientFrame} yields: a known C→S channel frame, or an
|
|
595
|
+
* {@link UnknownFrame} passthrough. Used by a server (or mock) receiving upstream frames.
|
|
596
|
+
*/
|
|
597
|
+
type ParsedChannelClientFrame = ChannelClientFrame | UnknownFrame;
|
|
598
|
+
/**
|
|
599
|
+
* What {@link parseInboxClientFrame} yields: a known C→S inbox frame, or an
|
|
600
|
+
* {@link UnknownFrame} passthrough.
|
|
601
|
+
*/
|
|
602
|
+
type ParsedInboxClientFrame = InboxClientFrame | UnknownFrame;
|
|
603
|
+
/** Any frame this package can represent, either direction, either socket family. */
|
|
604
|
+
type AnyFrame = ChannelServerFrame | ChannelClientFrame | InboxServerFrame | InboxClientFrame | UnknownFrame;
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Parse a text frame from a **channel** socket (S→C).
|
|
608
|
+
*
|
|
609
|
+
* Total and non-throwing:
|
|
610
|
+
* - malformed JSON, a non-object, or a missing/non-string `t` → `null`
|
|
611
|
+
* - a known `t` whose shape does not match → `null`
|
|
612
|
+
* - an unknown `t` → the frame is returned intact as an {@link UnknownFrame} (§6),
|
|
613
|
+
* because forward compatibility says ignore it, not lose it
|
|
614
|
+
*
|
|
615
|
+
* Unknown *fields* on known frames are preserved, so `parse` → {@link serializeFrame}
|
|
616
|
+
* round-trips without dropping anything.
|
|
617
|
+
*/
|
|
618
|
+
declare function parseChannelFrame(raw: string): ParsedChannelFrame | null;
|
|
619
|
+
/**
|
|
620
|
+
* Parse a text frame from an **inbox** socket (S→C).
|
|
621
|
+
*
|
|
622
|
+
* Same contract as {@link parseChannelFrame}. The two families are disjoint: an inbox
|
|
623
|
+
* `ready` and a channel `ready` share a `t` but not a shape, so a frame must be parsed
|
|
624
|
+
* with the function matching the socket it arrived on.
|
|
625
|
+
*/
|
|
626
|
+
declare function parseInboxFrame(raw: string): ParsedInboxFrame | null;
|
|
627
|
+
/**
|
|
628
|
+
* Parse a text frame a client sent on a **channel** socket (C→S).
|
|
629
|
+
*
|
|
630
|
+
* The upstream counterpart of {@link parseChannelFrame}, for a server or test mock
|
|
631
|
+
* receiving the frames a client sends. Same totality contract: `null` on malformed JSON
|
|
632
|
+
* / missing `t` / a known `t` with a bad shape, and an {@link UnknownFrame} passthrough
|
|
633
|
+
* for an unrecognised `t` (§6).
|
|
634
|
+
*/
|
|
635
|
+
declare function parseChannelClientFrame(raw: string): ParsedChannelClientFrame | null;
|
|
636
|
+
/**
|
|
637
|
+
* Parse a text frame a client sent on an **inbox** socket (C→S).
|
|
638
|
+
*
|
|
639
|
+
* Same contract as {@link parseChannelClientFrame}.
|
|
640
|
+
*/
|
|
641
|
+
declare function parseInboxClientFrame(raw: string): ParsedInboxClientFrame | null;
|
|
642
|
+
/**
|
|
643
|
+
* Serialize a frame to a JSON text frame.
|
|
644
|
+
*
|
|
645
|
+
* Primarily for C→S sends. It accepts any frame so that a parsed S→C frame can be
|
|
646
|
+
* re-serialized intact — the round-trip that proves unknown fields survive (§6).
|
|
647
|
+
*/
|
|
648
|
+
declare function serializeFrame(frame: AnyFrame): string;
|
|
649
|
+
/** True when the parser recognised this channel frame — i.e. it is not an `UnknownFrame`. */
|
|
650
|
+
declare const isKnownChannelFrame: (frame: ParsedChannelFrame) => frame is ChannelServerFrame;
|
|
651
|
+
/** True when the parser recognised this inbox frame — i.e. it is not an `UnknownFrame`. */
|
|
652
|
+
declare const isKnownInboxFrame: (frame: ParsedInboxFrame) => frame is InboxServerFrame;
|
|
653
|
+
/** True when the parser recognised this C→S channel frame (not an `UnknownFrame`). */
|
|
654
|
+
declare const isKnownChannelClientFrame: (frame: ParsedChannelClientFrame) => frame is ChannelClientFrame;
|
|
655
|
+
/** True when the parser recognised this C→S inbox frame (not an `UnknownFrame`). */
|
|
656
|
+
declare const isKnownInboxClientFrame: (frame: ParsedInboxClientFrame) => frame is InboxClientFrame;
|
|
657
|
+
declare const isChannelReady: (f: ParsedChannelFrame) => f is ChannelReadyFrame;
|
|
658
|
+
declare const isBatch: (f: ParsedChannelFrame) => f is BatchFrame;
|
|
659
|
+
declare const isRetract: (f: ParsedChannelFrame) => f is RetractFrame;
|
|
660
|
+
declare const isPresence: (f: ParsedChannelFrame) => f is PresenceFrame;
|
|
661
|
+
declare const isActivity: (f: ParsedChannelFrame) => f is ActivityFrame;
|
|
662
|
+
declare const isDirect: (f: ParsedChannelFrame) => f is DirectFrame;
|
|
663
|
+
declare const isReassign: (f: ParsedChannelFrame) => f is ReassignFrame;
|
|
664
|
+
declare const isError: (f: ParsedChannelFrame) => f is ErrorFrame;
|
|
665
|
+
declare const isPong: (f: ParsedChannelFrame) => f is PongFrame;
|
|
666
|
+
declare const isInboxReady: (f: ParsedInboxFrame) => f is InboxReadyFrame;
|
|
667
|
+
declare const isInboxEntry: (f: ParsedInboxFrame) => f is InboxEntryFrame;
|
|
668
|
+
declare const isInboxItem: (f: ParsedInboxFrame) => f is InboxItemFrame;
|
|
669
|
+
declare const isInboxCounter: (f: ParsedInboxFrame) => f is InboxCounterFrame;
|
|
670
|
+
declare const isInboxPong: (f: ParsedInboxFrame) => f is PongFrame;
|
|
671
|
+
|
|
672
|
+
export { type ActivityFrame, type ActivityUpFrame, type AggregatePresenceFrame, type AggregatePresenceSnapshot, type AnyFrame, type BatchFrame, type Capabilities, type ChannelClientFrame, type ChannelInfo, type ChannelMode, type ChannelReadyFrame, type ChannelServerFrame, type DetailedPresenceFrame, type DetailedPresenceSnapshot, type DirectFrame, type EphemeralFrame, type ErrorFrame, type HistoryResponse, type InboxClientFrame, type InboxCounterFrame, type InboxEntryFrame, type InboxEntryWire, type InboxItemFrame, type InboxItemReadFrame, type InboxItemWire, type InboxMuteFrame, type InboxReadAllFrame, type InboxReadFrame, type InboxReadyFrame, type InboxServerFrame, type MeInfo, type MemberRow, type MembersResponse, type Mention, type MetaFrame, PORTAL_ERROR_HEADER, PROTOCOL_VERSION, type ParsedChannelClientFrame, type ParsedChannelFrame, type ParsedInboxClientFrame, type ParsedInboxFrame, type PingFrame, type PongFrame, type PresenceFrame, type PublishBody, type PublishErrorBody, type PublishErrorCode, REFUSAL_STATUS, type ReadyPresence, type ReassignFrame, type RefusalBody, type RefusalCode, type RetractFrame, type SendAckWire, UPGRADE_PARAMS, type UnknownFrame, type WatermarkFrame, type WireMessage, type WirePresenceParticipant, type WireSender, isActivity, isBatch, isChannelReady, isDirect, isError, isInboxCounter, isInboxEntry, isInboxItem, isInboxPong, isInboxReady, isKnownChannelClientFrame, isKnownChannelFrame, isKnownInboxClientFrame, isKnownInboxFrame, isPong, isPresence, isReassign, isRefusalCode, isRetract, parseChannelClientFrame, parseChannelFrame, parseInboxClientFrame, parseInboxFrame, serializeFrame };
|