@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.
package/README.md CHANGED
@@ -1,77 +1,209 @@
1
1
  # @sockethub/client
2
2
 
3
- JavaScript client for [Sockethub](https://github.com/sockethub/sockethub) - a protocol
4
- gateway that translates ActivityStreams messages into various protocols (XMPP, IRC, RSS, etc.).
5
-
6
- Works in Node.js and browsers.
3
+ The Sockethub client is a small JavaScript SDK for app developers to connect to a
4
+ Sockethub server via Socket.IO and send/receive ActivityStreams messages. It works
5
+ in both Node.js and browsers, with schema-driven validation, automatic
6
+ reconnection, and credential replay.
7
+
8
+ ## What's Included
9
+
10
+ - `SockethubClient` for connection and message handling
11
+ - `SockethubClient.connect(baseUrl)` discovers the server's Socket.IO endpoint
12
+ from its service descriptor, so apps need only the base URL
13
+ - Schema-driven validation of outbound ActivityStreams messages (via `@sockethub/schemas`)
14
+ - `contextFor(platform)` builds canonical `@context` arrays from server metadata
15
+ - `ready()` promise and `ready`/`init_error` observability events
16
+ - Automatic outbound queuing until initialization is complete
17
+ - Auto-replay of credentials and connections on reconnect
18
+ - A browser-ready bundle in `dist/`
19
+
20
+ ## How It Works
21
+
22
+ `SockethubClient` wraps a [Socket.IO](https://socket.io) connection and manages
23
+ everything between your app and the Sockethub server:
24
+
25
+ 1. **Schema registry handshake.** On connect (and every reconnect) the client
26
+ requests the server's *platform schema registry* — the contexts, versions,
27
+ and JSON schemas for each platform the server has loaded. It caches that
28
+ registry along with a content fingerprint the server provides. On reconnect
29
+ the client echoes the fingerprint; if nothing has changed the server replies
30
+ `unchanged` instead of re-sending the full schema set, so the (potentially
31
+ large) registry crosses the wire only when it actually changes.
32
+ 2. **Initialization lifecycle.** `ready()` resolves once the registry is
33
+ applied; `getInitState()` and the `ready` / `init_error` events expose the
34
+ same lifecycle. Outbound events emitted before the client is ready are queued
35
+ in memory and flushed automatically once initialization completes.
36
+ 3. **Message handling.** Outgoing `message`/`credentials` events are normalized
37
+ (string `actor`/`target` references are expanded to objects) and validated
38
+ against the originating platform's schema before they leave the client;
39
+ incoming platform events are normalized before being handed to your
40
+ `message` listener. Validation uses the same schemas the server enforces, so
41
+ malformed activities are caught locally rather than round-tripping.
42
+ 4. **Context composition.** `contextFor(platform)` builds the canonical
43
+ `@context` array for a platform from the cached registry.
44
+ 5. **Reconnect resilience.** Credentials, platform connections, and room joins
45
+ are kept in memory and replayed when the socket reconnects (see
46
+ [Security & State Management](#security--state-management)).
7
47
 
8
48
  ## Install
9
49
 
10
50
  ### Node.js
11
51
 
12
- `$ npm install @sockethub/client`
52
+ `$ npm install @sockethub/client socket.io-client`
13
53
 
14
54
  ### Bun
15
55
 
16
- `$ bun install @sockethub/client`
17
-
18
- #### CommonJS
19
-
20
- ```javascript
21
- const SockethubClient = require('@sockethub/client');
22
- const io = require('@socket.io-client');
23
- const SOCKETHUB_SERVER = 'http://localhost:10550';
24
- const sc = SockethubClient(io(SOCKETHUB_SERVER));
25
- ```
26
-
27
- #### ESM
56
+ `$ bun install @sockethub/client socket.io-client`
28
57
 
29
58
  ```javascript
30
59
  import SockethubClient from '@sockethub/client';
31
- import { io } from '@socket.io-client';
32
- const SOCKETHUB_SERVER = 'http://localhost:10550';
33
- const sc = SockethubClient(io(SOCKETHUB_SERVER));
60
+ const sc = await SockethubClient.connect('http://localhost:10550');
34
61
  ```
35
62
 
63
+ `socket.io-client` is an optional peer dependency: `connect()` imports it when
64
+ no `io` global is present, and the constructor accepts a socket you created
65
+ yourself.
66
+
36
67
  ### Browser
37
68
 
38
- The browser bundle is available in the dist folder:
69
+ Two browser builds are published in `dist/`:
39
70
 
40
- ```
41
- import '@sockethub/client/dist/sockethub-client.js';
42
- ```
71
+ - `dist/sockethub-client.browser.js` is the IIFE/global build for plain `<script>` tags
72
+ - `dist/sockethub-client.js` is the ESM build for `<script type="module">`
43
73
 
44
- You can place it somewhere accessible from the web and include
45
- it via a `script` tag.
74
+ If you are serving assets from a Sockethub server, use the pre-copied global build
75
+ at `/sockethub-client.js` along with `/socket.io.js`:
46
76
 
47
- ```
48
- <script src="http://example.com/sockethub-client.js" type="module"></script>
77
+ ```html
78
+ <script src="/socket.io.js"></script>
79
+ <script src="/sockethub-client.js"></script>
49
80
  ```
50
81
 
51
- Once included in a web-page, the `SockethubClient` base object
52
- should be on the global scope.
82
+ These scripts set `io` and `SockethubClient` as globals.
83
+
84
+ If you are hosting the package files yourself and want ESM instead, serve
85
+ `dist/sockethub-client.js` and import it from a module script:
86
+
87
+ ```html
88
+ <script type="module">
89
+ import SockethubClient from '/dist/sockethub-client.js';
90
+ import { io } from '/socket.io.esm.min.js';
91
+
92
+ const sc = new SockethubClient(io('http://localhost:10550', { path: '/sockethub' }));
93
+ </script>
94
+ ```
53
95
 
54
96
  ## Quick Start
55
97
 
56
98
  ```javascript
57
99
  import SockethubClient from '@sockethub/client';
58
- import { io } from 'socket.io-client';
59
100
 
60
- const socket = io('http://localhost:10550', { path: '/sockethub' });
61
- const sc = new SockethubClient(socket);
101
+ // Discover the Socket.IO endpoint from the server's base URL. The fetched
102
+ // service descriptor (API versions, platforms, HTTP actions URL) is kept as
103
+ // sc.descriptor. To supply your own socket instead:
104
+ // new SockethubClient(io('http://localhost:10550', { path: '/sockethub' }))
105
+ const sc = await SockethubClient.connect('http://localhost:10550', {
106
+ initTimeoutMs: 5000,
107
+ });
62
108
 
63
109
  sc.socket.on('message', (msg) => console.log(msg));
110
+ sc.socket.on('ready', (info) => {
111
+ console.log('Sockethub ready:', info.reason, `API v${info.apiVersion}`,
112
+ info.platforms.map((p) => ({ id: p.id, apiVersion: p.apiVersion })));
113
+ });
114
+ sc.socket.on('init_error', (e) => {
115
+ console.warn('Sockethub init issue:', e.error);
116
+ });
117
+
118
+ // Wait for schema registry, then send a message
119
+ try {
120
+ await sc.ready();
121
+ sc.socket.emit('message', {
122
+ '@context': sc.contextFor('dummy'),
123
+ type: 'echo',
124
+ actor: { id: 'test@dummy', type: 'person' },
125
+ object: { type: 'message', content: 'hello world' }
126
+ }, (ack) => {
127
+ if (ack?.error) {
128
+ console.error('Send failed:', ack.error);
129
+ return;
130
+ }
131
+ console.log('Ack:', ack);
132
+ });
133
+ } catch (err) {
134
+ console.error('Sockethub failed to initialize:', err);
135
+ }
64
136
  ```
65
137
 
66
138
  See the [Client Guide](../../docs/client-guide.md) for detailed usage and examples.
67
139
 
68
140
  ## API
69
141
 
70
- - **`new SockethubClient(socket)`** - Create client instance
71
- - **`sc.socket.emit(event, data)`** - Send messages
142
+ - **`new SockethubClient(socket, options?)`** - Create client instance with optional init/queue settings
143
+ - **`sc.ready(timeoutMs?)`** - Wait for initialization to complete
144
+ - **`sc.isReady()`** - Check whether client is initialized
145
+ - **`sc.getInitState()`** - Return `"idle" | "initializing" | "ready" | "init_error" | "closed"`
146
+ - **`sc.contextFor(platform)`** - Build canonical `@context` for a platform from server registry
147
+ - **`sc.getRegisteredPlatforms()`** - Get server-registered platforms/contexts
148
+ - **`sc.getRegisteredBaseContexts()`** - Get AS2 and Sockethub base context URLs
149
+ - **`sc.getPlatformSchema(platform, schemaType?)`** - Get platform JSON schema
150
+ - **`sc.validateActivity(activity)`** - Validate against registered schemas
151
+ - **`sc.socket.emit(event, data, callback)`** - Send messages (queued until ready)
72
152
  - **`sc.socket.on(event, handler)`** - Listen for messages
73
153
  - **`sc.clearCredentials()`** - Clear stored credentials
74
- - **`sc.ActivityStreams`** - ActivityStreams library
154
+
155
+ Send a full `actor` object on each `credentials` and `message` event.
156
+
157
+ ## Result Handling
158
+
159
+ For client-initiated requests, pass a callback to `emit` and treat an `error`
160
+ field as failure:
161
+
162
+ ```javascript
163
+ sc.socket.emit("message", {
164
+ type: "echo",
165
+ "@context": sc.contextFor("dummy"),
166
+ actor: { id: "test", type: "person" },
167
+ object: { type: "message", content: "Hello!" },
168
+ }, (result) => {
169
+ if (result?.error) {
170
+ console.error("Sockethub request failed:", result.error);
171
+ return;
172
+ }
173
+
174
+ console.log("Sockethub request succeeded:", result);
175
+ });
176
+ ```
177
+
178
+ Also listen for ongoing platform events:
179
+
180
+ ```javascript
181
+ sc.socket.on("message", (msg) => {
182
+ console.log("incoming platform event:", msg);
183
+ });
184
+ ```
185
+
186
+ ## Sending messages
187
+
188
+ Include `id`, `type`, and usually `name` on the `actor` for `credentials` and
189
+ `message` events. Before sending, the client validates each outgoing activity
190
+ against the originating platform's schema from the server registry, surfacing a
191
+ client-side error instead of dispatching a malformed message.
192
+
193
+ ```javascript
194
+ sc.socket.emit("message", {
195
+ type: "join",
196
+ "@context": sc.contextFor("irc"),
197
+ actor: { id: "mynick@irc.libera.chat", type: "person", name: "mynick" },
198
+ target: {
199
+ id: "#sockethub@irc.libera.chat",
200
+ type: "room",
201
+ name: "#sockethub",
202
+ },
203
+ }, (result) => {
204
+ if (result?.error) console.error(result.error);
205
+ });
206
+ ```
75
207
 
76
208
  ## Security & State Management
77
209
 
@@ -83,10 +215,14 @@ memory and replaying it when the connection is re-established.
83
215
  #### What Gets Stored
84
216
 
85
217
  - Credentials (passwords, tokens, API keys)
86
- - Actor definitions
87
218
  - Platform connections
88
219
  - Channel/room joins
89
220
 
221
+ When one of your actors is renamed (an `update` with `object.type: "address"`, such as an IRC nick
222
+ change, whether you requested it and the server acknowledged it or the server reported it), the
223
+ stored entries move to the new actor so a reconnect replays the identity the connection currently
224
+ holds.
225
+
90
226
  #### Storage Location
91
227
 
92
228
  **All state is stored ONLY in JavaScript memory.** Nothing is persisted to:
@@ -0,0 +1,47 @@
1
+ interface PlatformRegistrySchemas {
2
+ credentials?: object;
3
+ messages?: object;
4
+ }
5
+ /**
6
+ * Server-declared platform metadata used by the client for context generation
7
+ * and runtime validation.
8
+ */
9
+ export interface PlatformRegistryEntry {
10
+ id: string;
11
+ apiVersion: number;
12
+ contextUrl: string;
13
+ contextVersion: string;
14
+ schemaVersion: string;
15
+ types: Array<string>;
16
+ schemas: PlatformRegistrySchemas;
17
+ }
18
+ export interface PlatformRegistryPayload {
19
+ apiVersion?: number;
20
+ fingerprint?: string;
21
+ unchanged?: boolean;
22
+ contexts?: {
23
+ as?: string;
24
+ sockethub?: string;
25
+ };
26
+ platforms?: Array<PlatformRegistryEntry>;
27
+ }
28
+ export interface ParsedPlatformRegistry {
29
+ apiVersion: number;
30
+ asContextUrl: string;
31
+ sockethubContextUrl: string;
32
+ platforms: Map<string, PlatformRegistryEntry>;
33
+ }
34
+ /**
35
+ * Validate a server `schemas` payload and rebuild it field by field, so
36
+ * nothing the server sends beyond the bootstrap contract (such as an exact
37
+ * package version) is retained or re-emitted. Returns undefined for a
38
+ * malformed payload; malformed platform entries are skipped.
39
+ */
40
+ export declare function parsePlatformRegistry(payload: unknown): ParsedPlatformRegistry | undefined;
41
+ /**
42
+ * Register a platform's context and schemas with the @sockethub/schemas
43
+ * validators so local validation uses the same canonical sources as the
44
+ * server.
45
+ */
46
+ export declare function registerPlatformSchemas(platform: PlatformRegistryEntry): void;
47
+ export {};
@@ -0,0 +1,30 @@
1
+ import type { ServiceDescriptor } from "@sockethub/schemas";
2
+ import type { ManagerOptions, Socket, SocketOptions } from "socket.io-client";
3
+ /** The `io()` factory exported by `socket.io-client`. */
4
+ export type SocketFactory = (uri: string, opts?: Partial<ManagerOptions & SocketOptions>) => Socket;
5
+ export interface DiscoverOptions {
6
+ /** Replacement for the global `fetch`, mainly for tests. */
7
+ fetch?: typeof fetch;
8
+ /** Abort discovery after this many milliseconds. Default 10000. */
9
+ discoveryTimeoutMs?: number;
10
+ }
11
+ /** Thrown when a server's descriptor cannot be fetched or is not usable. */
12
+ export declare class DiscoveryError extends Error {
13
+ constructor(message: string, options?: {
14
+ cause?: unknown;
15
+ });
16
+ }
17
+ /**
18
+ * Fetch and validate a Sockethub server's service descriptor from its base
19
+ * URL. Every failure rejects with a `DiscoveryError` that says what went
20
+ * wrong (unreachable, non-JSON, invalid descriptor).
21
+ */
22
+ export declare function discoverSockethub(baseUrl: string, options?: DiscoverOptions): Promise<ServiceDescriptor>;
23
+ /**
24
+ * Resolve an advertised endpoint path against the server origin, refusing any
25
+ * result that lands on another origin. The schema already rejects paths that
26
+ * URL resolution would read as protocol-relative; this is the belt to that
27
+ * brace for callers that build URLs from descriptor values.
28
+ */
29
+ export declare function resolveEndpoint(serverOrigin: string, path: string): string;
30
+ export declare function resolveSocketFactory(explicit?: SocketFactory): Promise<SocketFactory>;
@@ -0,0 +1,44 @@
1
+ import type { ActivityStream } from "@sockethub/schemas";
2
+ export interface EventMapping {
3
+ credentials: Map<string, ActivityStream>;
4
+ connect: Map<string, ActivityStream>;
5
+ join: Map<string, ActivityStream>;
6
+ }
7
+ /**
8
+ * True for the `{ error }` object the server acks a failed job with.
9
+ */
10
+ export declare function isErrorResult(value: unknown): boolean;
11
+ /**
12
+ * Type guard to check if an object is an ActivityStream with a valid actor.id.
13
+ */
14
+ export declare function hasActorId(obj: ActivityStream): obj is ActivityStream;
15
+ /**
16
+ * True for the activity that reports or requests a nick change: an
17
+ * `update` whose object is an `address`, with `actor` the old identity
18
+ * and `target` the new one.
19
+ */
20
+ export declare function isActorRename(activity: ActivityStream): boolean;
21
+ /**
22
+ * In-memory storage for client state that should be replayed on reconnection.
23
+ *
24
+ * Security: Stored ONLY in JavaScript heap memory. Never persisted to disk,
25
+ * localStorage, or any permanent storage. Cleared on page reload.
26
+ */
27
+ export declare class ReplayStore {
28
+ readonly events: EventMapping;
29
+ recordCredentials(content: ActivityStream): void;
30
+ /**
31
+ * Stores connect/join commands and drops them again on
32
+ * disconnect/leave, so a reconnect replays only what is still live.
33
+ */
34
+ recordMessage(content: ActivityStream): void;
35
+ /**
36
+ * Re-keys every stored entry for the rename's `actor` on its platform to
37
+ * its `target`. The credential object itself is left as the application
38
+ * sent it: the server keys the live connection on a fingerprint of that
39
+ * object, and the worker accepts the hash it was authorized with, so
40
+ * replaying the original object under the new actor is what lands back
41
+ * on the renamed connection. Returns the ids moved, for logging.
42
+ */
43
+ moveActor(rename: ActivityStream): Array<[string, string]>;
44
+ }