@sockethub/server 5.0.0-alpha.10

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.
Files changed (46) hide show
  1. package/LICENSE +165 -0
  2. package/README.md +130 -0
  3. package/bin/sockethub +4 -0
  4. package/dist/defaults.json +36 -0
  5. package/dist/index.js +166465 -0
  6. package/dist/index.js.map +1877 -0
  7. package/dist/platform.js +103625 -0
  8. package/dist/platform.js.map +1435 -0
  9. package/package.json +100 -0
  10. package/res/socket.io.js +4908 -0
  11. package/res/sockethub-client.js +631 -0
  12. package/res/sockethub-client.min.js +19 -0
  13. package/src/bootstrap/init.d.ts +21 -0
  14. package/src/bootstrap/init.test.ts +211 -0
  15. package/src/bootstrap/init.ts +160 -0
  16. package/src/bootstrap/load-platforms.ts +151 -0
  17. package/src/config.test.ts +33 -0
  18. package/src/config.ts +98 -0
  19. package/src/defaults.json +36 -0
  20. package/src/index.ts +68 -0
  21. package/src/janitor.test.ts +211 -0
  22. package/src/janitor.ts +157 -0
  23. package/src/listener.ts +173 -0
  24. package/src/middleware/create-activity-object.test.ts +30 -0
  25. package/src/middleware/create-activity-object.ts +22 -0
  26. package/src/middleware/expand-activity-stream.test.data.ts +351 -0
  27. package/src/middleware/expand-activity-stream.test.ts +77 -0
  28. package/src/middleware/expand-activity-stream.ts +37 -0
  29. package/src/middleware/store-credentials.test.ts +85 -0
  30. package/src/middleware/store-credentials.ts +16 -0
  31. package/src/middleware/validate.test.data.ts +259 -0
  32. package/src/middleware/validate.test.ts +44 -0
  33. package/src/middleware/validate.ts +73 -0
  34. package/src/middleware.test.ts +184 -0
  35. package/src/middleware.ts +71 -0
  36. package/src/platform-instance.test.ts +531 -0
  37. package/src/platform-instance.ts +360 -0
  38. package/src/platform.test.ts +375 -0
  39. package/src/platform.ts +358 -0
  40. package/src/process-manager.ts +88 -0
  41. package/src/routes.test.ts +54 -0
  42. package/src/routes.ts +61 -0
  43. package/src/sentry.test.ts +106 -0
  44. package/src/sentry.ts +19 -0
  45. package/src/sockethub.ts +198 -0
  46. package/src/util.ts +5 -0
