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