@sockethub/server 5.0.0-alpha.11 → 5.0.0-alpha.12

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.
@@ -0,0 +1,75 @@
1
+ import { mock } from "bun:test";
2
+ import type { IInitObject } from "./init.js";
3
+ import { __clearInit } from "./init.js";
4
+ import loadPlatforms from "./load-platforms.js";
5
+
6
+ function getFakePlatform(name: string) {
7
+ return class FakeSockethubPlatform {
8
+ get config() {
9
+ return {};
10
+ }
11
+
12
+ get schema() {
13
+ return {
14
+ name: name,
15
+ version: "0.0.1",
16
+ contextUrl: `https://sockethub.org/ns/context/platform/${name}/v1.jsonld`,
17
+ contextVersion: "1",
18
+ schemaVersion: "1",
19
+ credentials: {
20
+ required: ["object"],
21
+ properties: {
22
+ actor: {
23
+ type: "object",
24
+ required: ["id"],
25
+ },
26
+ object: {
27
+ type: "object",
28
+ required: ["type", "user", "pass"],
29
+ additionalProperties: false,
30
+ properties: {
31
+ type: {
32
+ type: "string",
33
+ },
34
+ user: {
35
+ type: "string",
36
+ },
37
+ pass: {
38
+ type: "string",
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ messages: {
45
+ required: ["type"],
46
+ properties: {
47
+ type: {
48
+ enum: ["echo", "fail"],
49
+ },
50
+ },
51
+ },
52
+ };
53
+ }
54
+ };
55
+ }
56
+
57
+ export async function initMockFakePlatform(platformName: string) {
58
+ const initObject = {
59
+ version: "init object",
60
+ platforms: new Map(),
61
+ } as IInitObject;
62
+ __clearInit();
63
+ const initFunc = async () => {
64
+ const modules = {};
65
+ modules[platformName] = getFakePlatform(platformName);
66
+ initObject.platforms = await loadPlatforms(
67
+ [platformName],
68
+ async (module) => {
69
+ return Promise.resolve(modules[module]);
70
+ },
71
+ );
72
+ return Promise.resolve(initObject);
73
+ };
74
+ return mock(initFunc);
75
+ }
@@ -1,79 +1,10 @@
1
- import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
2
- import * as sinon from "sinon";
1
+ import { beforeEach, describe, expect, it, mock } from "bun:test";
3
2
  import getInitObject, {
4
3
  __clearInit,
5
4
  printSettingsInfo,
6
5
  type IInitObject,
7
6
  } from "./init.js";
8
- import loadPlatforms from "./load-platforms.js";
9
-
10
- function getFakePlatform(name: string) {
11
- return class FakeSockethubPlatform {
12
- get config() {
13
- return {};
14
- }
15
-
16
- get schema() {
17
- return {
18
- name: name,
19
- version: "0.0.1",
20
- credentials: {
21
- required: ["object"],
22
- properties: {
23
- actor: {
24
- type: "object",
25
- required: ["id"],
26
- },
27
- object: {
28
- type: "object",
29
- required: ["type", "user", "pass"],
30
- additionalProperties: false,
31
- properties: {
32
- type: {
33
- type: "string",
34
- },
35
- user: {
36
- type: "string",
37
- },
38
- pass: {
39
- type: "string",
40
- },
41
- },
42
- },
43
- },
44
- },
45
- messages: {
46
- required: ["type"],
47
- properties: {
48
- type: {
49
- enum: ["echo", "fail"],
50
- },
51
- },
52
- },
53
- };
54
- }
55
- };
56
- }
57
-
58
- export async function initMockFakePlatform(platformName: string) {
59
- const initObject = {
60
- version: "init object",
61
- platforms: new Map(),
62
- } as IInitObject;
63
- __clearInit();
64
- const initFunc = async () => {
65
- const modules = {};
66
- modules[platformName] = getFakePlatform(platformName);
67
- initObject.platforms = await loadPlatforms(
68
- [platformName],
69
- async (module) => {
70
- return Promise.resolve(modules[module]);
71
- },
72
- );
73
- return Promise.resolve(initObject);
74
- };
75
- return mock(initFunc);
76
- }
7
+ import { initMockFakePlatform } from "./init.test-helpers.js";
77
8
 
78
9
  describe("platformLoad", () => {
79
10
  let loadInitMock;
@@ -126,32 +57,50 @@ describe("Init", () => {
126
57
  });
127
58
 
128
59
  describe("printSettingsInfo", () => {
129
- let logSpy: sinon.SinonSpy;
130
- let exitStub: sinon.SinonStub;
60
+ let logs: Array<string>;
61
+ let exitCalled: boolean;
62
+ let exitMock: () => never;
63
+ let logMock: (message?: unknown, ...optionalParams: Array<unknown>) => void;
131
64
 
132
65
  beforeEach(() => {
133
- logSpy = sinon.spy(console, "log");
134
- exitStub = sinon.stub(process, "exit");
135
- });
136
-
137
- afterEach(() => {
138
- sinon.restore();
66
+ logs = [];
67
+ exitCalled = false;
68
+ logMock = (message?: unknown, ...optionalParams: Array<unknown>) => {
69
+ const parts = [message, ...optionalParams].filter(
70
+ (part) => typeof part !== "undefined",
71
+ );
72
+ logs.push(parts.map((part) => String(part)).join(" "));
73
+ };
74
+ exitMock = () => {
75
+ exitCalled = true;
76
+ throw new Error("exit called");
77
+ };
139
78
  });
140
79
 
141
80
  it("displays sockethub version", () => {
142
81
  const platforms = new Map();
143
- printSettingsInfo("5.0.0-alpha.4", platforms);
144
-
145
- // Check for version in output (may have color codes)
146
- sinon.assert.calledWithMatch(logSpy, sinon.match(/5\.0\.0-alpha\.4/));
82
+ expect(() =>
83
+ printSettingsInfo("5.0.0-alpha.4", platforms, {
84
+ log: logMock,
85
+ exit: exitMock,
86
+ }),
87
+ ).toThrow("exit called");
88
+
89
+ expect(logs.join("\n")).toMatch(/5\.0\.0-alpha\.4/);
147
90
  });
148
91
 
149
92
  it("displays executable path", () => {
150
93
  const platforms = new Map();
151
- printSettingsInfo("5.0.0", platforms);
152
-
153
- sinon.assert.calledWithMatch(logSpy, sinon.match(/executable:/));
154
- sinon.assert.calledWithMatch(logSpy, sinon.match(/sockethub|init/));
94
+ expect(() =>
95
+ printSettingsInfo("5.0.0", platforms, {
96
+ log: logMock,
97
+ exit: exitMock,
98
+ }),
99
+ ).toThrow("exit called");
100
+
101
+ const output = logs.join("\n");
102
+ expect(output).toMatch(/executable:/);
103
+ expect(output).toMatch(/sockethub|init/);
155
104
  });
156
105
 
157
106
  it("displays platform information with colors", () => {
@@ -166,31 +115,44 @@ describe("printSettingsInfo", () => {
166
115
  types: ["echo", "greet"],
167
116
  config: {},
168
117
  schemas: {
118
+ contextUrl:
119
+ "https://sockethub.org/ns/context/platform/dummy/v1.jsonld",
120
+ contextVersion: "1",
121
+ schemaVersion: "1",
169
122
  credentials: {},
170
123
  messages: {},
171
124
  },
125
+ contextUrl:
126
+ "https://sockethub.org/ns/context/platform/dummy/v1.jsonld",
127
+ contextVersion: "1",
128
+ schemaVersion: "1",
172
129
  },
173
130
  ],
174
131
  ]);
175
132
 
176
- printSettingsInfo("5.0.0", platforms);
177
-
178
- // Verify platform name appears
179
- sinon.assert.calledWithMatch(
180
- logSpy,
181
- sinon.match(/platform-dummy/),
182
- );
183
- // Verify version appears
184
- sinon.assert.calledWithMatch(logSpy, sinon.match(/1\.0\.0/));
185
- // Verify path appears
186
- sinon.assert.calledWithMatch(logSpy, sinon.match(/path.*dummy/));
133
+ expect(() =>
134
+ printSettingsInfo("5.0.0", platforms, {
135
+ log: logMock,
136
+ exit: exitMock,
137
+ }),
138
+ ).toThrow("exit called");
139
+
140
+ const output = logs.join("\n");
141
+ expect(output).toMatch(/platform-dummy/);
142
+ expect(output).toMatch(/1\.0\.0/);
143
+ expect(output).toMatch(/path.*dummy/);
187
144
  });
188
145
 
189
146
  it("calls process.exit after printing", () => {
190
147
  const platforms = new Map();
191
- printSettingsInfo("5.0.0", platforms);
192
-
193
- sinon.assert.calledOnce(exitStub);
148
+ expect(() =>
149
+ printSettingsInfo("5.0.0", platforms, {
150
+ log: logMock,
151
+ exit: exitMock,
152
+ }),
153
+ ).toThrow("exit called");
154
+
155
+ expect(exitCalled).toBeTrue();
194
156
  });
195
157
 
196
158
  it("strips colors in non-TTY environment", () => {
@@ -199,11 +161,13 @@ describe("printSettingsInfo", () => {
199
161
  process.env.NO_COLOR = "1";
200
162
 
201
163
  const platforms = new Map();
202
- printSettingsInfo("5.0.0", platforms);
203
-
204
- // Output should not contain ANSI codes
205
- const calls = logSpy.getCalls();
206
- const output = calls.map((c) => c.args[0]).join("\n");
164
+ expect(() =>
165
+ printSettingsInfo("5.0.0", platforms, {
166
+ log: logMock,
167
+ exit: exitMock,
168
+ }),
169
+ ).toThrow("exit called");
170
+ const output = logs.join("\n");
207
171
  expect(output).not.toMatch(/\x1b\[/); // No ANSI escape codes
208
172
 
209
173
  process.env.NO_COLOR = oldNoColor;
@@ -1,13 +1,17 @@
1
1
  import { fileURLToPath } from "node:url";
2
- import chalk from "chalk";
3
-
4
2
  import { type RedisConfig, redisCheck } from "@sockethub/data-layer";
5
-
6
3
  import { createLogger } from "@sockethub/logger";
7
- import { addPlatformSchema } from "@sockethub/schemas";
4
+ import {
5
+ addPlatformContext,
6
+ addPlatformSchema,
7
+ InternalObjectTypesList,
8
+ setValidationErrorOptions,
9
+ } from "@sockethub/schemas";
10
+ import chalk from "chalk";
8
11
  import config from "../config.js";
9
12
  import loadPlatforms, {
10
13
  type PlatformMap,
14
+ type PlatformSchemaRegistry,
11
15
  type PlatformStruct,
12
16
  } from "./load-platforms.js";
13
17
 
@@ -18,6 +22,11 @@ export interface IInitObject {
18
22
  platforms: PlatformMap;
19
23
  }
20
24
 
25
+ interface PrintSettingsInfoOptions {
26
+ log?: (message?: unknown, ...optionalParams: Array<unknown>) => void;
27
+ exit?: (code?: number) => unknown;
28
+ }
29
+
21
30
  let init: IInitObject;
22
31
 
23
32
  function getExecutablePath(): string {
@@ -32,51 +41,52 @@ function getExecutablePath(): string {
32
41
  export function printSettingsInfo(
33
42
  version: string,
34
43
  platforms: Map<string, PlatformStruct>,
44
+ options: PrintSettingsInfoOptions = {},
35
45
  ) {
46
+ const logFn = options.log ?? console.log;
47
+ const exitFn = options.exit ?? process.exit;
36
48
  const execPath = getExecutablePath();
37
49
 
38
- console.log(`${chalk.cyan("sockethub")} ${version}`);
39
- console.log(`${chalk.cyan("executable:")} ${execPath}`);
50
+ logFn(`${chalk.cyan("sockethub")} ${version}`);
51
+ logFn(`${chalk.cyan("executable:")} ${execPath}`);
40
52
 
41
53
  const wsUrl = `ws://${config.get("sockethub:host")}:${config.get("sockethub:port")}${config.get("sockethub:path")}`;
42
- console.log(`${chalk.cyan("websocket:")} ${chalk.blue(wsUrl)}`);
54
+ logFn(`${chalk.cyan("websocket:")} ${chalk.blue(wsUrl)}`);
43
55
 
44
56
  const examplesUrl = `http://${config.get("public:host")}:${config.get(
45
57
  "public:port",
46
58
  )}${config.get("public:path")}`;
47
- console.log(
59
+ logFn(
48
60
  `${chalk.cyan("examples:")} ${config.get("examples") ? chalk.blue(examplesUrl) : "disabled"}`,
49
61
  );
50
62
 
51
- console.log(
52
- `${chalk.cyan("redis URL:")} ${chalk.blue(config.get("redis:url"))}`,
53
- );
63
+ logFn(`${chalk.cyan("redis URL:")} ${chalk.blue(config.get("redis:url"))}`);
54
64
 
55
- console.log(
65
+ logFn(
56
66
  `${chalk.cyan("platforms:")} ${Array.from(platforms.keys()).join(", ")}`,
57
67
  );
58
68
 
59
69
  if (platforms.size > 0) {
60
70
  for (const platform of platforms.values()) {
61
- console.log();
62
- console.log(chalk.green(`- ${platform.moduleName}`));
63
- console.log(` ${chalk.dim("version:")} ${platform.version}`);
64
- console.log(
71
+ logFn();
72
+ logFn(chalk.green(`- ${platform.moduleName}`));
73
+ logFn(` ${chalk.dim("version:")} ${platform.version}`);
74
+ logFn(
65
75
  ` ${chalk.dim("AS types:")} ${platform.types.map((t) => chalk.yellow(t)).join(", ")}`,
66
76
  );
67
77
  if (platform.modulePath) {
68
- console.log(` ${chalk.dim("path:")} ${platform.modulePath}`);
78
+ logFn(` ${chalk.dim("path:")} ${platform.modulePath}`);
69
79
  }
70
80
  }
71
81
  }
72
- console.log();
73
- process.exit();
82
+ logFn();
83
+ exitFn();
74
84
  }
75
85
 
76
86
  let initCalled = false;
77
87
  let initWaitCount = 0;
78
- let cancelWait: Timer;
79
- const resolveQueue = [];
88
+ let cancelWait: NodeJS.Timeout | undefined;
89
+ const resolveQueue: Array<(init: IInitObject) => void> = [];
80
90
 
81
91
  export default async function getInitObject(
82
92
  initFunc: () => Promise<IInitObject> = __loadInit,
@@ -108,6 +118,9 @@ export default async function getInitObject(
108
118
  if (init) {
109
119
  resolve(init);
110
120
  } else {
121
+ setValidationErrorOptions({
122
+ excludeTypes: InternalObjectTypesList,
123
+ });
111
124
  initFunc()
112
125
  .then((_init) => {
113
126
  init = _init;
@@ -122,12 +135,17 @@ export default async function getInitObject(
122
135
  }
123
136
 
124
137
  export async function registerPlatforms(initObj: IInitObject): Promise<void> {
125
- for (const [_, platform] of initObj.platforms) {
126
- for (const key of Object.keys(platform.schemas)) {
127
- if (!platform.schemas[key]) {
128
- return;
129
- }
130
- addPlatformSchema(platform.schemas[key], `${platform.id}/${key}`);
138
+ for (const [_platformId, platform] of initObj.platforms) {
139
+ const schemas: PlatformSchemaRegistry = platform.schemas;
140
+ addPlatformContext(platform.id, schemas.contextUrl);
141
+ if (schemas.credentials !== undefined) {
142
+ addPlatformSchema(
143
+ schemas.credentials,
144
+ `${platform.id}/credentials`,
145
+ );
146
+ }
147
+ if (schemas.messages !== undefined) {
148
+ addPlatformSchema(schemas.messages, `${platform.id}/messages`);
131
149
  }
132
150
  }
133
151
  }
@@ -13,10 +13,10 @@ import { createLogger } from "@sockethub/logger";
13
13
  import {
14
14
  type PlatformConfig,
15
15
  type PlatformInterface,
16
- type PlatformSchemaStruct,
17
16
  type PlatformSession,
18
17
  validatePlatformSchema,
19
18
  } from "@sockethub/schemas";
19
+ import type { Schema } from "ajv";
20
20
 
21
21
  const log = createLogger("server:bootstrap:platforms");
22
22
 
@@ -25,13 +25,26 @@ export type PlatformStruct = {
25
25
  moduleName: string;
26
26
  modulePath?: string;
27
27
  config: PlatformConfig;
28
- schemas: PlatformSchemaStruct;
28
+ schemas: PlatformSchemaRegistry;
29
29
  version: string;
30
+ contextUrl: string;
31
+ contextVersion: string;
32
+ schemaVersion: string;
30
33
  types: Array<string>;
31
34
  };
32
35
 
33
36
  export type PlatformMap = Map<string, PlatformStruct>;
34
37
 
38
+ export type PlatformSchemaRegistry = {
39
+ name: string;
40
+ version: string;
41
+ contextUrl: string;
42
+ contextVersion: string;
43
+ schemaVersion: string;
44
+ credentials?: Schema | boolean;
45
+ messages?: Schema | boolean;
46
+ };
47
+
35
48
  const dummySession: PlatformSession = {
36
49
  log: createLogger("platform:dummy"),
37
50
  sendToClient: () => {},
@@ -40,7 +53,7 @@ const dummySession: PlatformSession = {
40
53
 
41
54
  // if the platform schema lists valid types it implements (essentially methods/verbs for
42
55
  // Sockethub to call) then add it to the supported types list.
43
- function platformListsSupportedTypes(p): boolean {
56
+ function platformListsSupportedTypes(p: PlatformInterface): boolean {
44
57
  return (
45
58
  p.schema.messages.properties?.type?.enum &&
46
59
  p.schema.messages.properties.type.enum.length > 0
@@ -70,13 +83,23 @@ function resolveModulePath(platformName: string): string | undefined {
70
83
  return dirname(filePath);
71
84
  } catch (err) {
72
85
  log.warn(
73
- `failed to resolve module path for ${platformName}: ${err.message}`,
86
+ `failed to resolve module path for ${platformName}: ${
87
+ err instanceof Error ? err.message : String(err)
88
+ }`,
74
89
  );
75
90
  return undefined;
76
91
  }
77
92
  }
78
93
 
79
- async function loadPlatform(platformName: string, injectRequire) {
94
+ type PlatformConstructor = new (...args: unknown[]) => PlatformInterface;
95
+ type InjectRequire = (
96
+ moduleName: string,
97
+ ) => Promise<PlatformConstructor> | PlatformConstructor;
98
+
99
+ async function loadPlatform(
100
+ platformName: string,
101
+ injectRequire?: InjectRequire,
102
+ ) {
80
103
  log.debug(`loading ${platformName}`);
81
104
  let p: PlatformInterface;
82
105
  if (injectRequire) {
@@ -101,11 +124,11 @@ async function loadPlatform(platformName: string, injectRequire) {
101
124
 
102
125
  export default async function loadPlatforms(
103
126
  platformsList: Array<string>,
104
- injectRequire = undefined,
127
+ injectRequire?: InjectRequire,
105
128
  ): Promise<PlatformMap> {
106
129
  log.debug(`platforms to load: ${platformsList}`);
107
130
  // load platforms from config.platforms
108
- const platforms = new Map();
131
+ const platforms = new Map<string, PlatformStruct>();
109
132
 
110
133
  if (platformsList.length <= 0) {
111
134
  throw new Error(
@@ -115,7 +138,7 @@ export default async function loadPlatforms(
115
138
 
116
139
  for (const platformName of platformsList) {
117
140
  const p = await loadPlatform(platformName, injectRequire);
118
- let types = [];
141
+ let types: Array<string> = [];
119
142
 
120
143
  if (p.schema.credentials) {
121
144
  // register the platforms credentials schema
@@ -139,10 +162,18 @@ export default async function loadPlatforms(
139
162
  modulePath: modulePath,
140
163
  config: p.config,
141
164
  schemas: {
165
+ name: p.schema.name,
166
+ version: p.schema.version,
167
+ contextUrl: p.schema.contextUrl,
168
+ contextVersion: p.schema.contextVersion,
169
+ schemaVersion: p.schema.schemaVersion,
142
170
  credentials: p.schema.credentials || {},
143
171
  messages: p.schema.messages || {},
144
172
  },
145
173
  version: p.schema.version,
174
+ contextUrl: p.schema.contextUrl,
175
+ contextVersion: p.schema.contextVersion,
176
+ schemaVersion: p.schema.schemaVersion,
146
177
  types: types,
147
178
  });
148
179
  log.info(`loaded platform ${p.schema.name} v${p.schema.version}`);
package/src/config.ts CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import nconf from "nconf";
4
4
 
5
- import { type Logger, createWinstonLogger } from "./logger-core.js";
5
+ import { createWinstonLogger, type Logger } from "./logger-core.js";
6
6
  import { __dirname } from "./util.js";
7
7
 
8
8
  const data = JSON.parse(
@@ -109,6 +109,17 @@ export class Config {
109
109
  "logging:file",
110
110
  process.env.LOG_FILE || nconf.get("logging:file"),
111
111
  );
112
+
113
+ nconf.set(
114
+ "platformHeartbeat:intervalMs",
115
+ process.env.SOCKETHUB_PLATFORM_HEARTBEAT_INTERVAL_MS ||
116
+ nconf.get("platformHeartbeat:intervalMs"),
117
+ );
118
+ nconf.set(
119
+ "platformHeartbeat:timeoutMs",
120
+ process.env.SOCKETHUB_PLATFORM_HEARTBEAT_TIMEOUT_MS ||
121
+ nconf.get("platformHeartbeat:timeoutMs"),
122
+ );
112
123
  }
113
124
  get = (key: string) => nconf.get(key);
114
125
  }
package/src/defaults.json CHANGED
@@ -44,5 +44,9 @@
44
44
  "port": 10550,
45
45
  "host": "localhost",
46
46
  "path": "/sockethub"
47
+ },
48
+ "platformHeartbeat": {
49
+ "intervalMs": 5000,
50
+ "timeoutMs": 15000
47
51
  }
48
52
  }
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import config from "./config";
4
4
  import Sockethub from "./sockethub";
5
5
 
6
6
  let sentry: { readonly reportError: (err: Error) => void } = {
7
- reportError: (err: Error) => {},
7
+ reportError: (_err: Error) => {},
8
8
  };
9
9
 
10
10
  export async function server() {
@@ -34,8 +34,9 @@ export async function server() {
34
34
  try {
35
35
  sockethub = new Sockethub();
36
36
  } catch (err) {
37
- sentry.reportError(err);
38
- console.error(err);
37
+ const error = err instanceof Error ? err : new Error(String(err));
38
+ sentry.reportError(error);
39
+ console.error(error);
39
40
  process.exit(1);
40
41
  }
41
42
 
@@ -48,12 +49,12 @@ export async function server() {
48
49
  process.exit(1);
49
50
  });
50
51
 
51
- process.once("unhandledRejection", (err: Error) => {
52
+ process.once("unhandledRejection", (err: unknown) => {
52
53
  console.error(
53
54
  `${(new Date()).toUTCString()} UNHANDLED REJECTION\n`,
54
55
  err,
55
56
  );
56
- sentry.reportError(err);
57
+ sentry.reportError(err instanceof Error ? err : new Error(String(err)));
57
58
  process.exit(1);
58
59
  });
59
60
 
@@ -76,8 +77,9 @@ export async function server() {
76
77
  try {
77
78
  await sockethub.boot();
78
79
  } catch (err) {
79
- sentry.reportError(err);
80
- console.error(err);
80
+ const error = err instanceof Error ? err : new Error(String(err));
81
+ sentry.reportError(error);
82
+ console.error(error);
81
83
  process.exit(1);
82
84
  }
83
85
  }
package/src/janitor.ts CHANGED
@@ -44,7 +44,9 @@ export class Janitor {
44
44
  platformInstance: PlatformInstance,
45
45
  sessionId: string,
46
46
  ): void {
47
- for (const key in platformInstance.sessionCallbacks) {
47
+ for (const key of Object.keys(
48
+ platformInstance.sessionCallbacks,
49
+ ) as Array<"close" | "message">) {
48
50
  platformInstance.process.removeListener(
49
51
  key,
50
52
  platformInstance.sessionCallbacks[key].get(sessionId),