@sockethub/platform-xmpp 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.
package/README.md CHANGED
@@ -8,63 +8,311 @@ This platform provides XMPP client functionality, allowing web applications to c
8
8
  XMPP servers, send messages, manage contacts, join chat rooms, and handle presence
9
9
  updates through ActivityStreams messages.
10
10
 
11
- ## Implemented Verbs (`@type`)
11
+ ## Implemented Verbs (`type`)
12
12
 
13
+ * **connect** - Establish a connection to the XMPP server
14
+ * **disconnect** - Close the XMPP connection
15
+ * **join** - Join an XMPP chat room (MUC)
16
+ * **leave** - Leave an XMPP chat room
13
17
  * **send** - Send messages to contacts or chat rooms
14
- * **join** - Join XMPP chat rooms (MUCs)
15
- * **observe** - Subscribe to presence updates
16
- * **request-friend** - Send contact/buddy requests
17
- * **remove-friend** - Remove contacts from roster
18
- * **make-friend** - Accept contact requests
19
18
  * **update** - Update presence status and information
19
+ * **request-friend** - Send a contact/buddy request
20
+ * **remove-friend** - Remove a contact from the roster
21
+ * **make-friend** - Accept a contact request
22
+ * **query** - Query room attendance and room information via service discovery
23
+
24
+ ## Authentication
25
+
26
+ Credentials are sent to Sockethub via a `credentials` message before the
27
+ `connect` verb. The XMPP platform accepts a single `password` field.
28
+
29
+ Authentication requires an encrypted connection: direct TLS (`xmpps://`) or
30
+ a successful STARTTLS upgrade. If the server does not offer encryption, the
31
+ connection fails before credentials are sent.
32
+ This requirement also applies to automatic reconnections.
33
+
34
+ ### Credentials object
35
+
36
+ | Field | Type | Required | Description |
37
+ |---|---|---|---|
38
+ | `type` | string | yes | Must be `"credentials"`. |
39
+ | `userAddress` | string | yes | Bare JID, e.g. `user@jabber.net`. |
40
+ | `resource` | string | yes | XMPP resource identifier (e.g. `"phone"`). |
41
+ | `password` | string | yes | SASL password. Use this field for PLAIN-slot tokens too. |
42
+ | `server` | string | no | Overrides the host from `userAddress`. Host name or `xmpp(s)://` URI. |
43
+ | `port` | integer | no | Overrides the default port (1-65535). |
44
+
45
+ Without a `port`, a bare host is looked up through its `_xmpps-client` and
46
+ `_xmpp-client` SRV records, and its own addresses on ports 5223 (direct TLS)
47
+ and 5222. A bare host with a `port` skips SRV lookup and connects with
48
+ `xmpp://` (STARTTLS) on that port. For direct TLS, set `server` to an
49
+ `xmpps://` URI. The platform does not use HTTP host-meta discovery or the
50
+ WebSocket transport.
51
+
52
+ An internationalized host name (such as `bücher.example`) works only as a bare
53
+ host without a `port`. With a `port`, or inside an `xmpp(s)://` URI, it is
54
+ refused; use its ASCII form (`xn--bcher-kva.example`) instead.
55
+
56
+ ### Private addresses
57
+
58
+ The platform refuses to connect to loopback, private, link-local, and cloud
59
+ metadata addresses, using the same address rules as the
60
+ [outbound URL guard](../../docs/configuration.md#outbound-url-guard). To reach
61
+ an XMPP server on a private network, set `allowPrivateAddresses` in this
62
+ platform's `packageConfig` entry:
63
+
64
+ ```json
65
+ {
66
+ "packageConfig": {
67
+ "@sockethub/platform-xmpp": {
68
+ "allowPrivateAddresses": true
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ Do not enable it on a server that accepts untrusted clients. Any client could
75
+ then make Sockethub connect to internal services.
76
+
77
+ ### Password authentication
78
+
79
+ Use a standard XMPP account password. The server negotiates the strongest
80
+ available SASL mechanism (typically SCRAM-SHA-1, falling back to PLAIN).
81
+ The examples below assume you already have an initialized Sockethub client
82
+ instance named `sc`.
83
+
84
+ ```javascript
85
+ {
86
+ type: "credentials",
87
+ "@context": sc.contextFor("xmpp"),
88
+ actor: { id: "user@jabber.net", type: "person" },
89
+ object: {
90
+ type: "credentials",
91
+ userAddress: "user@jabber.net",
92
+ password: "s3cret",
93
+ resource: "phone"
94
+ }
95
+ }
96
+ ```
97
+
98
+ If your deployment expects a bearer-style token in the SASL PLAIN password
99
+ slot, pass that token string as `password`. This is only a narrow
100
+ compatibility mode. Dedicated token SASL mechanisms such as ejabberd
101
+ `X-OAUTH2`, Prosody `OAUTHBEARER`, Prosody community `X-TOKEN`, and SASL2 FAST
102
+ are not implemented by the bundled `@xmpp/client`, which only supports
103
+ `SCRAM-SHA-1`, `PLAIN`, and `ANONYMOUS` and prefers `SCRAM-SHA-1` when both
104
+ SCRAM and PLAIN are offered.
105
+
106
+ A failed authentication with either a traditional password or a token string
107
+ surfaces the same `SASLError: not-authorized` and is treated as a
108
+ non-recoverable connection error.
20
109
 
21
110
  ## Usage
22
111
 
112
+ ### Connect Example
113
+
114
+ ```javascript
115
+ {
116
+ type: "connect",
117
+ "@context": sc.contextFor("xmpp"),
118
+ actor: {
119
+ id: "user@example.org/web",
120
+ type: "person"
121
+ }
122
+ }
123
+ ```
124
+
23
125
  ### Send Message Example
24
126
 
25
- ```json
127
+ ```javascript
128
+ {
129
+ type: "send",
130
+ "@context": sc.contextFor("xmpp"),
131
+ actor: {
132
+ id: "user@example.org",
133
+ type: "person"
134
+ },
135
+ target: {
136
+ id: "friend@jabber.net",
137
+ type: "person"
138
+ },
139
+ object: {
140
+ type: "message",
141
+ content: "Hello from Sockethub!"
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### Send Room Message Example
147
+
148
+ ```javascript
26
149
  {
27
- "@type": "send",
28
- "context": "xmpp",
29
- "actor": {
30
- "@id": "user@example.org"
150
+ type: "send",
151
+ "@context": sc.contextFor("xmpp"),
152
+ actor: {
153
+ id: "user@example.org/web",
154
+ type: "person"
31
155
  },
32
- "target": {
33
- "@id": "friend@jabber.net"
156
+ target: {
157
+ id: "room@conference.example.org",
158
+ type: "room"
34
159
  },
35
- "object": {
36
- "@type": "Note",
37
- "content": "Hello from Sockethub!"
160
+ object: {
161
+ type: "message",
162
+ content: "Hello room"
163
+ }
164
+ }
165
+ ```
166
+
167
+ ### Update Presence Example
168
+
169
+ ```javascript
170
+ {
171
+ type: "update",
172
+ "@context": sc.contextFor("xmpp"),
173
+ actor: {
174
+ id: "user@example.org/web",
175
+ type: "person"
176
+ },
177
+ object: {
178
+ type: "presence",
179
+ presence: "away",
180
+ content: "Out for lunch"
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### Query Room Attendance Response Example
186
+
187
+ ```javascript
188
+ {
189
+ type: "query",
190
+ "@context": sc.contextFor("xmpp"),
191
+ actor: {
192
+ id: "room@conference.example.org",
193
+ type: "room"
194
+ },
195
+ target: {
196
+ id: "user@example.org/web",
197
+ type: "person"
198
+ },
199
+ object: {
200
+ type: "attendance",
201
+ members: ["alice", "bob", "carol"]
202
+ }
203
+ }
204
+ ```
205
+
206
+ ### Query Room Attendance Example
207
+
208
+ ```javascript
209
+ {
210
+ type: "query",
211
+ "@context": sc.contextFor("xmpp"),
212
+ actor: {
213
+ id: "user@example.org/web",
214
+ type: "person"
215
+ },
216
+ target: {
217
+ id: "room@conference.example.org",
218
+ type: "room"
219
+ },
220
+ object: {
221
+ type: "attendance"
222
+ }
223
+ }
224
+ ```
225
+
226
+ ### Query Room Information Example
227
+
228
+ ```javascript
229
+ {
230
+ type: "query",
231
+ "@context": sc.contextFor("xmpp"),
232
+ actor: {
233
+ id: "user@example.org/web",
234
+ type: "person"
235
+ },
236
+ target: {
237
+ id: "room@conference.example.org",
238
+ type: "room"
239
+ },
240
+ object: {
241
+ type: "room-info"
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### Query Room Information Response Example
247
+
248
+ ```javascript
249
+ {
250
+ type: "query",
251
+ "@context": sc.contextFor("xmpp"),
252
+ actor: {
253
+ id: "room@conference.example.org",
254
+ type: "room",
255
+ name: "Room Display Name"
256
+ },
257
+ target: {
258
+ id: "user@example.org/web",
259
+ type: "person"
260
+ },
261
+ object: {
262
+ type: "room-info",
263
+ features: ["http://jabber.org/protocol/muc", "muc_passwordprotected"],
264
+ identities: [{
265
+ category: "conference",
266
+ type: "text",
267
+ name: "Room Display Name"
268
+ }]
269
+ }
270
+ }
271
+ ```
272
+
273
+ ### Disconnect Example
274
+
275
+ ```javascript
276
+ {
277
+ type: "disconnect",
278
+ "@context": sc.contextFor("xmpp"),
279
+ actor: {
280
+ id: "user@example.org/web",
281
+ type: "person"
38
282
  }
39
283
  }
40
284
  ```
41
285
 
42
286
  ### Join Chat Room Example
43
287
 
44
- ```json
288
+ ```javascript
45
289
  {
46
- "@type": "join",
47
- "context": "xmpp",
48
- "actor": {
49
- "@id": "user@example.org"
290
+ type: "join",
291
+ "@context": sc.contextFor("xmpp"),
292
+ actor: {
293
+ id: "user@example.org",
294
+ type: "person"
50
295
  },
51
- "target": {
52
- "@id": "room@conference.example.org"
296
+ target: {
297
+ id: "room@conference.example.org",
298
+ type: "room"
53
299
  }
54
300
  }
55
301
  ```
56
302
 
57
303
  ### Request Friend Example
58
304
 
59
- ```json
305
+ ```javascript
60
306
  {
61
- "@type": "request-friend",
62
- "context": "xmpp",
63
- "actor": {
64
- "@id": "user@example.org"
307
+ type: "request-friend",
308
+ "@context": sc.contextFor("xmpp"),
309
+ actor: {
310
+ id: "user@example.org",
311
+ type: "person"
65
312
  },
66
- "target": {
67
- "@id": "friend@jabber.net"
313
+ target: {
314
+ id: "friend@jabber.net",
315
+ type: "person"
68
316
  }
69
317
  }
70
318
  ```
@@ -86,4 +334,5 @@ updates through ActivityStreams messages.
86
334
 
87
335
  ## API
88
336
 
89
- Detailed API documentation can be found [here](API.md)
337
+ API reference docs are generated from the source JSDoc and are not committed to this repository.
338
+ Run `bun run doc` in this package to generate them locally.
@@ -0,0 +1,16 @@
1
+ import type { XmppClientInstance } from "@xmpp/client";
2
+ /**
3
+ * Refuse XMPP connections to loopback, private, link-local, and cloud
4
+ * metadata addresses, matching the outbound guard used by the HTTP platforms.
5
+ *
6
+ * Every socket gets the shared guarded DNS lookup, which checks the address
7
+ * actually connected to. IP-literal hosts skip DNS, so they are checked
8
+ * directly; `@xmpp/resolve` turns SRV and A records into IP literals, so
9
+ * discovered candidates are checked here too. Transports that do not use a
10
+ * host/port socket (WebSocket) are refused outright. xmpp.js treats a
11
+ * refusal as "no transport" and skips that candidate.
12
+ *
13
+ * The wrapper lives on the shared module-level prototypes, like
14
+ * {@link setTlsServername}, and its setting is refreshed on every call.
15
+ */
16
+ export declare function guardXmppConnections(client: XmppClientInstance, allowPrivateAddresses: boolean): void;
@@ -0,0 +1,25 @@
1
+ import type { XmppElement } from "@xmpp/client";
2
+ import type { XmppHandlerSession } from "./types.js";
3
+ export declare class IncomingHandlers {
4
+ /** @internal Exposed to allow test access for null-handling validation. */
5
+ session: XmppHandlerSession | undefined;
6
+ constructor(session?: XmppHandlerSession);
7
+ close(): void;
8
+ error(err: {
9
+ text?: string;
10
+ condition?: string;
11
+ toString(): string;
12
+ }): void;
13
+ presence(stanza: XmppElement): void;
14
+ subscribe(to: {
15
+ id: string;
16
+ type: string;
17
+ }, from: string, name?: string): void;
18
+ notifyChatMessage(stanza: XmppElement): void;
19
+ notifyError(stanza: XmppElement): void;
20
+ notifyRoomAttendance(stanza: XmppElement): void;
21
+ notifyRoomInfo(stanza: XmppElement): void;
22
+ notifyRoomInfoError(stanza: XmppElement): void;
23
+ online(): void;
24
+ stanza(stanza: XmppElement): void;
25
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * This is a platform for Sockethub implementing XMPP functionality.
3
+ *
4
+ * Developed by Nick Jennings (https://github.com/silverbucket)
5
+ *
6
+ * Sockethub is licensed under the LGPLv3.
7
+ * See the LICENSE file for details.
8
+ *
9
+ * The latest version of this module can be found here:
10
+ * git://github.com/sockethub/sockethub.git
11
+ *
12
+ * For more information about Sockethub visit http://sockethub.org/.
13
+ *
14
+ * This program is distributed in the hope that it will be useful,
15
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17
+ */
18
+ import type { ActivityStream, Logger, PersistentPlatformConfig, PersistentPlatformInterface, PlatformCallback, PlatformSchemaStruct, PlatformSendToClient } from "@sockethub/schemas";
19
+ import { type XmppClientInstance, type XmppClientOptions, type XmppElement } from "@xmpp/client";
20
+ import type { XmppCredentialsObject, XmppPlatformSession } from "./types.js";
21
+ export type { XmppCredentialsObject, XmppPlatformSession } from "./types.js";
22
+ /**
23
+ * xmpp.js never sets TLS SNI, and `@xmpp/resolve` connects to raw IPs from
24
+ * SRV/A lookups, so direct TLS (`xmpps://`) is validated against the IP and
25
+ * fails (ERR_TLS_CERT_ALTNAME_INVALID). Both connect paths call
26
+ * `Transport.prototype.socketParameters(uri)`, so wrapping the prototypes
27
+ * covers them.
28
+ *
29
+ * The transport classes are shared module-level prototypes, so the wrapper is
30
+ * installed once per prototype and keeps the active domain in prototype state.
31
+ * That state is refreshed on every call: a later client for a different JID
32
+ * domain must not reuse the first wrapper's servername, and an IP-literal
33
+ * domain must send no SNI at all. Remove once xmpp.js sets `servername`
34
+ * upstream (xmppjs/xmpp.js#1128) and `@xmpp/*` is bumped.
35
+ */
36
+ export declare function setTlsServername(client: XmppClientInstance, domain: string): void;
37
+ /**
38
+ * Handles all actions related to communication via. the XMPP protocol.
39
+ *
40
+ * Uses `xmpp.js` as a base tool for interacting with XMPP.
41
+ *
42
+ * {@link https://github.com/xmppjs/xmpp.js}
43
+ */
44
+ export default class XMPP implements PersistentPlatformInterface {
45
+ credentialsHash: string | undefined;
46
+ readonly config: PersistentPlatformConfig;
47
+ readonly log: Logger;
48
+ readonly sendToClient: PlatformSendToClient;
49
+ /** @internal Set of bare JIDs for rooms this client has joined. */
50
+ __knownRooms: Set<string>;
51
+ /** @internal Bare JID of the authenticated account, used by IncomingHandlers. */
52
+ __bareJid: string | undefined;
53
+ protected __clientConstructor: (options: XmppClientOptions) => XmppClientInstance;
54
+ protected __xml: (name: string, attrs?: Record<string, string | undefined>, ...children: (XmppElement | undefined)[]) => XmppElement;
55
+ private readonly id;
56
+ private __initialized;
57
+ /** @internal Exposed for test access and IncomingHandlers. */
58
+ __client: XmppClientInstance | undefined;
59
+ /**
60
+ * Constructor called from the Sockethub `Platform` instance, passing in a
61
+ * session object.
62
+ * @param session - {@link XmppPlatformSession}
63
+ */
64
+ constructor(session: XmppPlatformSession);
65
+ protected createClient(): void;
66
+ protected createXml(): void;
67
+ /**
68
+ * Mark the platform as disconnected and uninitialized
69
+ * @param stopReconnection - If true, stop automatic reconnection
70
+ */
71
+ private __markDisconnected;
72
+ /**
73
+ * Classify error to determine if reconnection should be attempted
74
+ * @param err - The error from XMPP client
75
+ * @returns 'RECOVERABLE' or 'NON_RECOVERABLE'
76
+ */
77
+ private __classifyError;
78
+ /**
79
+ * Check if the XMPP client is properly connected and can send messages
80
+ * @returns true if client is connected and operational
81
+ */
82
+ private __isClientConnected;
83
+ /**
84
+ * @description
85
+ * JSON schema defining the types this platform accepts.
86
+ *
87
+ * Deployments that accept a bearer-style token in the SASL PLAIN password
88
+ * slot should still pass that token string as `password`. Dedicated token
89
+ * SASL mechanisms such as ejabberd `X-OAUTH2`, Prosody `OAUTHBEARER`,
90
+ * Prosody community `X-TOKEN`, and SASL2 FAST are not implemented by this
91
+ * client. See the package README for canonical credentials and usage
92
+ * payload examples.
93
+ **/
94
+ get schema(): PlatformSchemaStruct;
95
+ /**
96
+ * Returns whether the platform is ready to handle jobs.
97
+ * For XMPP, this means we have successfully connected to the server.
98
+ * During temporary network interruptions with automatic reconnection,
99
+ * remains true to allow queued jobs to retry rather than fail.
100
+ * @returns true if ready to handle jobs
101
+ */
102
+ isInitialized(): boolean;
103
+ /**
104
+ * Connect to the XMPP server.
105
+ *
106
+ * @param job - activity streams object
107
+ * @param credentials - credentials object
108
+ * @param done - callback when job is done
109
+ */
110
+ connect(job: ActivityStream, credentials: XmppCredentialsObject, done: PlatformCallback): void;
111
+ /**
112
+ * Join a room, optionally defining a display name for that room.
113
+ *
114
+ * @param job - activity streams object
115
+ * @param done - callback when job is done
116
+ */
117
+ join(job: ActivityStream, done: PlatformCallback): Promise<void>;
118
+ /**
119
+ * Leave a room
120
+ *
121
+ * @param job - activity streams object
122
+ * @param done - callback when job is done
123
+ */
124
+ leave(job: ActivityStream, done: PlatformCallback): void;
125
+ /**
126
+ * Send a message to a room or private conversation.
127
+ *
128
+ * @param job - activity streams object
129
+ * @param done - callback when job is done
130
+ */
131
+ send(job: ActivityStream, done: PlatformCallback): void;
132
+ /**
133
+ * @description
134
+ * Indicate presence and status message.
135
+ * Valid presence values are "away", "chat", "dnd", "xa", "offline", "online".
136
+ *
137
+ * @param job - activity streams object
138
+ * @param done - callback when job is done
139
+ */
140
+ update(job: ActivityStream, done: PlatformCallback): void;
141
+ /**
142
+ * @description
143
+ * Send friend request
144
+ *
145
+ * @param job - activity streams object
146
+ * @param done - callback when job is done
147
+ */
148
+ "request-friend"(job: ActivityStream, done: PlatformCallback): void;
149
+ /**
150
+ * @description
151
+ * Send a remove friend request
152
+ *
153
+ * @param job - activity streams object
154
+ * @param done - callback when job is done
155
+ */
156
+ "remove-friend"(job: ActivityStream, done: PlatformCallback): void;
157
+ /**
158
+ * @description
159
+ * Confirm a friend request
160
+ *
161
+ * @param job - activity streams object
162
+ * @param done - callback when job is done
163
+ */
164
+ "make-friend"(job: ActivityStream, done: PlatformCallback): void;
165
+ /**
166
+ * Indicate an intent to query something (e.g. get room attendance or room information).
167
+ *
168
+ * @param job - activity streams object
169
+ * @param done - callback when job is done
170
+ */
171
+ query(job: ActivityStream, done: PlatformCallback): void;
172
+ /**
173
+ * Disconnect XMPP client
174
+ * @param _job - activity streams object
175
+ * @param done - callback when done
176
+ */
177
+ disconnect(_job: ActivityStream, done: PlatformCallback): void;
178
+ /**
179
+ * Called when it's time to close any connections or clean data before being wiped
180
+ * forcefully.
181
+ * @param done - callback when complete
182
+ */
183
+ cleanup(done: PlatformCallback): void;
184
+ private __registerHandlers;
185
+ }