@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 +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
package/README.md
CHANGED
|
@@ -1,77 +1,209 @@
|
|
|
1
1
|
# @sockethub/client
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
Two browser builds are published in `dist/`:
|
|
39
70
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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="
|
|
77
|
+
```html
|
|
78
|
+
<script src="/socket.io.js"></script>
|
|
79
|
+
<script src="/sockethub-client.js"></script>
|
|
49
80
|
```
|
|
50
81
|
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|