@sockethub/client 5.0.0-alpha.9 → 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,352 @@
1
+ import type { ActivityStream, ServiceDescriptor, ServiceEndpoints } from "@sockethub/schemas";
2
+ import EventEmitter from "eventemitter3";
3
+ import type { ManagerOptions, Socket, SocketOptions } from "socket.io-client";
4
+ import { type PlatformRegistryEntry, type PlatformRegistryPayload } from "./client-registry";
5
+ import { type DiscoverOptions, type SocketFactory } from "./discovery";
6
+ export type { ServiceDescriptor, ServiceEndpoints };
7
+ export type { PlatformRegistryEntry, PlatformRegistryPayload, } from "./client-registry";
8
+ export { type DiscoverOptions, DiscoveryError, discoverSockethub, resolveEndpoint, type SocketFactory, } from "./discovery";
9
+ export type { EventMapping } from "./replay-store";
10
+ type InitState = "idle" | "initializing" | "ready" | "init_error" | "closed";
11
+ type ReadyReason = "initial-connect" | "reconnect" | "schemas-update";
12
+ type InitErrorPhase = "schemas-request" | "schemas-apply" | "timeout";
13
+ export interface SockethubClientOptions {
14
+ initTimeoutMs?: number;
15
+ maxQueuedOutbound?: number;
16
+ maxQueuedAgeMs?: number;
17
+ /**
18
+ * Client-side schema validation. `"auto"` (default) validates with Ajv when
19
+ * the environment allows code generation; environments that forbid it (a
20
+ * browser-extension CSP) skip validation automatically. `"off"` always
21
+ * skips it. The server validates every message regardless.
22
+ */
23
+ validation?: "auto" | "off";
24
+ }
25
+ export interface ConnectOptions extends SockethubClientOptions, DiscoverOptions {
26
+ /**
27
+ * The `io()` factory to create the socket with. Defaults to the `io`
28
+ * global set by `/socket.io.js`, then to the `socket.io-client` package
29
+ * when it can be imported.
30
+ */
31
+ io?: SocketFactory;
32
+ /**
33
+ * Extra Socket.IO options (auth, transports, ...). `path` is always taken
34
+ * from the discovered descriptor.
35
+ */
36
+ socketOptions?: Partial<ManagerOptions & SocketOptions>;
37
+ }
38
+ interface CustomEmitter extends EventEmitter {
39
+ _emit(s: string, o: unknown, c?: unknown): void;
40
+ connect(): void;
41
+ disconnect(): void;
42
+ connected: boolean;
43
+ id: string;
44
+ }
45
+ export interface ClientReadyInfo {
46
+ state: "ready";
47
+ reason: ReadyReason;
48
+ apiVersion: number;
49
+ contexts: {
50
+ as: string;
51
+ sockethub: string;
52
+ };
53
+ platforms: Array<{
54
+ id: string;
55
+ apiVersion: number;
56
+ contextUrl: string;
57
+ contextVersion: string;
58
+ schemaVersion: string;
59
+ types: Array<string>;
60
+ }>;
61
+ }
62
+ export interface ClientInitError {
63
+ error: string;
64
+ phase: InitErrorPhase;
65
+ retrying: boolean;
66
+ }
67
+ /**
68
+ * SockethubClient - Client library for Sockethub protocol gateway
69
+ *
70
+ * A JavaScript client for connecting to Sockethub servers. Provides a high-level
71
+ * API for sending and receiving ActivityStreams messages over Socket.IO, with
72
+ * automatic state management and reconnection handling.
73
+ *
74
+ * Sockethub acts as a protocol gateway, translating ActivityStreams messages into
75
+ * various protocols (XMPP, IRC, RSS, etc.). This client handles the communication
76
+ * with the Sockethub server, including credential management, connection state,
77
+ * and automatic reconnection.
78
+ *
79
+ * ## Automatic Reconnection & State Replay
80
+ *
81
+ * This client automatically handles transient network disconnections by storing
82
+ * connection state in memory and replaying it when the socket reconnects.
83
+ *
84
+ * ### Security Model
85
+ *
86
+ * **Storage Location:**
87
+ * - All credentials and state are stored ONLY in JavaScript memory (heap)
88
+ * - Nothing is persisted to localStorage, sessionStorage, cookies, or disk
89
+ * - Memory is cleared when the browser tab closes or page refreshes
90
+ *
91
+ * **Replay Triggers:**
92
+ * - Automatic replay occurs on Socket.IO reconnection events
93
+ * - Typically triggered by brief network interruptions (WiFi switching, mobile network blips)
94
+ * - Does NOT occur on page refresh (new SockethubClient instance = empty state)
95
+ *
96
+ * **Server Restart Behavior:**
97
+ * - If server restarts, client socket will reconnect and replay credentials
98
+ * - Server must handle replayed credentials appropriately (validate, reject stale sessions, etc.)
99
+ * - Applications should implement proper session validation server-side
100
+ *
101
+ * **Lifetime:**
102
+ * - Credentials exist only during the browser tab's lifetime
103
+ * - Cleared on page reload, tab close, or manual disconnect
104
+ * - Not accessible across tabs or after browser restart
105
+ *
106
+ * **What Gets Replayed:**
107
+ * - Credentials (username/password/tokens sent via credentials event)
108
+ * - Activity Objects (actor definitions)
109
+ * - Connect commands (platform connections)
110
+ * - Join commands (room/channel joins)
111
+ *
112
+ * When one of this client's actors is renamed (an `update` with
113
+ * `object.type: "address"`, e.g. an IRC nick change, whether this client
114
+ * requested it and the server acknowledged it, or the server reported it),
115
+ * the stored entries move to the new actor so a reconnect replays the current
116
+ * identity rather than the old one.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * // Discover the Socket.IO endpoint from the server's base URL and connect
121
+ * const client = await SockethubClient.connect('http://localhost:10550');
122
+ *
123
+ * // Or wrap a socket you created yourself
124
+ * const client = new SockethubClient(io('http://localhost:10550', { path: '/sockethub' }));
125
+ *
126
+ * // Wait for schema registry before sending messages
127
+ * await client.ready();
128
+ *
129
+ * // Build canonical @context for a platform
130
+ * const ctx = client.contextFor('irc');
131
+ *
132
+ * // Send credentials - these will be replayed on reconnection
133
+ * client.socket.emit('credentials', {
134
+ * '@context': ctx,
135
+ * type: 'credentials',
136
+ * actor: { id: 'user@example.com', type: 'person' },
137
+ * object: { type: 'credentials', username: 'user', password: 'pass' }
138
+ * });
139
+ * ```
140
+ */
141
+ export default class SockethubClient {
142
+ private replayStore;
143
+ private _socket;
144
+ socket: CustomEmitter;
145
+ debug: boolean;
146
+ /**
147
+ * The service descriptor fetched by `SockethubClient.connect()`: API
148
+ * versions, enabled platforms, and the advertised endpoints (including
149
+ * `endpoints.httpActions` when that transport is on). Undefined for
150
+ * clients built from a ready-made socket.
151
+ */
152
+ readonly descriptor?: ServiceDescriptor;
153
+ /**
154
+ * The origin the descriptor was discovered from and the socket connects
155
+ * to. `endpointUrl()` resolves the descriptor's endpoint paths against it.
156
+ * Undefined for clients built from a ready-made socket.
157
+ */
158
+ readonly serverOrigin?: string;
159
+ /**
160
+ * Absolute URL of an advertised endpoint, resolved against
161
+ * `serverOrigin`, or undefined when the server does not advertise it (for
162
+ * example `httpActions` while HTTP actions are disabled) or when this
163
+ * client was not created by `connect()`.
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const url = sc.endpointUrl('httpActions');
168
+ * if (url) {
169
+ * await fetch(url, { method: 'POST', body });
170
+ * }
171
+ * ```
172
+ */
173
+ endpointUrl(name: keyof ServiceEndpoints): string | undefined;
174
+ private readonly options;
175
+ private platformRegistry;
176
+ private asContextUrl?;
177
+ private sockethubContextUrl?;
178
+ private apiVersion?;
179
+ private initState;
180
+ private hasReadyOnce;
181
+ private initCycle?;
182
+ private initTokenCounter;
183
+ private initTimeoutTimer?;
184
+ private waitingWarningTimer?;
185
+ private waitingWarningIntervalMs;
186
+ private readyWaiters;
187
+ private outboundQueue;
188
+ private outboundSequence;
189
+ private registryFingerprint?;
190
+ private latestReadyInfo?;
191
+ /**
192
+ * Discover a server's endpoints from its base URL and connect to it.
193
+ *
194
+ * Fetches the base URL with `Accept: application/json`, validates the
195
+ * service descriptor, and opens a Socket.IO connection to the base URL's
196
+ * origin with the advertised path. The descriptor is exposed as
197
+ * `client.descriptor` and the origin as `client.serverOrigin`. Rejects
198
+ * with a `DiscoveryError` when the server is unreachable, does not answer
199
+ * with JSON, or does not advertise a Socket.IO endpoint.
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const sc = await SockethubClient.connect('https://sh.example.org', {
204
+ * initTimeoutMs: 5000,
205
+ * });
206
+ * await sc.ready();
207
+ * console.log(sc.endpointUrl('httpActions')); // absolute URL, or undefined
208
+ * ```
209
+ */
210
+ static connect(baseUrl: string, options?: ConnectOptions): Promise<SockethubClient>;
211
+ constructor(socket: Socket, options?: SockethubClientOptions, discovered?: {
212
+ descriptor: ServiceDescriptor;
213
+ serverOrigin: string;
214
+ });
215
+ /**
216
+ * Clear stored credentials to prevent automatic replay on reconnection.
217
+ *
218
+ * This method removes all stored credentials from memory. Useful for
219
+ * security-sensitive applications that want to prevent automatic credential
220
+ * replay when the socket reconnects.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * // Clear credentials on disconnect
225
+ * sc.socket.on('disconnect', () => {
226
+ * sc.clearCredentials();
227
+ * });
228
+ * ```
229
+ */
230
+ clearCredentials(): void;
231
+ /**
232
+ * Return the platform registry discovered from the server.
233
+ */
234
+ getRegisteredPlatforms(): Array<PlatformRegistryEntry>;
235
+ /**
236
+ * Indicates whether server-provided schema/context registry data is loaded.
237
+ */
238
+ isSchemasReady(): boolean;
239
+ /**
240
+ * Indicates whether the client has completed schema initialization.
241
+ */
242
+ isReady(): boolean;
243
+ /**
244
+ * Returns the current client initialization state.
245
+ */
246
+ getInitState(): InitState;
247
+ /**
248
+ * Return the canonical base contexts learned from the server registry.
249
+ */
250
+ getRegisteredBaseContexts(): {
251
+ as: string;
252
+ sockethub: string;
253
+ };
254
+ getPlatformSchema(platform: string, schemaType?: "messages" | "credentials"): object | undefined;
255
+ /**
256
+ * Wait for schema registry data from the server and return the normalized payload.
257
+ * @deprecated Use ready(timeoutMs?) instead.
258
+ */
259
+ waitForSchemas(timeoutMs?: number): Promise<PlatformRegistryPayload>;
260
+ /**
261
+ * Wait until the client reaches a ready state.
262
+ */
263
+ ready(timeoutMs?: number): Promise<ClientReadyInfo>;
264
+ /**
265
+ * Validate an activity stream against currently registered platform schemas.
266
+ * Returns an empty string when valid.
267
+ */
268
+ validateActivity(activity: ActivityStream): string;
269
+ /**
270
+ * Build canonical Sockethub contexts for a platform using server-provided schema metadata.
271
+ */
272
+ contextFor(platform: string): ActivityStream["@context"];
273
+ private createPublicEmitter;
274
+ /**
275
+ * Ask server for the latest platform/context registry via ack callback.
276
+ * This keeps client context composition aligned with server schema state.
277
+ */
278
+ private requestSchemaRegistry;
279
+ /**
280
+ * Apply server-provided registry metadata to local runtime state.
281
+ * Also registers platform contexts/schemas with @sockethub/schemas validators
282
+ * so local validation uses the same canonical sources as the server.
283
+ */
284
+ private applyPlatformRegistry;
285
+ private buildPlatformRegistryPayload;
286
+ private buildReadyInfo;
287
+ private resolveReadyWaiters;
288
+ private rejectReadyWaiters;
289
+ private emitInitError;
290
+ private emitClientError;
291
+ private clearInitTimers;
292
+ private startWaitingWarnings;
293
+ private startInitialization;
294
+ private markReady;
295
+ private handleSchemasPayload;
296
+ private handlePublicEmit;
297
+ private enqueueOutbound;
298
+ private flushOutboundQueue;
299
+ private sendOutbound;
300
+ private log;
301
+ private registerSocketIOHandlers;
302
+ /**
303
+ * Keeps the replay maps on the actor the server now knows after a rename
304
+ * the server reported.
305
+ *
306
+ * A server-forced nick change arrives as an incoming `update` with
307
+ * `object.type: "address"`. The stored credentials, connect, and join
308
+ * entries are keyed by the actor at send time, so left alone a reconnect
309
+ * would replay the old nick and open a second connection under an
310
+ * identity this one no longer holds.
311
+ *
312
+ * Only entries for the renamed actor on that platform move. The maps hold
313
+ * only this client's own actors, and `rememberRequestedRename` keeps them
314
+ * on the nick this connection currently holds, so a rename of some other
315
+ * user (including one who later took a nick this client released) never
316
+ * matches a stored entry.
317
+ *
318
+ * A rejected nick change is not a rename. The server echoes the failed
319
+ * job — the original update plus `error` — to every other session sharing
320
+ * the connection. Following that echo would point those sessions at a
321
+ * nick the server refused, so their next command misses the live worker
322
+ * and opens a second connection.
323
+ */
324
+ private followActorRename;
325
+ /**
326
+ * Moves the replay maps when a nick change this client requested is
327
+ * acknowledged. The platform consumes the server's confirmation as the
328
+ * job's completion rather than delivering it as a message, so without
329
+ * this the maps would stay on the released nick: a reconnect would
330
+ * replay it, and whoever took it in the meantime could steer where
331
+ * `followActorRename` moves this client's state.
332
+ */
333
+ private rememberRequestedRename;
334
+ private moveReplayState;
335
+ /**
336
+ * Replays previously sent events to the server after reconnection.
337
+ *
338
+ * This method is called automatically when the Socket.IO connection is
339
+ * re-established after a transient network interruption. It resends
340
+ * credentials and connection state so the user doesn't need to manually
341
+ * re-authenticate or rejoin channels.
342
+ *
343
+ * Security considerations:
344
+ * - Only replays events stored in memory during this session
345
+ * - Does not replay after page refresh (memory cleared)
346
+ * - Server should validate replayed credentials (may be stale/revoked)
347
+ *
348
+ * @param name - Event name to emit ("credentials", "message")
349
+ * @param asMap - Map of events to replay
350
+ */
351
+ private replay;
352
+ }