@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.
- package/LICENSE +165 -0
- package/README.md +130 -0
- package/bin/sockethub +4 -0
- package/dist/defaults.json +36 -0
- package/dist/index.js +166465 -0
- package/dist/index.js.map +1877 -0
- package/dist/platform.js +103625 -0
- package/dist/platform.js.map +1435 -0
- package/package.json +100 -0
- package/res/socket.io.js +4908 -0
- package/res/sockethub-client.js +631 -0
- package/res/sockethub-client.min.js +19 -0
- package/src/bootstrap/init.d.ts +21 -0
- package/src/bootstrap/init.test.ts +211 -0
- package/src/bootstrap/init.ts +160 -0
- package/src/bootstrap/load-platforms.ts +151 -0
- package/src/config.test.ts +33 -0
- package/src/config.ts +98 -0
- package/src/defaults.json +36 -0
- package/src/index.ts +68 -0
- package/src/janitor.test.ts +211 -0
- package/src/janitor.ts +157 -0
- package/src/listener.ts +173 -0
- package/src/middleware/create-activity-object.test.ts +30 -0
- package/src/middleware/create-activity-object.ts +22 -0
- package/src/middleware/expand-activity-stream.test.data.ts +351 -0
- package/src/middleware/expand-activity-stream.test.ts +77 -0
- package/src/middleware/expand-activity-stream.ts +37 -0
- package/src/middleware/store-credentials.test.ts +85 -0
- package/src/middleware/store-credentials.ts +16 -0
- package/src/middleware/validate.test.data.ts +259 -0
- package/src/middleware/validate.test.ts +44 -0
- package/src/middleware/validate.ts +73 -0
- package/src/middleware.test.ts +184 -0
- package/src/middleware.ts +71 -0
- package/src/platform-instance.test.ts +531 -0
- package/src/platform-instance.ts +360 -0
- package/src/platform.test.ts +375 -0
- package/src/platform.ts +358 -0
- package/src/process-manager.ts +88 -0
- package/src/routes.test.ts +54 -0
- package/src/routes.ts +61 -0
- package/src/sentry.test.ts +106 -0
- package/src/sentry.ts +19 -0
- package/src/sockethub.ts +198 -0
- package/src/util.ts +5 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { type ChildProcess, fork } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import debug from "debug";
|
|
4
|
+
|
|
5
|
+
import { type JobDataDecrypted, JobQueue } from "@sockethub/data-layer";
|
|
6
|
+
import type {
|
|
7
|
+
ActivityStream,
|
|
8
|
+
CompletedJobHandler,
|
|
9
|
+
InternalActivityStream,
|
|
10
|
+
Logger,
|
|
11
|
+
PlatformConfig,
|
|
12
|
+
} from "@sockethub/schemas";
|
|
13
|
+
import type { Socket } from "socket.io";
|
|
14
|
+
|
|
15
|
+
import config from "./config.js";
|
|
16
|
+
import { getSocket } from "./listener.js";
|
|
17
|
+
import { __dirname } from "./util.js";
|
|
18
|
+
|
|
19
|
+
// collection of platform instances, stored by `id`
|
|
20
|
+
export const platformInstances = new Map<string, PlatformInstance>();
|
|
21
|
+
|
|
22
|
+
export interface PlatformInstanceParams {
|
|
23
|
+
identifier: string;
|
|
24
|
+
platform: string;
|
|
25
|
+
parentId?: string;
|
|
26
|
+
actor?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type EnvFormat = {
|
|
30
|
+
DEBUG?: string;
|
|
31
|
+
REDIS_URL: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface MessageFromPlatform extends Array<string | ActivityStream> {
|
|
35
|
+
0: string;
|
|
36
|
+
1: ActivityStream;
|
|
37
|
+
2: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MessageFromParent extends Array<string | unknown> {
|
|
41
|
+
0: string;
|
|
42
|
+
1: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default class PlatformInstance {
|
|
46
|
+
id: string;
|
|
47
|
+
flaggedForTermination = false;
|
|
48
|
+
queue: JobQueue;
|
|
49
|
+
JobQueue: typeof JobQueue;
|
|
50
|
+
getSocket: typeof getSocket;
|
|
51
|
+
readonly global: boolean = false;
|
|
52
|
+
readonly completedJobHandlers: Map<string, CompletedJobHandler> = new Map();
|
|
53
|
+
config: PlatformConfig;
|
|
54
|
+
readonly name: string;
|
|
55
|
+
process: ChildProcess;
|
|
56
|
+
readonly debug: Logger;
|
|
57
|
+
readonly parentId: string;
|
|
58
|
+
readonly sessions: Set<string> = new Set();
|
|
59
|
+
readonly sessionCallbacks: object = {
|
|
60
|
+
close: (() => new Map())(),
|
|
61
|
+
message: (() => new Map())(),
|
|
62
|
+
};
|
|
63
|
+
private readonly actor?: string;
|
|
64
|
+
|
|
65
|
+
constructor(params: PlatformInstanceParams) {
|
|
66
|
+
this.id = params.identifier;
|
|
67
|
+
this.name = params.platform;
|
|
68
|
+
this.parentId = params.parentId;
|
|
69
|
+
if (params.actor) {
|
|
70
|
+
this.actor = params.actor;
|
|
71
|
+
} else {
|
|
72
|
+
this.global = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.debug = debug(`sockethub:server:platform-instance:${this.id}`);
|
|
76
|
+
const env: EnvFormat = {
|
|
77
|
+
REDIS_URL: config.get("redis:url") as string,
|
|
78
|
+
};
|
|
79
|
+
if (process.env.DEBUG) {
|
|
80
|
+
env.DEBUG = process.env.DEBUG;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.createQueue();
|
|
84
|
+
this.initProcess(this.parentId, this.name, this.id, env);
|
|
85
|
+
this.createGetSocket();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
createQueue() {
|
|
89
|
+
this.JobQueue = JobQueue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
initProcess(parentId: string, name: string, id: string, env: EnvFormat) {
|
|
93
|
+
// spin off a process
|
|
94
|
+
this.process = fork(
|
|
95
|
+
join(__dirname, "platform.js"),
|
|
96
|
+
[parentId, name, id],
|
|
97
|
+
{ env: env },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
createGetSocket() {
|
|
102
|
+
this.getSocket = getSocket;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Destroys all references to this platform instance, internal listeners and controlled processes
|
|
107
|
+
*/
|
|
108
|
+
public async shutdown() {
|
|
109
|
+
this.debug("platform process shutdown");
|
|
110
|
+
this.flaggedForTermination = true;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
this.process.removeAllListeners("close");
|
|
114
|
+
this.process.unref();
|
|
115
|
+
this.process.kill();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// needs to happen
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await this.queue.shutdown();
|
|
122
|
+
this.queue = undefined;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// this needs to happen
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
platformInstances.delete(this.id);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// this needs to happen
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* When jobs are completed or failed, we prepare the results and send them to the client socket
|
|
136
|
+
*/
|
|
137
|
+
public initQueue(secret: string) {
|
|
138
|
+
this.queue = new this.JobQueue(
|
|
139
|
+
this.parentId,
|
|
140
|
+
this.id,
|
|
141
|
+
secret,
|
|
142
|
+
config.get("redis"),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
this.queue.on(
|
|
146
|
+
"completed",
|
|
147
|
+
async (
|
|
148
|
+
job: JobDataDecrypted,
|
|
149
|
+
result: ActivityStream | undefined,
|
|
150
|
+
) => {
|
|
151
|
+
await this.handleJobResult("completed", job, result);
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
this.queue.on(
|
|
156
|
+
"failed",
|
|
157
|
+
async (
|
|
158
|
+
job: JobDataDecrypted,
|
|
159
|
+
result: ActivityStream | undefined,
|
|
160
|
+
) => {
|
|
161
|
+
await this.handleJobResult("failed", job, result);
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Register listener to be called when the process emits a message.
|
|
168
|
+
* @param sessionId ID of socket connection that will receive messages from platform emits
|
|
169
|
+
*/
|
|
170
|
+
public registerSession(sessionId: string) {
|
|
171
|
+
if (!this.sessions.has(sessionId)) {
|
|
172
|
+
this.sessions.add(sessionId);
|
|
173
|
+
for (const type of Object.keys(this.sessionCallbacks)) {
|
|
174
|
+
const cb = this.callbackFunction(type, sessionId);
|
|
175
|
+
this.process.on(type, cb);
|
|
176
|
+
this.sessionCallbacks[type].set(sessionId, cb);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Sends a message to client (user), can be registered with an event emitted from the platform
|
|
183
|
+
* process.
|
|
184
|
+
* @param sessionId ID of the socket connection to send the message to
|
|
185
|
+
* @param msg ActivityStream object to send to client
|
|
186
|
+
*/
|
|
187
|
+
public async sendToClient(sessionId: string, msg: InternalActivityStream) {
|
|
188
|
+
return this.getSocket(sessionId).then(
|
|
189
|
+
(socket: Socket) => {
|
|
190
|
+
try {
|
|
191
|
+
// this property should never be exposed externally
|
|
192
|
+
// biome-ignore lint/performance/noDelete: <explanation>
|
|
193
|
+
delete msg.sessionSecret;
|
|
194
|
+
} finally {
|
|
195
|
+
msg.context = this.name;
|
|
196
|
+
if (
|
|
197
|
+
msg.type === "error" &&
|
|
198
|
+
typeof msg.actor === "undefined" &&
|
|
199
|
+
this.actor
|
|
200
|
+
) {
|
|
201
|
+
// ensure an actor is present if not otherwise defined
|
|
202
|
+
msg.actor = { id: this.actor, type: "unknown" };
|
|
203
|
+
}
|
|
204
|
+
socket.emit("message", msg as ActivityStream);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
(err) => this.debug(`sendToClient ${err}`),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// send message to every connected socket associated with this platform instance.
|
|
212
|
+
private broadcastToSharedPeers(sessionId: string, msg: ActivityStream) {
|
|
213
|
+
for (const sid of this.sessions.values()) {
|
|
214
|
+
if (sid !== sessionId) {
|
|
215
|
+
this.debug(`broadcasting message to ${sid}`);
|
|
216
|
+
this.sendToClient(sid, msg);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// handle job results coming in on the queue from platform instances
|
|
222
|
+
private async handleJobResult(
|
|
223
|
+
state: string,
|
|
224
|
+
job: JobDataDecrypted,
|
|
225
|
+
result: ActivityStream | undefined,
|
|
226
|
+
) {
|
|
227
|
+
let payload = result; // some platforms return new AS objects as result
|
|
228
|
+
if (state === "failed") {
|
|
229
|
+
payload = job.msg; // failures always use original AS job object
|
|
230
|
+
payload.error = result
|
|
231
|
+
? result.toString()
|
|
232
|
+
: "job failed for unknown reason";
|
|
233
|
+
}
|
|
234
|
+
this.debug(
|
|
235
|
+
`${job.title} ${state}${payload?.error ? `: ${payload.error}` : ""}`,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (!payload || typeof payload === "string") {
|
|
239
|
+
payload = job.msg;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// send result to client
|
|
243
|
+
const callback = this.completedJobHandlers.get(job.title);
|
|
244
|
+
if (callback) {
|
|
245
|
+
callback(payload);
|
|
246
|
+
this.completedJobHandlers.delete(job.title);
|
|
247
|
+
} else {
|
|
248
|
+
await this.sendToClient(job.sessionId, payload);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (payload) {
|
|
252
|
+
// let all related peers know of result as an independent message
|
|
253
|
+
// (not as part of a job completion, or failure)
|
|
254
|
+
this.broadcastToSharedPeers(job.sessionId, payload);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// persistent
|
|
258
|
+
if (
|
|
259
|
+
this.config.persist &&
|
|
260
|
+
this.config.requireCredentials?.includes(job.msg.type)
|
|
261
|
+
) {
|
|
262
|
+
if (state === "failed") {
|
|
263
|
+
// Only terminate if platform is not yet initialized
|
|
264
|
+
// If already initialized, credential failures are non-fatal (wrong session credentials)
|
|
265
|
+
if (!this.config.initialized) {
|
|
266
|
+
this.debug(
|
|
267
|
+
`critical job type ${job.msg.type} failed during initialization, flagging for termination`,
|
|
268
|
+
);
|
|
269
|
+
await this.queue.pause();
|
|
270
|
+
this.config.initialized = false;
|
|
271
|
+
this.flaggedForTermination = true;
|
|
272
|
+
} else {
|
|
273
|
+
this.debug(
|
|
274
|
+
`credential job ${job.msg.type} failed on initialized platform, not flagged for termination`,
|
|
275
|
+
);
|
|
276
|
+
// Platform stays alive - error sent to client via sendToClient above
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
this.debug("persistent platform initialized");
|
|
280
|
+
await this.queue.resume();
|
|
281
|
+
this.config.initialized = true;
|
|
282
|
+
this.flaggedForTermination = false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Sends error message to client and clears all references to this class.
|
|
289
|
+
* @param sessionId
|
|
290
|
+
* @param errorMessage
|
|
291
|
+
*/
|
|
292
|
+
private async reportError(sessionId: string, errorMessage: string) {
|
|
293
|
+
const errorObject: ActivityStream = {
|
|
294
|
+
context: this.name,
|
|
295
|
+
type: "error",
|
|
296
|
+
actor: { id: this.actor, type: "unknown" },
|
|
297
|
+
error: errorMessage,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Only attempt to send to client if we have a valid session
|
|
301
|
+
try {
|
|
302
|
+
if (sessionId && this.sessions.has(sessionId)) {
|
|
303
|
+
await this.sendToClient(sessionId, errorObject);
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
this.debug(`Failed to send error to client: ${err.message}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.sessions.clear();
|
|
310
|
+
await this.shutdown();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Updates the instance with a new identifier, updating the platformInstances mapping as well.
|
|
315
|
+
* @param identifier
|
|
316
|
+
*/
|
|
317
|
+
private updateIdentifier(identifier: string) {
|
|
318
|
+
platformInstances.delete(this.id);
|
|
319
|
+
this.id = identifier;
|
|
320
|
+
platformInstances.set(this.id, this);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Generates a function tied to a given client session (socket connection), the generated
|
|
325
|
+
* function will be called for each session ID registered, for every platform emit.
|
|
326
|
+
* @param listener
|
|
327
|
+
* @param sessionId
|
|
328
|
+
*/
|
|
329
|
+
private callbackFunction(listener: string, sessionId: string) {
|
|
330
|
+
const funcs = {
|
|
331
|
+
close: async (e: object) => {
|
|
332
|
+
this.debug(`close event triggered ${this.id}: ${e}`);
|
|
333
|
+
// Check if process is still connected before attempting error reporting
|
|
334
|
+
if (this.process?.connected && !this.flaggedForTermination) {
|
|
335
|
+
await this.reportError(
|
|
336
|
+
sessionId,
|
|
337
|
+
`Error: session thread closed unexpectedly: ${e}`,
|
|
338
|
+
);
|
|
339
|
+
} else {
|
|
340
|
+
this.debug(
|
|
341
|
+
"Process already disconnected or flagged for termination, skipping error report",
|
|
342
|
+
);
|
|
343
|
+
await this.shutdown();
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
message: async ([first, second, third]: MessageFromPlatform) => {
|
|
347
|
+
if (first === "updateActor") {
|
|
348
|
+
// We need to update the key to the store in order to find it in the future.
|
|
349
|
+
this.updateIdentifier(third);
|
|
350
|
+
} else if (first === "error" && typeof second === "string") {
|
|
351
|
+
await this.reportError(sessionId, second);
|
|
352
|
+
} else {
|
|
353
|
+
// treat like a message to clients
|
|
354
|
+
await this.sendToClient(sessionId, second);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
return funcs[listener];
|
|
359
|
+
}
|
|
360
|
+
}
|