@sockethub/activity-streams 4.4.0-alpha.5 → 4.4.0-alpha.7

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/package.json CHANGED
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "@sockethub/activity-streams",
3
- "version": "4.4.0-alpha.5",
3
+ "version": "4.4.0-alpha.7",
4
4
  "description": "A simple tool to facilitate handling and referencing activity streams without unnecessary verbosity",
5
5
  "type": "module",
6
- "main": "./src/activity-streams.ts",
6
+ "main": "./dist/activity-streams.js",
7
+ "exports": {
8
+ ".": {
9
+ "bun": "./src/activity-streams.ts",
10
+ "import": "./dist/activity-streams.js",
11
+ "default": "./dist/activity-streams.js"
12
+ }
13
+ },
7
14
  "files": [
15
+ "src/",
8
16
  "dist/"
9
17
  ],
10
18
  "engines": {
@@ -23,6 +31,9 @@
23
31
  ],
24
32
  "author": "Nick Jennings <nick@silverbucket.net>",
25
33
  "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
26
37
  "bugs": {
27
38
  "url": "https://github.com/sockethub/sockethub/issues"
28
39
  },
@@ -35,13 +46,13 @@
35
46
  "test:browser": "wtr dist/**/*.test.js --node-resolve --puppeteer"
36
47
  },
37
48
  "dependencies": {
38
- "eventemitter3": "5.0.1"
49
+ "eventemitter3": "^5.0.4"
39
50
  },
40
51
  "devDependencies": {
41
- "@sockethub/schemas": "3.0.0-alpha.5",
52
+ "@sockethub/schemas": "^3.0.0-alpha.7",
42
53
  "@types/bun": "latest",
43
54
  "@web/test-runner": "^0.19.0",
44
55
  "@web/test-runner-puppeteer": "^0.17.0"
45
56
  },
46
- "gitHead": "341ea9eeca6afd1442fe6e01457bc21d112b91a4"
57
+ "gitHead": "38927776c210a22bc54ceb86ccc7276c5f27b463"
47
58
  }
