@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,151 @@
1
+ /**
2
+ * bootstrap/platforms.ts
3
+ *
4
+ * A Singleton responsible for finding and loading all valid Sockethub
5
+ * platforms, and whitelisting or blacklisting (or neither) based on the
6
+ * config.
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import debug from "debug";
12
+
13
+ import {
14
+ type PlatformConfig,
15
+ type PlatformInterface,
16
+ type PlatformSchemaStruct,
17
+ type PlatformSession,
18
+ validatePlatformSchema,
19
+ } from "@sockethub/schemas";
20
+
21
+ const log = debug("sockethub:server:bootstrap:platforms");
22
+
23
+ export type PlatformStruct = {
24
+ id: string;
25
+ moduleName: string;
26
+ modulePath?: string;
27
+ config: PlatformConfig;
28
+ schemas: PlatformSchemaStruct;
29
+ version: string;
30
+ types: Array<string>;
31
+ };
32
+
33
+ export type PlatformMap = Map<string, PlatformStruct>;
34
+
35
+ const dummySession: PlatformSession = {
36
+ debug: () => {},
37
+ sendToClient: () => {},
38
+ updateActor: async () => {},
39
+ };
40
+
41
+ // if the platform schema lists valid types it implements (essentially methods/verbs for
42
+ // Sockethub to call) then add it to the supported types list.
43
+ function platformListsSupportedTypes(p): boolean {
44
+ return (
45
+ p.schema.messages.properties?.type?.enum &&
46
+ p.schema.messages.properties.type.enum.length > 0
47
+ );
48
+ }
49
+
50
+ // Resolve the absolute filesystem path to a platform module
51
+ function resolveModulePath(platformName: string): string | undefined {
52
+ try {
53
+ // Resolve to file:// URL
54
+ const resolved = import.meta.resolve(platformName);
55
+
56
+ // Convert to absolute path
57
+ const filePath = fileURLToPath(resolved);
58
+
59
+ // Walk up to find package.json (package root)
60
+ let dir = dirname(filePath);
61
+ for (let i = 0; i < 5; i++) {
62
+ const pkgPath = join(dir, "package.json");
63
+ if (existsSync(pkgPath)) {
64
+ return dir;
65
+ }
66
+ dir = dirname(dir);
67
+ }
68
+
69
+ // Fallback: return directory of entry point
70
+ return dirname(filePath);
71
+ } catch (err) {
72
+ log(
73
+ `failed to resolve module path for ${platformName}: ${err.message}`,
74
+ );
75
+ return undefined;
76
+ }
77
+ }
78
+
79
+ async function loadPlatform(platformName: string, injectRequire) {
80
+ log(`loading ${platformName}`);
81
+ let p: PlatformInterface;
82
+ if (injectRequire) {
83
+ const P = await injectRequire(platformName);
84
+ p = new P();
85
+ } else {
86
+ const P = await import(platformName);
87
+ p = new P.default(dummySession);
88
+ }
89
+ const err = validatePlatformSchema(p.schema);
90
+
91
+ if (err) {
92
+ throw new Error(`${platformName} ${err}`);
93
+ }
94
+ if (typeof p.config !== "object") {
95
+ throw new Error(
96
+ `${platformName} platform must have a config property that is an object.`,
97
+ );
98
+ }
99
+ return p;
100
+ }
101
+
102
+ export default async function loadPlatforms(
103
+ platformsList: Array<string>,
104
+ injectRequire = undefined,
105
+ ): Promise<PlatformMap> {
106
+ log(`platforms to load: ${platformsList}`);
107
+ // load platforms from config.platforms
108
+ const platforms = new Map();
109
+
110
+ if (platformsList.length <= 0) {
111
+ throw new Error(
112
+ "No platforms defined. Please check your sockethub.config.json",
113
+ );
114
+ }
115
+
116
+ for (const platformName of platformsList) {
117
+ const p = await loadPlatform(platformName, injectRequire);
118
+ let types = [];
119
+
120
+ if (p.schema.credentials) {
121
+ // register the platforms credentials schema
122
+ types.push("credentials");
123
+ } else {
124
+ p.config.requireCredentials = [];
125
+ }
126
+
127
+ if (platformListsSupportedTypes(p)) {
128
+ types = [...types, ...p.schema.messages.properties.type.enum];
129
+ }
130
+
131
+ // Resolve module path (skip in test mode with injectRequire)
132
+ const modulePath = injectRequire
133
+ ? undefined
134
+ : resolveModulePath(platformName);
135
+
136
+ platforms.set(p.schema.name, {
137
+ id: p.schema.name,
138
+ moduleName: p.schema.name,
139
+ modulePath: modulePath,
140
+ config: p.config,
141
+ schemas: {
142
+ credentials: p.schema.credentials || {},
143
+ messages: p.schema.messages || {},
144
+ },
145
+ version: p.schema.version,
146
+ types: types,
147
+ });
148
+ }
149
+
150
+ return platforms;
151
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { Config } from "./config.js";
4
+
5
+ describe("config", () => {
6
+ it("loads default values", () => {
7
+ const config = new Config();
8
+ expect(config).toHaveProperty("get");
9
+ expect(config.get("sockethub:host")).toEqual("localhost");
10
+ });
11
+
12
+ it("host overrides from env", () => {
13
+ const hostname = "a host string";
14
+ process.env = { HOST: hostname };
15
+ const config = new Config();
16
+ expect(config).toHaveProperty("get");
17
+ expect(config.get("sockethub:host")).toEqual(hostname);
18
+ });
19
+
20
+ it("defaults to redis config", () => {
21
+ process.env = { REDIS_URL: "" };
22
+ const config = new Config();
23
+ expect(config).toHaveProperty("get");
24
+ expect(config.get("redis")).toEqual({ url: "redis://127.0.0.1:6379" });
25
+ });
26
+
27
+ // it("redis url overridden by env var", () => {
28
+ // process.env = { REDIS_URL: "foobar83" };
29
+ // const config = new Config();
30
+ // expect(config).toHaveProperty("get");
31
+ // expect(config.get("redis")).toEqual({ url: "foobar83" });
32
+ // });
33
+ });
package/src/config.ts ADDED
@@ -0,0 +1,98 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import debug from "debug";
4
+ import nconf from "nconf";
5
+
6
+ import { __dirname } from "./util.js";
7
+
8
+ const log = debug("sockethub:server:bootstrap:config");
9
+ const data = JSON.parse(
10
+ fs.readFileSync(path.resolve(__dirname, "defaults.json"), "utf-8"),
11
+ );
12
+
13
+ const defaultConfig = "sockethub.config.json";
14
+
15
+ export class Config {
16
+ constructor() {
17
+ log("initializing config");
18
+ // assign config loading priorities (command-line, environment, cfg, defaults)
19
+ nconf.argv({
20
+ info: {
21
+ type: "boolean",
22
+ describe: "Display Sockethub runtime information",
23
+ },
24
+ examples: {
25
+ type: "boolean",
26
+ describe:
27
+ "Enable the examples pages served at [host]:[port]/examples",
28
+ },
29
+ config: {
30
+ alias: "c",
31
+ type: "string",
32
+ describe: "Path to sockethub.config.json",
33
+ },
34
+ port: {
35
+ type: "number",
36
+ alias: "sockethub.port",
37
+ },
38
+ host: {
39
+ type: "string",
40
+ alias: "sockethub.host",
41
+ },
42
+ "redis.url": {
43
+ type: "string",
44
+ describe: "Redis URL e.g. redis://host:port",
45
+ },
46
+ "sentry.dsn": {
47
+ type: "string",
48
+ describe: "Provide your Sentry DSN",
49
+ },
50
+ });
51
+
52
+ // get value of flags defined by any command-line params
53
+ const examples = nconf.get("examples");
54
+
55
+ // Load the main config
56
+ let configFile = nconf.get("config");
57
+ if (configFile) {
58
+ configFile = path.resolve(configFile);
59
+ if (!fs.existsSync(configFile)) {
60
+ throw new Error(`Config file not found: ${configFile}`);
61
+ }
62
+ log(`reading config file at ${configFile}`);
63
+ nconf.file(configFile);
64
+ } else {
65
+ if (fs.existsSync(`${process.cwd()}/${defaultConfig}`)) {
66
+ log(`loading local ${defaultConfig}`);
67
+ nconf.file(`${process.cwd()}/${defaultConfig}`);
68
+ }
69
+ nconf.use("memory");
70
+ }
71
+
72
+ // only override config file if explicitly mentioned in command-line params
73
+ nconf.set("examples", examples ? true : nconf.get("examples"));
74
+
75
+ // load defaults
76
+ nconf.defaults(data);
77
+
78
+ nconf.required(["platforms"]);
79
+
80
+ nconf.set(
81
+ "sockethub:host",
82
+ process.env.HOST || nconf.get("sockethub:host"),
83
+ );
84
+ nconf.set(
85
+ "sockethub:port",
86
+ process.env.PORT || nconf.get("sockethub:port"),
87
+ );
88
+
89
+ // allow a redis://user:host:port url, takes precedence
90
+ if (process.env.REDIS_URL) {
91
+ nconf.set("redis:url", process.env.REDIS_URL);
92
+ }
93
+ }
94
+ get = (key: string) => nconf.get(key);
95
+ }
96
+
97
+ const config = new Config();
98
+ export default config;
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://sockethub.org/schemas/3.0.0-alpha.4/sockethub-config.json",
3
+ "examples": true,
4
+ "log_file": "",
5
+ "packageConfig": {
6
+ "@sockethub/activity-streams": {
7
+ "specialObjs": ["credentials"],
8
+ "failOnUnknownObjectProperties": true
9
+ }
10
+ },
11
+ "platforms": [
12
+ "@sockethub/platform-dummy",
13
+ "@sockethub/platform-feeds",
14
+ "@sockethub/platform-irc",
15
+ "@sockethub/platform-metadata",
16
+ "@sockethub/platform-xmpp"
17
+ ],
18
+ "public": {
19
+ "protocol": "http",
20
+ "host": "localhost",
21
+ "port": 10550,
22
+ "path": "/"
23
+ },
24
+ "redis": {
25
+ "url": "redis://127.0.0.1:6379"
26
+ },
27
+ "sentry": {
28
+ "dsn": "",
29
+ "traceSampleRate": 1.0
30
+ },
31
+ "sockethub": {
32
+ "port": 10550,
33
+ "host": "localhost",
34
+ "path": "/sockethub"
35
+ }
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ import debug from "debug";
2
+ import config from "./config";
3
+ import Sockethub from "./sockethub";
4
+
5
+ let sentry: { readonly reportError: (err: Error) => void } = {
6
+ reportError: (err: Error) => {},
7
+ };
8
+
9
+ export async function server() {
10
+ let sockethub: Sockethub;
11
+ const log = debug("sockethub:init");
12
+
13
+ // conditionally initialize sentry
14
+ if (config.get("sentry:dsn")) {
15
+ log("initializing sentry");
16
+ sentry = await import("./sentry");
17
+ }
18
+
19
+ try {
20
+ sockethub = new Sockethub();
21
+ } catch (err) {
22
+ sentry.reportError(err);
23
+ console.error(err);
24
+ process.exit(1);
25
+ }
26
+
27
+ process.once("uncaughtException", (err: Error) => {
28
+ console.error(
29
+ `${(new Date()).toUTCString()} UNCAUGHT EXCEPTION\n`,
30
+ err.stack,
31
+ );
32
+ sentry.reportError(err);
33
+ process.exit(1);
34
+ });
35
+
36
+ process.once("unhandledRejection", (err: Error) => {
37
+ console.error(
38
+ `${(new Date()).toUTCString()} UNHANDLED REJECTION\n`,
39
+ err,
40
+ );
41
+ sentry.reportError(err);
42
+ process.exit(1);
43
+ });
44
+
45
+ process.once("SIGTERM", () => {
46
+ console.log("Received TERM signal. Exiting.");
47
+ process.exit(0);
48
+ });
49
+
50
+ process.once("SIGINT", () => {
51
+ console.log("Received INT signal. Exiting.");
52
+ process.exit(0);
53
+ });
54
+
55
+ process.once("exit", async () => {
56
+ console.log("sockethub shutdown...");
57
+ await sockethub.shutdown();
58
+ process.exit(0);
59
+ });
60
+
61
+ try {
62
+ await sockethub.boot();
63
+ } catch (err) {
64
+ sentry.reportError(err);
65
+ console.error(err);
66
+ process.exit(1);
67
+ }
68
+ }
@@ -0,0 +1,211 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import * as sinon from "sinon";
3
+
4
+ import { Janitor } from "./janitor.js";
5
+
6
+ const sockets = [
7
+ { id: "socket foo", emit: () => {} },
8
+ { id: "socket bar", emit: () => {} },
9
+ ];
10
+
11
+ function getPlatformInstanceFake() {
12
+ return {
13
+ flaggedForTermination: false,
14
+ config: {
15
+ initialized: false,
16
+ persist: true,
17
+ requireCredentials: ["foo", "bar"],
18
+ },
19
+ global: false,
20
+ shutdown: sinon.stub(),
21
+ process: {
22
+ removeListener: sinon.stub(),
23
+ },
24
+ sessions: new Set(["session foo", "session bar"]),
25
+ sessionCallbacks: {
26
+ close: (() =>
27
+ new Map([
28
+ ["session foo", function sessionFooClose() {}],
29
+ ["session bar", function sessionBarClose() {}],
30
+ ]))(),
31
+ message: (() =>
32
+ new Map([
33
+ ["session foo", function sessionFooMessage() {}],
34
+ ["session bar", function sessionBarMessage() {}],
35
+ ]))(),
36
+ },
37
+ };
38
+ }
39
+
40
+ const cycleInterval = 10;
41
+
42
+ describe("Janitor", () => {
43
+ let sandbox, fetchSocketsFake, janitor;
44
+
45
+ beforeEach((done) => {
46
+ sandbox = sinon.createSandbox();
47
+ fetchSocketsFake = sandbox.stub().returns(sockets);
48
+
49
+ janitor = new Janitor();
50
+ janitor.getSockets = fetchSocketsFake;
51
+ expect(janitor.cycleInterval).not.toEqual(cycleInterval);
52
+ janitor.cycleInterval = cycleInterval;
53
+ expect(janitor.cycleInterval).toEqual(cycleInterval);
54
+ janitor.start();
55
+ setTimeout(() => {
56
+ expect(janitor.cycleCount).toEqual(1);
57
+ done();
58
+ }, cycleInterval);
59
+ });
60
+
61
+ afterEach((done) => {
62
+ sandbox.reset();
63
+ janitor.stop();
64
+ setTimeout(() => {
65
+ done();
66
+ }, janitor.cycleInterval * 2);
67
+ });
68
+
69
+ it("runs cycle at every cycleInterval", (done) => {
70
+ const currCycleCount = janitor.cycleCount;
71
+ expect(currCycleCount).not.toEqual(0);
72
+ setTimeout(() => {
73
+ expect(janitor.cycleCount).toEqual(currCycleCount + 1);
74
+ setTimeout(() => {
75
+ expect(janitor.cycleCount).toEqual(currCycleCount + 2);
76
+ done();
77
+ }, cycleInterval);
78
+ }, cycleInterval);
79
+ });
80
+
81
+ describe("removeSessionCallbacks", () => {
82
+ it("removes session listeners and callbacks for a given platform", () => {
83
+ const pi = getPlatformInstanceFake();
84
+ const barMessage = pi.sessionCallbacks.message.get("session bar");
85
+ const barClose = pi.sessionCallbacks.close.get("session bar");
86
+ pi.flaggedForTermination = true;
87
+ janitor.removeSessionCallbacks(pi, "session foo");
88
+ sinon.assert.calledTwice(pi.process.removeListener);
89
+ expect(
90
+ pi.sessionCallbacks.message.get("session foo"),
91
+ ).toBeUndefined();
92
+ expect(pi.sessionCallbacks.message.get("session bar")).toEqual(
93
+ barMessage,
94
+ );
95
+ expect(
96
+ pi.sessionCallbacks.close.get("session foo"),
97
+ ).toBeUndefined();
98
+ expect(pi.sessionCallbacks.close.get("session bar")).toEqual(
99
+ barClose,
100
+ );
101
+ });
102
+ });
103
+
104
+ describe("removeStaleSocketSessions", () => {
105
+ it("doesnt do anything if the socket is active and stop is not flagged", async () => {
106
+ const pi = getPlatformInstanceFake();
107
+ janitor.removeSessionCallbacks = sinon.stub();
108
+ janitor.socketExists = sinon.stub().returns(true);
109
+ expect(janitor.stopTriggered).toBeFalse();
110
+ await janitor.removeStaleSocketSessions(pi);
111
+ sinon.assert.notCalled(janitor.removeSessionCallbacks);
112
+ });
113
+
114
+ it("removes session if the socket is active and stop is flagged", async () => {
115
+ const pi = getPlatformInstanceFake();
116
+ janitor.removeSessionCallbacks = sinon.stub();
117
+ janitor.socketExists = sinon.stub().returns(true);
118
+ janitor.stop();
119
+ expect(janitor.stopTriggered).toBeTrue();
120
+ await janitor.removeStaleSocketSessions(pi);
121
+ sinon.assert.calledTwice(janitor.removeSessionCallbacks);
122
+ sinon.assert.calledWith(
123
+ janitor.removeSessionCallbacks,
124
+ pi,
125
+ "session foo",
126
+ );
127
+ sinon.assert.calledWith(
128
+ janitor.removeSessionCallbacks,
129
+ pi,
130
+ "session bar",
131
+ );
132
+ });
133
+
134
+ it("removes session if the socket is inactive", async () => {
135
+ const pi = getPlatformInstanceFake();
136
+ janitor.removeSessionCallbacks = sinon.stub();
137
+ janitor.socketExists = sinon
138
+ .stub()
139
+ .onFirstCall()
140
+ .returns(false)
141
+ .onSecondCall()
142
+ .returns(true);
143
+ expect(janitor.stopTriggered).toBeFalse();
144
+ await janitor.removeStaleSocketSessions(pi);
145
+ sinon.assert.calledOnce(janitor.removeSessionCallbacks);
146
+ sinon.assert.calledWith(
147
+ janitor.removeSessionCallbacks,
148
+ pi,
149
+ "session foo",
150
+ );
151
+ });
152
+ });
153
+
154
+ describe("performStaleCheck", () => {
155
+ it("removes flagged and uninitialized platform instances", async () => {
156
+ const pi = getPlatformInstanceFake();
157
+ pi.flaggedForTermination = true;
158
+ pi.config.initialized = false;
159
+ janitor.removeStaleSocketSessions = sandbox.stub();
160
+ janitor.removeStalePlatformInstance = sandbox.stub();
161
+ await janitor.performStaleCheck(pi);
162
+ sinon.assert.calledOnce(janitor.removeStaleSocketSessions);
163
+ sinon.assert.calledOnce(janitor.removeStalePlatformInstance);
164
+ expect(pi.flaggedForTermination).toBeTrue();
165
+ });
166
+
167
+ it("flags for termination when there are not sockets", async () => {
168
+ const pi = getPlatformInstanceFake();
169
+ pi.sessions = new Set();
170
+ pi.flaggedForTermination = false;
171
+ pi.config.initialized = true;
172
+ janitor.removeStaleSocketSessions = sandbox.stub();
173
+ janitor.removeStalePlatformInstance = sandbox.stub();
174
+ await janitor.performStaleCheck(pi);
175
+ sinon.assert.calledOnce(janitor.removeStaleSocketSessions);
176
+ sinon.assert.calledOnce(janitor.removeStalePlatformInstance);
177
+ });
178
+ });
179
+
180
+ describe("removeStalePlatformInstance", () => {
181
+ it("flags stale platform", async () => {
182
+ const pi = getPlatformInstanceFake();
183
+ expect(pi.flaggedForTermination).toBeFalse();
184
+ await janitor.removeStalePlatformInstance(pi);
185
+ sinon.assert.notCalled(pi.shutdown);
186
+ expect(pi.flaggedForTermination).toBeTrue();
187
+ });
188
+
189
+ it("removes flagged stale platform", async () => {
190
+ const pi = getPlatformInstanceFake();
191
+ pi.flaggedForTermination = true;
192
+ await janitor.removeStalePlatformInstance(pi);
193
+ sinon.assert.calledOnce(pi.shutdown);
194
+ });
195
+ });
196
+
197
+ it("closes all connections when stop() is called", (done) => {
198
+ const prevCycle = janitor.cycleCount;
199
+ janitor.stop();
200
+ setTimeout(() => {
201
+ expect(janitor.cycleCount).toEqual(prevCycle);
202
+ setTimeout(() => {
203
+ expect(janitor.cycleCount).toEqual(prevCycle);
204
+ setTimeout(() => {
205
+ expect(janitor.cycleCount).toEqual(prevCycle);
206
+ done();
207
+ }, cycleInterval);
208
+ }, cycleInterval);
209
+ }, cycleInterval);
210
+ });
211
+ });