@@ -0,0 +1,531 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import * as sinon from "sinon";
3
+ import { __dirname } from "./util.js";
4
+ const FORK_PATH = __dirname + "/platform.js";
5
+
6
+ import PlatformInstance, { platformInstances } from "./platform-instance.js";
7
+
8
+ describe("PlatformInstance", () => {
9
+ let pi, sandbox, forkFake, socketMock, getSocketFake;
10
+
11
+ beforeEach(() => {
12
+ sandbox = sinon.createSandbox();
13
+ socketMock = {
14
+ emit: sandbox.spy(),
15
+ };
16
+ getSocketFake = sinon.fake.resolves(socketMock);
17
+ forkFake = sandbox.fake();
18
+ });
19
+
20
+ function getTestPlatformInstanceClass() {
21
+ class TestPlatformInstance extends PlatformInstance {
22
+ createQueue() {
23
+ this.JobQueue = sandbox.stub().returns({
24
+ shutdown: sandbox.stub(),
25
+ on: sandbox.stub(),
26
+ getJob: sandbox.stub(),
27
+ initResultEvents: sandbox.stub(),
28
+ });
29
+ }
30
+
31
+ initProcess(parentId, name, id, env) {
32
+ this.process = forkFake(FORK_PATH, [parentId, name, id], env);
33
+ }
34
+
35
+ createGetSocket() {
36
+ this.getSocket = getSocketFake;
37
+ }
38
+ }
39
+ return TestPlatformInstance;
40
+ }
41
+
42
+ afterEach(() => {
43
+ sinon.restore();
44
+ });
45
+
46
+ describe("private instance per-actor", () => {
47
+ it("is set as non-global when an actor is provided", async () => {
48
+ const TestPlatformInstance = getTestPlatformInstanceClass();
49
+ const pi = new TestPlatformInstance({
50
+ identifier: "id",
51
+ platform: "name",
52
+ parentId: "parentId",
53
+ actor: "actor string",
54
+ });
55
+ expect(pi.global).toEqual(false);
56
+ sandbox.assert.calledWith(forkFake, FORK_PATH, [
57
+ "parentId",
58
+ "name",
59
+ "id",
60
+ ]);
61
+ await pi.shutdown();
62
+ });
63
+ });
64
+
65
+ describe("PlatformInstance objects", () => {
66
+ beforeEach(() => {
67
+ const TestPlatformInstance = getTestPlatformInstanceClass();
68
+ pi = new TestPlatformInstance({
69
+ identifier: "platform identifier",
70
+ platform: "a platform name",
71
+ parentId: "the parentId",
72
+ });
73
+ platformInstances.set(pi.id, pi);
74
+
75
+ pi.process = {
76
+ on: sandbox.spy(),
77
+ removeListener: sandbox.spy(),
78
+ removeAllListeners: sandbox.spy(),
79
+ unref: sandbox.spy(),
80
+ kill: sandbox.spy(),
81
+ };
82
+ });
83
+
84
+ afterEach(async () => {
85
+ await pi.shutdown();
86
+ });
87
+
88
+ it("has expected properties", () => {
89
+ const TestPlatformInstance = getTestPlatformInstanceClass();
90
+ expect(typeof TestPlatformInstance).toEqual("function");
91
+ });
92
+
93
+ it("should have a platformInstances Map", () => {
94
+ expect(platformInstances instanceof Map).toEqual(true);
95
+ });
96
+
97
+ it("has certain accessible properties", () => {
98
+ expect(pi.id).toEqual("platform identifier");
99
+ expect(pi.name).toEqual("a platform name");
100
+ expect(pi.parentId).toEqual("the parentId");
101
+ expect(pi.flaggedForTermination).toEqual(false);
102
+ expect(pi.global).toEqual(true);
103
+ expect(
104
+ forkFake.calledWith(FORK_PATH, [
105
+ "the parentId",
106
+ "a platform name",
107
+ "platform identifier",
108
+ ]),
109
+ ).toEqual(true);
110
+ });
111
+
112
+ describe("registerSession", () => {
113
+ beforeEach(() => {
114
+ pi.callbackFunction = sandbox.fake();
115
+ });
116
+
117
+ it("adds a close and message handler when a session is registered", () => {
118
+ pi.registerSession("my session id");
119
+ expect(pi.callbackFunction.callCount).toEqual(2);
120
+ sandbox.assert.calledWith(
121
+ pi.callbackFunction,
122
+ "close",
123
+ "my session id",
124
+ );
125
+ sandbox.assert.calledWith(
126
+ pi.callbackFunction,
127
+ "message",
128
+ "my session id",
129
+ );
130
+ expect(pi.sessions.has("my session id")).toEqual(true);
131
+ });
132
+
133
+ it("is able to generate failure reports", async () => {
134
+ pi.registerSession("my session id");
135
+ expect(pi.sessions.has("my session id")).toEqual(true);
136
+ pi.sendToClient = sandbox.stub();
137
+ pi.shutdown = sandbox.stub();
138
+ await pi.reportError("my session id", "an error message");
139
+ expect(pi.sessions.size).toEqual(0);
140
+ });
141
+ });
142
+
143
+ it("initializes the job queue", () => {
144
+ expect(pi.queue).toBeUndefined();
145
+ pi.initQueue("a secret");
146
+ expect(pi.queue).toBeDefined();
147
+ });
148
+
149
+ it("cleans up its references when shutdown", async () => {
150
+ pi.initQueue("a secret");
151
+ expect(pi.queue).toBeDefined();
152
+ expect(platformInstances.has("platform identifier")).toBeTrue();
153
+ await pi.shutdown();
154
+ expect(pi.queue).toBeUndefined();
155
+ expect(platformInstances.has("platform identifier")).toBeFalse();
156
+ });
157
+
158
+ it("updates its identifier when changed", () => {
159
+ pi.updateIdentifier("foo bar");
160
+ expect(pi.id).toEqual("foo bar");
161
+ expect(platformInstances.has("platform identifier")).toBeFalse();
162
+ expect(platformInstances.has("foo bar")).toBeTrue();
163
+ });
164
+
165
+ it("sends messages to client using socket session id", async () => {
166
+ await pi.sendToClient("my session id", {
167
+ foo: "this is a message object",
168
+ sessionSecret: "private data",
169
+ });
170
+ expect(getSocketFake.callCount).toEqual(1);
171
+ sandbox.assert.calledOnce(getSocketFake);
172
+ sandbox.assert.calledWith(getSocketFake, "my session id");
173
+ sandbox.assert.calledOnce(socketMock.emit);
174
+ sandbox.assert.calledWith(socketMock.emit, "message", {
175
+ foo: "this is a message object",
176
+ context: "a platform name",
177
+ });
178
+ });
179
+
180
+ it("broadcasts to peers", async () => {
181
+ pi.sessions.add("other peer");
182
+ pi.sessions.add("another peer");
183
+ await pi.broadcastToSharedPeers("myself", { foo: "bar" });
184
+ expect(getSocketFake.callCount).toEqual(2);
185
+ sandbox.assert.calledWith(getSocketFake, "other peer");
186
+ });
187
+
188
+ describe("handleJobResult", () => {
189
+ beforeEach(() => {
190
+ pi.sendToClient = sandbox.fake();
191
+ pi.broadcastToSharedPeers = sandbox.fake();
192
+ pi.config = { persist: false };
193
+ });
194
+
195
+ it("broadcasts to peers when handling a completed job", async () => {
196
+ pi.sessions.add("other peer");
197
+ await pi.handleJobResult(
198
+ "completed",
199
+ { msg: { foo: "bar" } },
200
+ undefined,
201
+ );
202
+ expect(pi.sendToClient.callCount).toEqual(1);
203
+ expect(pi.broadcastToSharedPeers.callCount).toEqual(1);
204
+ });
205
+
206
+ it("appends completed result message when present", async () => {
207
+ await pi.handleJobResult(
208
+ "completed",
209
+ { sessionId: "a session id", msg: { foo: "bar" } },
210
+ "a good result message",
211
+ );
212
+ expect(pi.broadcastToSharedPeers.callCount).toEqual(1);
213
+ sandbox.assert.calledWith(pi.sendToClient, "a session id", {
214
+ foo: "bar",
215
+ });
216
+ });
217
+
218
+ it("appends failed result message when present", async () => {
219
+ await pi.handleJobResult(
220
+ "failed",
221
+ { sessionId: "a session id", msg: { foo: "bar" } },
222
+ "a bad result message",
223
+ );
224
+ expect(pi.broadcastToSharedPeers.callCount).toEqual(1);
225
+ sandbox.assert.calledWith(pi.sendToClient, "a session id", {
226
+ foo: "bar",
227
+ error: "a bad result message",
228
+ });
229
+ });
230
+ });
231
+
232
+ describe("callbackFunction", () => {
233
+ beforeEach(() => {
234
+ pi.reportError = sandbox.fake();
235
+ pi.sendToClient = sandbox.fake();
236
+ pi.updateIdentifier = sandbox.fake();
237
+ });
238
+
239
+ it("close events from platform thread are reported", async () => {
240
+ // Mock process as connected and not flagged for termination
241
+ pi.process.connected = true;
242
+ pi.flaggedForTermination = false;
243
+
244
+ const close = pi.callbackFunction("close", "my session id");
245
+ await close("error msg");
246
+ sandbox.assert.calledWith(
247
+ pi.reportError,
248
+ "my session id",
249
+ "Error: session thread closed unexpectedly: error msg",
250
+ );
251
+ });
252
+
253
+ it("close events skip error reporting when process disconnected", async () => {
254
+ // Mock process as disconnected
255
+ pi.process.connected = false;
256
+ pi.flaggedForTermination = false;
257
+ pi.shutdown = sandbox.stub();
258
+
259
+ const close = pi.callbackFunction("close", "my session id");
260
+ await close("error msg");
261
+
262
+ // Should NOT attempt to report error
263
+ sandbox.assert.notCalled(pi.reportError);
264
+ // Should call shutdown
265
+ sandbox.assert.called(pi.shutdown);
266
+ });
267
+
268
+ it("close events skip error reporting when flagged for termination", async () => {
269
+ // Mock process as flagged for termination
270
+ pi.process.connected = true;
271
+ pi.flaggedForTermination = true;
272
+ pi.shutdown = sandbox.stub();
273
+
274
+ const close = pi.callbackFunction("close", "my session id");
275
+ await close("error msg");
276
+
277
+ // Should NOT attempt to report error
278
+ sandbox.assert.notCalled(pi.reportError);
279
+ // Should call shutdown
280
+ sandbox.assert.called(pi.shutdown);
281
+ });
282
+
283
+ it("message events from platform thread are route based on command: error", () => {
284
+ const message = pi.callbackFunction("message", "my session id");
285
+ message(["error", "error message"]);
286
+ sandbox.assert.calledWith(
287
+ pi.reportError,
288
+ "my session id",
289
+ "error message",
290
+ );
291
+ });
292
+
293
+ it("message events from platform thread are route based on command: updateActor", () => {
294
+ const message = pi.callbackFunction("message", "my session id");
295
+ message(["updateActor", undefined, { foo: "bar" }]);
296
+ sandbox.assert.calledWith(pi.updateIdentifier, { foo: "bar" });
297
+ });
298
+
299
+ it("message events from platform thread are route based on command: else", () => {
300
+ const message = pi.callbackFunction("message", "my session id");
301
+ message(["blah", { foo: "bar" }]);
302
+ sandbox.assert.calledWith(pi.sendToClient, "my session id", {
303
+ foo: "bar",
304
+ });
305
+ });
306
+ });
307
+ });
308
+
309
+ describe("credential failure handling", () => {
310
+ let queueMock: any;
311
+ let processMock: any;
312
+
313
+ beforeEach(() => {
314
+ queueMock = {
315
+ pause: sandbox.stub().resolves(),
316
+ resume: sandbox.stub().resolves(),
317
+ shutdown: sandbox.stub().resolves(),
318
+ on: sandbox.stub(),
319
+ getJob: sandbox.stub(),
320
+ initResultEvents: sandbox.stub(),
321
+ };
322
+
323
+ processMock = {
324
+ removeAllListeners: sandbox.stub(),
325
+ unref: sandbox.stub(),
326
+ kill: sandbox.stub(),
327
+ };
328
+ });
329
+
330
+ describe("POSITIVE: Platform initialized - credential failure should NOT terminate", () => {
331
+ it("should keep platform alive when credential job fails on initialized platform", async () => {
332
+ const TestPlatformInstance = getTestPlatformInstanceClass();
333
+ pi = new TestPlatformInstance({
334
+ identifier: "test-platform-id",
335
+ platform: "xmpp",
336
+ parentId: "test-parent",
337
+ actor: "testuser@localhost",
338
+ });
339
+
340
+ // Override queue with our mock
341
+ pi.queue = queueMock;
342
+ pi.process = processMock;
343
+
344
+ // Setup: Platform is already initialized
345
+ pi.config = {
346
+ persist: true,
347
+ initialized: true,
348
+ requireCredentials: ["connect"],
349
+ };
350
+ pi.flaggedForTermination = false;
351
+
352
+ const job = {
353
+ sessionId: "session123",
354
+ msg: {
355
+ type: "connect",
356
+ context: "xmpp",
357
+ actor: { id: "testuser@localhost", type: "person" },
358
+ },
359
+ title: "xmpp-1",
360
+ sessionSecret: "secret",
361
+ };
362
+
363
+ const errorResult = "credentials mismatch for testuser@localhost";
364
+
365
+ pi.sendToClient = sandbox.stub();
366
+
367
+ // Simulate job failure
368
+ await pi.handleJobResult("failed", job, errorResult);
369
+
370
+ // ASSERTIONS
371
+ // 1. Platform should NOT be flagged for termination
372
+ expect(pi.flaggedForTermination).toBe(false);
373
+
374
+ // 2. Queue should NOT be paused
375
+ sinon.assert.notCalled(queueMock.pause);
376
+
377
+ // 3. Platform should remain initialized
378
+ expect(pi.config.initialized).toBe(true);
379
+
380
+ // 4. Error should still be sent to client
381
+ sinon.assert.called(pi.sendToClient);
382
+ });
383
+
384
+ it("should allow subsequent jobs after non-fatal credential error", async () => {
385
+ const TestPlatformInstance = getTestPlatformInstanceClass();
386
+ pi = new TestPlatformInstance({
387
+ identifier: "test-platform-id",
388
+ platform: "xmpp",
389
+ parentId: "test-parent",
390
+ actor: "testuser@localhost",
391
+ });
392
+
393
+ pi.queue = queueMock;
394
+ pi.process = processMock;
395
+
396
+ pi.config = {
397
+ persist: true,
398
+ initialized: true,
399
+ requireCredentials: ["connect"],
400
+ };
401
+ pi.flaggedForTermination = false;
402
+ pi.sendToClient = sandbox.stub();
403
+
404
+ const failedJob = {
405
+ sessionId: "session123",
406
+ msg: {
407
+ type: "connect",
408
+ context: "xmpp",
409
+ actor: { id: "testuser@localhost", type: "person" },
410
+ },
411
+ title: "xmpp-1",
412
+ sessionSecret: "secret",
413
+ };
414
+
415
+ // First job fails with credential error
416
+ await pi.handleJobResult("failed", failedJob, "credential error");
417
+
418
+ // Platform should still be operational
419
+ expect(pi.flaggedForTermination).toBe(false);
420
+ expect(pi.config.initialized).toBe(true);
421
+
422
+ // Second job succeeds
423
+ const successJob = {
424
+ sessionId: "session456",
425
+ msg: {
426
+ type: "send",
427
+ context: "xmpp",
428
+ actor: { id: "testuser@localhost", type: "person" },
429
+ },
430
+ title: "xmpp-2",
431
+ sessionSecret: "secret",
432
+ };
433
+
434
+ await pi.handleJobResult("completed", successJob, undefined);
435
+
436
+ // Platform should still be alive
437
+ expect(pi.flaggedForTermination).toBe(false);
438
+ expect(pi.config.initialized).toBe(true);
439
+ });
440
+ });
441
+
442
+ describe("NEGATIVE: Platform NOT initialized - credential failure SHOULD terminate", () => {
443
+ it("should terminate platform when credential job fails on uninitialized platform", async () => {
444
+ const TestPlatformInstance = getTestPlatformInstanceClass();
445
+ pi = new TestPlatformInstance({
446
+ identifier: "test-platform-id",
447
+ platform: "xmpp",
448
+ parentId: "test-parent",
449
+ actor: "testuser@localhost",
450
+ });
451
+
452
+ pi.queue = queueMock;
453
+ pi.process = processMock;
454
+
455
+ // Setup: Platform is NOT initialized
456
+ pi.config = {
457
+ persist: true,
458
+ initialized: false,
459
+ requireCredentials: ["connect"],
460
+ };
461
+ pi.flaggedForTermination = false;
462
+ pi.sendToClient = sandbox.stub();
463
+
464
+ const job = {
465
+ sessionId: "session123",
466
+ msg: {
467
+ type: "connect",
468
+ context: "xmpp",
469
+ actor: { id: "testuser@localhost", type: "person" },
470
+ },
471
+ title: "xmpp-1",
472
+ sessionSecret: "secret",
473
+ };
474
+
475
+ const errorResult = "invalid credentials for testuser@localhost";
476
+
477
+ // Simulate job failure on uninitialized platform
478
+ await pi.handleJobResult("failed", job, errorResult);
479
+
480
+ // ASSERTIONS
481
+ // 1. Platform SHOULD be flagged for termination
482
+ expect(pi.flaggedForTermination).toBe(true);
483
+
484
+ // 2. Queue SHOULD be paused
485
+ sinon.assert.calledOnce(queueMock.pause);
486
+
487
+ // 3. Platform should remain uninitialized
488
+ expect(pi.config.initialized).toBe(false);
489
+
490
+ // 4. Error should still be sent to client
491
+ sinon.assert.called(pi.sendToClient);
492
+ });
493
+
494
+ it("should pause queue when credential initialization fails", async () => {
495
+ const TestPlatformInstance = getTestPlatformInstanceClass();
496
+ pi = new TestPlatformInstance({
497
+ identifier: "test-platform-id",
498
+ platform: "xmpp",
499
+ parentId: "test-parent",
500
+ actor: "testuser@localhost",
501
+ });
502
+
503
+ pi.queue = queueMock;
504
+ pi.process = processMock;
505
+
506
+ pi.config = {
507
+ persist: true,
508
+ initialized: false,
509
+ requireCredentials: ["connect"],
510
+ };
511
+ pi.sendToClient = sandbox.stub();
512
+
513
+ const job = {
514
+ sessionId: "session123",
515
+ msg: {
516
+ type: "connect",
517
+ context: "xmpp",
518
+ actor: { id: "testuser@localhost", type: "person" },
519
+ },
520
+ title: "xmpp-1",
521
+ sessionSecret: "secret",
522
+ };
523
+
524
+ await pi.handleJobResult("failed", job, "connection failed");
525
+
526
+ // Queue must be paused
527
+ sinon.assert.calledOnce(queueMock.pause);
528
+ });
529
+ });
530
+ });
531
+ });