@@ -0,0 +1,302 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import type { ActivityStream } from "@sockethub/schemas";
3
+ import { ASFactory } from "./activity-streams";
4
+
5
+ describe("warn test", () => {
6
+ expect(typeof ASFactory).toEqual("function");
7
+ const activity = ASFactory();
8
+ test("rejects nondefined special types", () => {
9
+ expect(() => {
10
+ activity.Stream({
11
+ type: "lol",
12
+ platform: "irc",
13
+ actor: "thingy",
14
+ object: { type: "hola", content: "har", secure: true },
15
+ target: ["thingy1", "thingy2"],
16
+ });
17
+ }).toThrow(Error);
18
+ });
19
+ });
20
+
21
+ describe("no special props", () => {
22
+ const activity = ASFactory();
23
+ test("init", () => {
24
+ expect(typeof ASFactory).toEqual("function");
25
+ });
26
+
27
+ test("returns expected object with no special types", () => {
28
+ expect(
29
+ activity.Stream({
30
+ type: "send",
31
+ context: "irc",
32
+ actor: "thingy",
33
+ object: { type: "hola", content: "har" },
34
+ target: ["thingy1", "thingy2"],
35
+ }),
36
+ ).toEqual({
37
+ type: "send",
38
+ context: "irc",
39
+ actor: { id: "thingy" },
40
+ object: { type: "hola", content: "har" },
41
+ target: ["thingy1", "thingy2"],
42
+ });
43
+ });
44
+ });
45
+
46
+ describe("basic tests", () => {
47
+ const config = {
48
+ customProps: {
49
+ credentials: ["secure"],
50
+ dude: ["foo", "secure"],
51
+ },
52
+ specialObjs: ["dude"],
53
+ failOnUnknownObjectProperties: true,
54
+ };
55
+
56
+ const activity = ASFactory(config);
57
+ test("init", () => {
58
+ expect(typeof ASFactory).toEqual("function");
59
+ });
60
+
61
+ describe("object tests", () => {
62
+ test("has expected structure", () => {
63
+ expect(typeof activity).toEqual("object");
64
+ expect(typeof activity.Object).toEqual("object");
65
+ expect(typeof activity.Stream).toEqual("function");
66
+ expect(typeof activity.on).toEqual("function");
67
+ expect(typeof activity.once).toEqual("function");
68
+ expect(typeof activity.off).toEqual("function");
69
+ });
70
+
71
+ test("returns undefined when no params are passed", () => {
72
+ // @ts-ignore
73
+ expect(activity.Object.get()).toBeUndefined();
74
+ });
75
+
76
+ test("returns object when given a valid lookup id", () => {
77
+ expect(activity.Object.create({ id: "thingy1" })).toEqual({
78
+ id: "thingy1",
79
+ });
80
+ expect(activity.Object.get("thingy1")).toEqual({
81
+ id: "thingy1",
82
+ });
83
+ });
84
+
85
+ test("throws an exception when called with no identifier", () => {
86
+ expect(activity.Object.create).toThrow(Error);
87
+ });
88
+
89
+ test("creates a second object and returns is as expected", () => {
90
+ expect(activity.Object.create({ id: "thingy2" })).toEqual({
91
+ id: "thingy2",
92
+ });
93
+ expect(activity.Object.get("thingy2")).toEqual({
94
+ id: "thingy2",
95
+ });
96
+ });
97
+
98
+ test("returns a basic ActivtyObject when receiving an unknown id with expand=true", () => {
99
+ expect(activity.Object.get("thingy3", true)).toEqual({
100
+ id: "thingy3",
101
+ });
102
+ });
103
+
104
+ test("returns given id param when lookup fails and expand=false", () => {
105
+ expect(
106
+ // @ts-ignore
107
+ activity.Object.get({
108
+ id: "thingy3",
109
+ foo: "bar",
110
+ }),
111
+ ).toEqual({
112
+ id: "thingy3",
113
+ foo: "bar",
114
+ });
115
+ });
116
+ });
117
+
118
+ interface TestActivityStream extends ActivityStream {
119
+ type: string;
120
+ verb?: string;
121
+ context: string;
122
+ platform?: string;
123
+ }
124
+
125
+ describe("stream tests", () => {
126
+ let stream: TestActivityStream;
127
+
128
+ beforeEach(() => {
129
+ stream = activity.Stream({
130
+ verb: "lol",
131
+ platform: "irc",
132
+ actor: "thingy1",
133
+ context: "irc",
134
+ object: {
135
+ objectType: "credentials",
136
+ content: "har",
137
+ secure: true,
138
+ },
139
+ target: ["thingy1", "thingy2"],
140
+ }) as ActivityStream;
141
+ });
142
+
143
+ test("renames mapped props", () => {
144
+ expect(stream.type).toEqual("lol");
145
+ expect(stream.verb).toBeUndefined();
146
+ expect(stream.context).toEqual("irc");
147
+ expect(stream.platform).toBeUndefined();
148
+ });
149
+
150
+ test("expands existing objects", () => {
151
+ // @ts-ignore
152
+ expect(stream.target).toEqual([
153
+ { id: "thingy1" },
154
+ { id: "thingy2" },
155
+ ]);
156
+ // @ts-ignore
157
+ expect(stream.actor).toEqual({ id: "thingy1" });
158
+ });
159
+
160
+ test("handles customProps as expected", () => {
161
+ expect(stream.object).toEqual({
162
+ type: "credentials",
163
+ content: "har",
164
+ secure: true,
165
+ });
166
+ expect(stream.object.objectType).toBeUndefined();
167
+ });
168
+
169
+ test("respects specialObj properties", () => {
170
+ const stream2 = activity.Stream({
171
+ type: "lol",
172
+ platform: "irc",
173
+ actor: "thingy",
174
+ object: {
175
+ type: "dude",
176
+ foo: "bar",
177
+ content: "har",
178
+ secure: true,
179
+ },
180
+ target: ["thingy1", "thingy2"],
181
+ });
182
+ expect(stream2).toEqual({
183
+ type: "lol",
184
+ context: "irc",
185
+ actor: { id: "thingy" },
186
+ target: [{ id: "thingy1" }, { id: "thingy2" }],
187
+ object: {
188
+ type: "dude",
189
+ foo: "bar",
190
+ content: "har",
191
+ secure: true,
192
+ },
193
+ });
194
+ });
195
+
196
+ test("rejects nondefined special types", () => {
197
+ expect(() => {
198
+ activity.Stream({
199
+ type: "lol",
200
+ platform: "irc",
201
+ actor: "thingy",
202
+ object: {
203
+ type: "hola",
204
+ foo: "bar",
205
+ content: "har",
206
+ secure: true,
207
+ },
208
+ target: ["thingy1", "thingy2"],
209
+ });
210
+ }).toThrow('ActivityStreams validation failed: property "foo" with value "bar" is not allowed on the "object" object of type "hola".');
211
+ });
212
+ });
213
+
214
+ describe("emitters", () => {
215
+ test("emits an event on object creation", () => {
216
+ function onHandler(obj) {
217
+ expect(obj).toEqual({ id: "thingy3" });
218
+ activity.off("activity-object-create", onHandler);
219
+ }
220
+ activity.on("activity-object-create", onHandler);
221
+ activity.Object.create({ id: "thingy3" });
222
+ });
223
+
224
+ test("emits an event on object deletion", () => {
225
+ activity.once("activity-object-delete", (id) => {
226
+ expect(id).toEqual("thingy2");
227
+ });
228
+ activity.Object.delete("thingy2");
229
+ });
230
+ });
231
+ });
232
+
233
+ describe("Object.create error handling tests", () => {
234
+ const activity = ASFactory();
235
+
236
+ test("throws clear error when passed null", () => {
237
+ expect(() => {
238
+ // @ts-ignore - intentionally testing invalid input
239
+ activity.Object.create(null);
240
+ }).toThrow('ActivityStreams validation failed: the "object" property is null. Example: { id: "user@example.com", type: "person" }');
241
+ });
242
+
243
+ test("throws clear error when passed undefined", () => {
244
+ expect(() => {
245
+ // @ts-ignore - intentionally testing invalid input
246
+ activity.Object.create(undefined);
247
+ }).toThrow('ActivityStreams validation failed: the "object" property is undefined. Example: { id: "user@example.com", type: "person" }');
248
+ });
249
+
250
+ test("throws clear error when passed string", () => {
251
+ expect(() => {
252
+ // @ts-ignore - intentionally testing invalid input
253
+ activity.Object.create("string value");
254
+ }).toThrow('ActivityStreams validation failed: the "object" property received string "string value" but expected an object. Use: { id: "string value", type: "person" }');
255
+ });
256
+
257
+ test("throws clear error when passed number", () => {
258
+ expect(() => {
259
+ // @ts-ignore - intentionally testing invalid input
260
+ activity.Object.create(123);
261
+ }).toThrow('ActivityStreams validation failed: the "object" property must be an object, received number (123). Example: { id: "user@example.com", type: "person" }');
262
+ });
263
+
264
+ test("throws clear error when passed boolean false", () => {
265
+ expect(() => {
266
+ // @ts-ignore - intentionally testing invalid input
267
+ activity.Object.create(false);
268
+ }).toThrow('ActivityStreams validation failed: the "object" property must be an object, received boolean (false). Example: { id: "user@example.com", type: "person" }');
269
+ });
270
+
271
+ test("throws clear error when passed boolean true", () => {
272
+ expect(() => {
273
+ // @ts-ignore - intentionally testing invalid input
274
+ activity.Object.create(true);
275
+ }).toThrow('ActivityStreams validation failed: the "object" property must be an object, received boolean (true). Example: { id: "user@example.com", type: "person" }');
276
+ });
277
+
278
+ test("throws clear error when passed array", () => {
279
+ expect(() => {
280
+ // @ts-ignore - intentionally testing invalid input
281
+ activity.Object.create([]);
282
+ }).toThrow('ActivityStreams validation failed: the "object" property must be an object, received array (). Example: { id: "user@example.com", type: "person" }');
283
+ });
284
+
285
+ test("throws clear error when passed empty object (missing required id)", () => {
286
+ expect(() => {
287
+ activity.Object.create({});
288
+ }).toThrow('ActivityStreams validation failed: the "object" property requires an \'id\' property. Example: { id: "user@example.com", type: "person" }');
289
+ });
290
+
291
+ test("throws clear error when object has properties but missing id", () => {
292
+ expect(() => {
293
+ activity.Object.create({ type: "person", name: "John" });
294
+ }).toThrow('ActivityStreams validation failed: the "object" property requires an \'id\' property. Example: { id: "user@example.com", type: "person" }');
295
+ });
296
+
297
+ test("handles object with only id property", () => {
298
+ expect(() => {
299
+ activity.Object.create({ id: "test-id" });
300
+ }).not.toThrow();
301
+ });
302
+ });
@@ -0,0 +1,344 @@
1
+ /*!
2
+ * activity-streams
3
+ * https://github.com/silverbucket/activity-streams
4
+ *
5
+ * Developed and Maintained by:
6
+ * Nick Jennings <nick@silverbucket.net>
7
+ *
8
+ * activity-streams is released under the MIT (see LICENSE).
9
+ *
10
+ * You don't have to do anything special to choose one license or the other
11
+ * and you don't have to notify anyone which license you are using.
12
+ * Please see the corresponding license file for details of these licenses.
13
+ * You are free to use, modify and distribute this software, but all copyright
14
+ * information must remain.
15
+ *
16
+ */
17
+
18
+ import type { ActivityObject, ActivityStream } from "@sockethub/schemas";
19
+ import EventEmitter from "eventemitter3";
20
+
21
+ const ee = new EventEmitter();
22
+ const baseProps = {
23
+ stream: [
24
+ "id",
25
+ "type",
26
+ "actor",
27
+ "target",
28
+ "object",
29
+ "context",
30
+ "context",
31
+ "published",
32
+ "error",
33
+ ],
34
+ object: [
35
+ "id",
36
+ "type",
37
+ "context",
38
+ "alias",
39
+ "attachedTo",
40
+ "attachment",
41
+ "attributedTo",
42
+ "attributedWith",
43
+ "condition",
44
+ "content",
45
+ "contentMap",
46
+ "context",
47
+ "contextOf",
48
+ "name",
49
+ "endTime",
50
+ "generator",
51
+ "generatorOf",
52
+ "group",
53
+ "icon",
54
+ "image",
55
+ "inReplyTo",
56
+ "members",
57
+ "memberOf",
58
+ "message",
59
+ "location",
60
+ "locationOf",
61
+ "objectOf",
62
+ "originOf",
63
+ "presence",
64
+ "preview",
65
+ "previewOf",
66
+ "provider",
67
+ "providerOf",
68
+ "published",
69
+ "rating",
70
+ "relationship",
71
+ "resultOf",
72
+ "replies",
73
+ "role",
74
+ "scope",
75
+ "scopeOf",
76
+ "startTime",
77
+ "status",
78
+ "summary",
79
+ "topic",
80
+ "tag",
81
+ "tagOf",
82
+ "targetOf",
83
+ "title",
84
+ "titleMap",
85
+ "updated",
86
+ "url",
87
+ "xmpp:stanza-id",
88
+ ],
89
+ };
90
+ const rename = {
91
+ "@id": "id",
92
+ "@type": "type",
93
+ verb: "type",
94
+ displayName: "name",
95
+ objectType: "type",
96
+ platform: "context",
97
+ };
98
+ const expand = {
99
+ actor: {
100
+ primary: "id",
101
+ props: baseProps,
102
+ },
103
+ target: {
104
+ primary: "id",
105
+ props: baseProps,
106
+ },
107
+ object: {
108
+ primary: "content",
109
+ props: baseProps,
110
+ },
111
+ };
112
+
113
+ type CustomProps = {
114
+ [key: string]: string | number | boolean | object | string[];
115
+ };
116
+
117
+ const objs = new Map();
118
+ const customProps: CustomProps = {};
119
+
120
+ let failOnUnknownObjectProperties = false;
121
+ let warnOnUnknownObjectProperties = true;
122
+ let specialObjs = []; // the objects don't get rejected for bad props
123
+
124
+ function matchesCustomProp(type: string, key: string) {
125
+ if (customProps[type] instanceof Object) {
126
+ const obj = customProps[type] as string[];
127
+ if (obj.includes(key)) {
128
+ return true;
129
+ }
130
+ }
131
+ return false;
132
+ }
133
+
134
+ function renameProp<T>(obj: T, key: string): T {
135
+ obj[rename[key]] = obj[key];
136
+ delete obj[key];
137
+ return obj;
138
+ }
139
+
140
+ function validateObject<T>(type: string, incomingObj: T, requireId = false) {
141
+ // Input validation with clear error messages
142
+ if (incomingObj === null) {
143
+ throw new Error(
144
+ `ActivityStreams validation failed: the "${type}" property is null. Example: { id: "user@example.com", type: "person" }`,
145
+ );
146
+ }
147
+
148
+ if (incomingObj === undefined) {
149
+ throw new Error(
150
+ `ActivityStreams validation failed: the "${type}" property is undefined. Example: { id: "user@example.com", type: "person" }`,
151
+ );
152
+ }
153
+
154
+ if (typeof incomingObj === "string") {
155
+ throw new Error(
156
+ `ActivityStreams validation failed: the "${type}" property received string "${incomingObj}" but expected an object. Use: { id: "${incomingObj}", type: "person" }`,
157
+ );
158
+ }
159
+
160
+ if (typeof incomingObj !== "object" || Array.isArray(incomingObj)) {
161
+ const receivedType = Array.isArray(incomingObj)
162
+ ? "array"
163
+ : typeof incomingObj;
164
+ const receivedValue = String(incomingObj);
165
+ throw new Error(
166
+ `ActivityStreams validation failed: the "${type}" property must be an object, received ${receivedType} (${receivedValue}). Example: { id: "user@example.com", type: "person" }`,
167
+ );
168
+ }
169
+
170
+ // Require 'id' property when explicitly requested (e.g., Object.create())
171
+ const obj = incomingObj as ActivityObject;
172
+ if (requireId && !obj.id) {
173
+ throw new Error(
174
+ `ActivityStreams validation failed: the "${type}" property requires an 'id' property. Example: { id: "user@example.com", type: "person" }`,
175
+ );
176
+ }
177
+
178
+ const unknownKeys = Object.keys(incomingObj).filter(
179
+ (key: string): boolean => {
180
+ return !baseProps[type].includes(key);
181
+ },
182
+ );
183
+
184
+ for (const key of unknownKeys) {
185
+ let ao: ActivityObject = incomingObj as ActivityObject;
186
+ if (rename[key]) {
187
+ // rename property instead of fail
188
+ ao = renameProp(ao, key);
189
+ continue;
190
+ }
191
+
192
+ if (matchesCustomProp(ao.type, key)) {
193
+ // custom property matches, continue
194
+ continue;
195
+ }
196
+
197
+ if (!specialObjs.includes(ao.type)) {
198
+ // not defined as a special prop
199
+ // don't know what to do with it, so throw error
200
+ console.log(ao);
201
+ const receivedValue =
202
+ typeof ao[key] === "string" ? `"${ao[key]}"` : String(ao[key]);
203
+ const err = `ActivityStreams validation failed: property "${key}" with value ${receivedValue} is not allowed on the "${type}" object of type "${ao.type}".`;
204
+ if (failOnUnknownObjectProperties) {
205
+ throw new Error(err);
206
+ }
207
+ if (warnOnUnknownObjectProperties) {
208
+ console.warn(err);
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ function ensureProps(obj: ActivityObject): ActivityObject {
215
+ // ensure the name property, which can generally be inferred from the id
216
+ // name = obj.match(/(?(?\w+):\/\/)(?:.+@)?(.+?)(?:\/|$)/)[1]
217
+ return obj;
218
+ }
219
+
220
+ function expandStream(meta: ActivityStream) {
221
+ const stream = {};
222
+ for (const key of Object.keys(meta)) {
223
+ if (typeof meta[key] === "string") {
224
+ stream[key] = objs.get(meta[key]) || meta[key];
225
+ } else if (Array.isArray(meta[key])) {
226
+ stream[key] = [];
227
+ for (const entry of meta[key]) {
228
+ if (typeof entry === "string") {
229
+ stream[key].push(objs.get(entry) || entry);
230
+ }
231
+ }
232
+ } else {
233
+ stream[key] = meta[key];
234
+ }
235
+ }
236
+
237
+ // only expand string into objects if they are in the expand list
238
+ for (const key of Object.keys(expand)) {
239
+ if (typeof stream[key] === "string") {
240
+ const idx = expand[key].primary;
241
+ const obj = {};
242
+ obj[idx] = stream[key];
243
+ stream[key] = obj;
244
+ }
245
+ }
246
+ return stream;
247
+ }
248
+
249
+ function Stream(
250
+ meta: ActivityStream,
251
+ ): ActivityStream | ActivityObject | Record<string, never> {
252
+ validateObject("stream", meta);
253
+ if (typeof meta.object === "object") {
254
+ validateObject("object", meta.object);
255
+ }
256
+ const stream = expandStream(meta);
257
+ ee.emit("activity-stream", stream);
258
+ return stream;
259
+ }
260
+
261
+ export interface ActivityObjectManager {
262
+ create(obj: unknown): unknown;
263
+ delete(id: string): boolean;
264
+ list(): IterableIterator<unknown>;
265
+ get(id: string, expand?: boolean): unknown;
266
+ }
267
+
268
+ const _Object: ActivityObjectManager = {
269
+ create: (obj: ActivityObject) => {
270
+ validateObject("object", obj, true); // require ID for Object.create()
271
+ const ao = ensureProps(obj);
272
+ objs.set(ao.id, ao);
273
+ ee.emit("activity-object-create", ao);
274
+ return ao;
275
+ },
276
+
277
+ delete: (id) => {
278
+ const result = objs.delete(id);
279
+ if (result) {
280
+ ee.emit("activity-object-delete", id);
281
+ }
282
+ return result;
283
+ },
284
+
285
+ get: (id, expand) => {
286
+ let obj = objs.get(id);
287
+ if (!obj) {
288
+ if (!expand) {
289
+ return id;
290
+ }
291
+ obj = { id: id };
292
+ }
293
+ return ensureProps(obj);
294
+ },
295
+
296
+ list: (): IterableIterator<unknown> => objs.keys(),
297
+ };
298
+
299
+ export interface ASFactoryOptions {
300
+ specialObjs?: Array<string>;
301
+ failOnUnknownObjectProperties?: boolean;
302
+ warnOnUnknownObjectProperties?: boolean;
303
+ customProps?: CustomProps;
304
+ }
305
+
306
+ type EventCallback = (...args: unknown[]) => void;
307
+
308
+ export interface ASManager {
309
+ Stream(meta: unknown): ActivityStream | ActivityObject;
310
+ Object: ActivityObjectManager;
311
+ on(event: string, func: EventCallback): void;
312
+ once(event: string, func: EventCallback): void;
313
+ off(event: string, func: EventCallback): void;
314
+ }
315
+
316
+ export function ASFactory(opts: ASFactoryOptions = {}): ASManager {
317
+ specialObjs = opts?.specialObjs || [];
318
+ failOnUnknownObjectProperties =
319
+ typeof opts.failOnUnknownObjectProperties === "boolean"
320
+ ? opts.failOnUnknownObjectProperties
321
+ : failOnUnknownObjectProperties;
322
+ warnOnUnknownObjectProperties =
323
+ typeof opts.warnOnUnknownObjectProperties === "boolean"
324
+ ? opts.warnOnUnknownObjectProperties
325
+ : warnOnUnknownObjectProperties;
326
+ for (const propName of Object.keys(opts.customProps || {})) {
327
+ if (typeof opts.customProps[propName] === "object") {
328
+ customProps[propName] = opts.customProps[propName];
329
+ }
330
+ }
331
+
332
+ return {
333
+ Stream: Stream,
334
+ Object: _Object,
335
+ on: (event, func) => ee.on(event, func),
336
+ once: (event, func) => ee.once(event, func),
337
+ off: (event, funcName) => ee.off(event, funcName),
338
+ } as ASManager;
339
+ }
340
+
341
+ // biome-ignore lint/suspicious/noExplicitAny: <explanation>
342
+ ((global: any) => {
343
+ global.ASFactor = ASFactory;
344
+ })(typeof window === "object" ? window : {});