@sockethub/client 5.0.0-alpha.9 → 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.
- package/README.md +173 -37
- package/dist/client-registry.d.ts +47 -0
- package/dist/discovery.d.ts +30 -0
- package/dist/replay-store.d.ts +44 -0
- package/dist/sockethub-client.browser.js +18029 -0
- package/dist/sockethub-client.d.ts +345 -0
- package/dist/sockethub-client.js +17752 -343
- package/dist/sockethub-client.min.js +31 -18
- package/package.json +19 -10
- package/src/sockethub-client.test.ts +0 -325
- package/src/sockethub-client.ts +0 -331
|
@@ -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
|
+
}
|