@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.
@@ -4,22 +4,89 @@
4
4
 
5
5
  import { createLogger } from "@sockethub/logger";
6
6
  import {
7
+ type ActivityObject,
7
8
  type ActivityStream,
8
- type MiddlewareCallback,
9
+ AS2_BASE_CONTEXT_URL,
10
+ buildCanonicalContext,
11
+ resolvePlatformId,
12
+ SOCKETHUB_BASE_CONTEXT_URL,
9
13
  validateActivityObject,
10
14
  validateActivityStream,
11
15
  validateCredentials,
12
16
  } from "@sockethub/schemas";
13
17
 
14
18
  import getInitObject, { type IInitObject } from "../bootstrap/init.js";
19
+ import type { MiddlewareChainInterface } from "../middleware.js";
15
20
 
16
21
  // called when registered with the middleware function, define the type of validation
17
22
  // that will be called when the middleware eventually does.
23
+ export default function validate<T extends ActivityObject>(
24
+ type: "activity-object",
25
+ sockethubId: string,
26
+ passedInitObj?: IInitObject,
27
+ ): (msg: T, done: MiddlewareChainInterface<T>) => void;
28
+ export default function validate<T extends ActivityStream>(
29
+ type: "credentials" | "message",
30
+ sockethubId: string,
31
+ passedInitObj?: IInitObject,
32
+ ): (msg: T, done: MiddlewareChainInterface<T>) => void;
18
33
  export default function validate(
19
34
  type: string,
20
35
  sockethubId: string,
21
36
  passedInitObj?: IInitObject,
22
37
  ) {
38
+ const baseContextUrls = new Set([
39
+ AS2_BASE_CONTEXT_URL,
40
+ SOCKETHUB_BASE_CONTEXT_URL,
41
+ ]);
42
+
43
+ const getLegacyContext = (stream: ActivityStream): string | undefined => {
44
+ const legacyContext = (stream as ActivityStream & { context?: unknown })
45
+ .context;
46
+ return typeof legacyContext === "string" ? legacyContext : undefined;
47
+ };
48
+
49
+ const getContextValues = (stream: ActivityStream): Array<string> => {
50
+ if (!Array.isArray(stream["@context"])) {
51
+ return [];
52
+ }
53
+ return stream["@context"].filter(
54
+ (value): value is string => typeof value === "string",
55
+ );
56
+ };
57
+
58
+ const getPlatformContextCandidates = (
59
+ stream: ActivityStream,
60
+ ): Array<string> => {
61
+ return getContextValues(stream).filter(
62
+ (value) => !baseContextUrls.has(value),
63
+ );
64
+ };
65
+
66
+ const normalizeLegacyContext = (
67
+ stream: ActivityStream,
68
+ initObj: IInitObject,
69
+ ) => {
70
+ if (resolvePlatformId(stream)) {
71
+ return;
72
+ }
73
+ const legacyContext = getLegacyContext(stream);
74
+ if (!legacyContext) {
75
+ return;
76
+ }
77
+ const platformMeta = initObj.platforms.get(legacyContext);
78
+ if (!platformMeta) {
79
+ return;
80
+ }
81
+ const platformContextUrl =
82
+ platformMeta.contextUrl ||
83
+ `https://sockethub.org/ns/context/platform/${legacyContext}/v1.jsonld`;
84
+ // Intentionally mutate the incoming stream so legacy `context` payloads
85
+ // continue through canonical @context/platform validation paths.
86
+ stream["@context"] = buildCanonicalContext(platformContextUrl);
87
+ stream.platform = legacyContext;
88
+ };
89
+
23
90
  let initObj = passedInitObj;
24
91
  if (!passedInitObj) {
25
92
  getInitObject().then((init) => {
@@ -28,45 +95,85 @@ export default function validate(
28
95
  }
29
96
 
30
97
  const sessionLog = createLogger(`server:validate:${sockethubId}`);
31
- return (msg: ActivityStream, done: MiddlewareCallback) => {
98
+ return (
99
+ msg: ActivityStream | ActivityObject,
100
+ done: MiddlewareChainInterface<ActivityStream | ActivityObject>,
101
+ ) => {
32
102
  sessionLog.debug(`applying schema validation for ${type}`);
33
103
  if (type === "activity-object") {
34
- const err = validateActivityObject(msg);
35
- if (err) {
36
- done(new Error(err));
37
- } else {
38
- done(msg);
39
- }
40
- } else if (!initObj.platforms.has(msg.context)) {
41
- return done(
42
- new Error(
43
- `platform context ${msg.context} not registered with this Sockethub instance.`,
44
- ),
45
- );
46
- } else if (type === "credentials") {
47
- const err = validateCredentials(msg);
104
+ const err = validateActivityObject(msg as ActivityObject);
48
105
  if (err) {
49
106
  done(new Error(err));
50
107
  } else {
51
108
  done(msg);
52
109
  }
53
110
  } else {
54
- const err = validateActivityStream(msg);
55
- if (err) {
56
- done(new Error(err));
111
+ if (!initObj) {
112
+ return done(
113
+ new Error(
114
+ "Sockethub platforms not initialized for validation.",
115
+ ),
116
+ );
117
+ }
118
+ const stream = msg as ActivityStream;
119
+ // Intentional mutation for backward compatibility with legacy `context`.
120
+ normalizeLegacyContext(stream, initObj);
121
+ const platformId = resolvePlatformId(stream);
122
+ if (!platformId) {
123
+ const platformContextCandidates =
124
+ getPlatformContextCandidates(stream);
125
+ const contextDetails =
126
+ platformContextCandidates.length === 0
127
+ ? " No platform @context values were provided."
128
+ : platformContextCandidates.length > 1
129
+ ? ` Multiple platform @context values were provided: ${platformContextCandidates.join(", ")}.`
130
+ : ` Unregistered platform @context value: ${platformContextCandidates[0]}`;
131
+ return done(
132
+ new Error(
133
+ `platform context URL not registered with this Sockethub instance.${contextDetails}`,
134
+ ),
135
+ );
136
+ }
137
+ if (!initObj.platforms.has(platformId)) {
138
+ return done(
139
+ new Error(
140
+ `platform ${platformId} resolved from @context is not enabled in this Sockethub instance.`,
141
+ ),
142
+ );
143
+ }
144
+ stream.platform = platformId;
145
+ if (type === "credentials") {
146
+ const err = validateCredentials(stream);
147
+ if (err) {
148
+ done(new Error(err));
149
+ } else {
150
+ done(stream);
151
+ }
57
152
  } else {
58
- const platformMeta = initObj.platforms.get(msg.context);
59
- if (!platformMeta.types.includes(msg.type)) {
60
- return done(
61
- new Error(
62
- `platform type ${msg.type} not supported by ${msg.context} ` +
63
- `platform. (types: ${platformMeta.types.join(
64
- ", ",
65
- )})`,
66
- ),
67
- );
153
+ const err = validateActivityStream(stream);
154
+ if (err) {
155
+ done(new Error(err));
156
+ } else {
157
+ const platformMeta = initObj.platforms.get(platformId);
158
+ if (!platformMeta) {
159
+ return done(
160
+ new Error(
161
+ `platform ${platformId} not registered with this Sockethub instance.`,
162
+ ),
163
+ );
164
+ }
165
+ if (!platformMeta.types.includes(stream.type)) {
166
+ return done(
167
+ new Error(
168
+ `platform type ${stream.type} not supported by ${platformId} ` +
169
+ `platform. (types: ${platformMeta.types.join(
170
+ ", ",
171
+ )})`,
172
+ ),
173
+ );
174
+ }
175
+ done(stream);
68
176
  }
69
- done(msg);
70
177
  }
71
178
  }
72
179
  };
package/src/middleware.ts CHANGED
@@ -5,24 +5,27 @@ import type {
5
5
  Logger,
6
6
  } from "@sockethub/schemas";
7
7
 
8
- export default function middleware(name: string): MiddlewareChain {
9
- return new MiddlewareChain(name);
8
+ export default function middleware<T>(name: string): MiddlewareChain<T> {
9
+ return new MiddlewareChain<T>(name);
10
10
  }
11
11
 
12
- export type MiddlewareChainInterface = (
13
- error: ActivityStream | ActivityObject | Error,
14
- data?: ActivityStream | ActivityObject | MiddlewareChainInterface,
15
- next?: MiddlewareChainInterface,
16
- ) => void;
12
+ export type MiddlewareNext<T> = (data?: T | Error) => void;
17
13
 
18
- type ErrorHandlerInterface = (err: Error, data?: unknown, cb?: unknown) => void;
14
+ export type MiddlewareHandler<T> = (data: T, next: MiddlewareNext<T>) => void;
15
+ export type MiddlewareErrorHandler<T> = (
16
+ err: Error,
17
+ data: T,
18
+ next: MiddlewareNext<T>,
19
+ ) => void;
19
20
 
20
21
  export type LogErrorInterface = (msg: Error) => void;
22
+ export type MiddlewareChainInterface<T = ActivityStream | ActivityObject> =
23
+ MiddlewareNext<T>;
21
24
 
22
- export class MiddlewareChain {
25
+ export class MiddlewareChain<T> {
23
26
  public name: string;
24
- private chain: Array<MiddlewareChainInterface> = [];
25
- private errHandler: ErrorHandlerInterface = (err: Error) => {
27
+ private chain: Array<MiddlewareHandler<T>> = [];
28
+ private errHandler: MiddlewareErrorHandler<T> = (err: Error) => {
26
29
  throw err;
27
30
  };
28
31
  private readonly logger: Logger;
@@ -32,16 +35,16 @@ export class MiddlewareChain {
32
35
  this.logger = createLogger(`middleware:${name}`);
33
36
  }
34
37
 
35
- use(func: ErrorHandlerInterface | MiddlewareChainInterface): this {
38
+ use(func: MiddlewareErrorHandler<T> | MiddlewareHandler<T>): this {
36
39
  if (typeof func !== "function") {
37
40
  throw new Error(
38
41
  "middleware use() can only take a function as an argument",
39
42
  );
40
43
  }
41
44
  if (func.length === 3) {
42
- this.errHandler = func as ErrorHandlerInterface;
45
+ this.errHandler = func as MiddlewareErrorHandler<T>;
43
46
  } else if (func.length === 2) {
44
- this.chain.push(func as MiddlewareChainInterface);
47
+ this.chain.push(func as MiddlewareHandler<T>);
45
48
  } else {
46
49
  throw new Error(
47
50
  `middleware function provided with incorrect number of params: ${func.length}`,
@@ -51,22 +54,23 @@ export class MiddlewareChain {
51
54
  }
52
55
 
53
56
  done() {
54
- return (data: unknown, callbackFunc: MiddlewareChainInterface) => {
55
- let cb = callbackFunc;
57
+ return (data: T, callbackFunc: MiddlewareNext<T>) => {
58
+ let cb = callbackFunc as MiddlewareNext<T>;
56
59
  let position = 0;
57
60
  if (typeof callbackFunc !== "function") {
58
61
  cb = () => {
59
62
  // ensure we have a callback function
60
63
  };
61
64
  }
62
- const next = (_data: unknown) => {
63
- if (_data instanceof Error) {
64
- this.logger.error(_data.toString());
65
- this.errHandler(_data, data, cb);
65
+ const next: MiddlewareNext<T> = (_data) => {
66
+ const nextData = _data === undefined ? data : _data;
67
+ if (nextData instanceof Error) {
68
+ this.logger.error(nextData.toString());
69
+ this.errHandler(nextData, data, cb);
66
70
  } else if (typeof this.chain[position] === "function") {
67
- this.chain[position++](_data as ActivityStream, next);
71
+ this.chain[position++](nextData as T, next);
68
72
  } else {
69
- cb(_data as ActivityStream);
73
+ cb(nextData as T);
70
74
  }
71
75
  };
72
76
  next(data);
@@ -1,5 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
2
  import * as sinon from "sinon";
3
+ import {
4
+ buildCanonicalContext,
5
+ INTERNAL_PLATFORM_CONTEXT_URL,
6
+ } from "@sockethub/schemas";
3
7
  import { __dirname } from "./util.js";
4
8
  const FORK_PATH = __dirname + "/platform.js";
5
9
 
@@ -173,8 +177,37 @@ describe("PlatformInstance", () => {
173
177
  sandbox.assert.calledOnce(socketMock.emit);
174
178
  sandbox.assert.calledWith(socketMock.emit, "message", {
175
179
  foo: "this is a message object",
176
- context: "a platform name",
180
+ platform: "a platform name",
181
+ "@context": buildCanonicalContext(
182
+ INTERNAL_PLATFORM_CONTEXT_URL,
183
+ ),
184
+ });
185
+ });
186
+
187
+ it("injects platform-specific @context when contextUrl is set", async () => {
188
+ const testContextUrl =
189
+ "https://sockethub.org/ns/context/platform/dummy/v1.jsonld";
190
+ pi.contextUrl = testContextUrl;
191
+ await pi.sendToClient("my session id", {
192
+ type: "echo",
193
+ actor: { id: "test@dummy", type: "person" },
194
+ });
195
+ sandbox.assert.calledOnce(socketMock.emit);
196
+ const emittedMsg = socketMock.emit.firstCall.args[1];
197
+ expect(emittedMsg["@context"]).toEqual(
198
+ buildCanonicalContext(testContextUrl),
199
+ );
200
+ });
201
+
202
+ it("strips legacy context field from outbound payloads", async () => {
203
+ await pi.sendToClient("my session id", {
204
+ type: "echo",
205
+ context: "dummy",
206
+ actor: { id: "test@dummy", type: "person" },
177
207
  });
208
+ const emittedMsg = socketMock.emit.firstCall.args[1];
209
+ expect(emittedMsg.context).toBeUndefined();
210
+ expect(emittedMsg["@context"]).toBeDefined();
178
211
  });
179
212
 
180
213
  it("broadcasts to peers", async () => {
@@ -212,6 +245,7 @@ describe("PlatformInstance", () => {
212
245
  expect(pi.broadcastToSharedPeers.callCount).toEqual(1);
213
246
  sandbox.assert.calledWith(pi.sendToClient, "a session id", {
214
247
  foo: "bar",
248
+ platform: "a platform name",
215
249
  });
216
250
  });
217
251
 
@@ -225,6 +259,7 @@ describe("PlatformInstance", () => {
225
259
  sandbox.assert.calledWith(pi.sendToClient, "a session id", {
226
260
  foo: "bar",
227
261
  error: "a bad result message",
262
+ platform: "a platform name",
228
263
  });
229
264
  });
230
265
  });
@@ -353,7 +388,7 @@ describe("PlatformInstance", () => {
353
388
  sessionId: "session123",
354
389
  msg: {
355
390
  type: "connect",
356
- context: "xmpp",
391
+ platform: "xmpp",
357
392
  actor: { id: "testuser@localhost", type: "person" },
358
393
  },
359
394
  title: "xmpp-1",
@@ -405,7 +440,7 @@ describe("PlatformInstance", () => {
405
440
  sessionId: "session123",
406
441
  msg: {
407
442
  type: "connect",
408
- context: "xmpp",
443
+ platform: "xmpp",
409
444
  actor: { id: "testuser@localhost", type: "person" },
410
445
  },
411
446
  title: "xmpp-1",
@@ -424,7 +459,7 @@ describe("PlatformInstance", () => {
424
459
  sessionId: "session456",
425
460
  msg: {
426
461
  type: "send",
427
- context: "xmpp",
462
+ platform: "xmpp",
428
463
  actor: { id: "testuser@localhost", type: "person" },
429
464
  },
430
465
  title: "xmpp-2",
@@ -465,7 +500,7 @@ describe("PlatformInstance", () => {
465
500
  sessionId: "session123",
466
501
  msg: {
467
502
  type: "connect",
468
- context: "xmpp",
503
+ platform: "xmpp",
469
504
  actor: { id: "testuser@localhost", type: "person" },
470
505
  },
471
506
  title: "xmpp-1",
@@ -514,7 +549,7 @@ describe("PlatformInstance", () => {
514
549
  sessionId: "session123",
515
550
  msg: {
516
551
  type: "connect",
517
- context: "xmpp",
552
+ platform: "xmpp",
518
553
  actor: { id: "testuser@localhost", type: "person" },
519
554
  },
520
555
  title: "xmpp-1",