@mh-gg/base-plugins 0.1.1-alpha.20260613T085325975Z

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 (61) hide show
  1. package/README.md +247 -0
  2. package/package.json +48 -0
  3. package/src/approvals/index.cjs +65 -0
  4. package/src/attachments/index.cjs +75 -0
  5. package/src/calendar/index.cjs +48 -0
  6. package/src/checklists/index.cjs +58 -0
  7. package/src/comments/index.cjs +15 -0
  8. package/src/comments/plugin.cjs +66 -0
  9. package/src/comments/reducer.cjs +81 -0
  10. package/src/comments/schemas.cjs +60 -0
  11. package/src/comments/state.cjs +17 -0
  12. package/src/comments/threads.cjs +44 -0
  13. package/src/comments/views.cjs +27 -0
  14. package/src/composer/capabilities.cjs +19 -0
  15. package/src/composer/compose.cjs +37 -0
  16. package/src/composer/index.cjs +15 -0
  17. package/src/composer/operations.cjs +42 -0
  18. package/src/composer/registry.cjs +155 -0
  19. package/src/composer/selection.cjs +39 -0
  20. package/src/composer/suite.cjs +32 -0
  21. package/src/crdt/client.mjs +207 -0
  22. package/src/crdt/index.cjs +258 -0
  23. package/src/embeds/index.cjs +90 -0
  24. package/src/files/index.cjs +133 -0
  25. package/src/index.cjs +19 -0
  26. package/src/labels/index.cjs +46 -0
  27. package/src/location-pins/index.cjs +142 -0
  28. package/src/markdown/documents/index.cjs +128 -0
  29. package/src/markdown/index.cjs +8 -0
  30. package/src/markdown/parser/index.cjs +127 -0
  31. package/src/markdown/providers/audio.cjs +77 -0
  32. package/src/markdown/providers/cloud.cjs +72 -0
  33. package/src/markdown/providers/developer.cjs +45 -0
  34. package/src/markdown/providers/direct.cjs +49 -0
  35. package/src/markdown/providers/games.cjs +26 -0
  36. package/src/markdown/providers/images.cjs +88 -0
  37. package/src/markdown/providers/index.cjs +97 -0
  38. package/src/markdown/providers/maps.cjs +24 -0
  39. package/src/markdown/providers/productivity.cjs +30 -0
  40. package/src/markdown/providers/res-inspired.cjs +11 -0
  41. package/src/markdown/providers/social.cjs +33 -0
  42. package/src/markdown/providers/video.cjs +139 -0
  43. package/src/markdown/resolve.cjs +87 -0
  44. package/src/media-rooms/index.cjs +244 -0
  45. package/src/presence/index.cjs +193 -0
  46. package/src/reactions/index.cjs +47 -0
  47. package/src/screen-share/index.cjs +84 -0
  48. package/src/shared/constants.cjs +87 -0
  49. package/src/shared/embed.cjs +82 -0
  50. package/src/shared/index.cjs +20 -0
  51. package/src/shared/roles.cjs +5 -0
  52. package/src/shared/scopes.cjs +15 -0
  53. package/src/shared/url.cjs +32 -0
  54. package/src/shared/validation.cjs +31 -0
  55. package/test/composable-plugins.test.cjs +170 -0
  56. package/test/crdt-plugin.test.cjs +168 -0
  57. package/test/embed-autodetect-providers.test.cjs +138 -0
  58. package/test/markdown-media-workflow-plugins.test.cjs +201 -0
  59. package/test/markdown-parser-edge-cases.test.cjs +86 -0
  60. package/test/plugin-structure.test.cjs +69 -0
  61. package/test/shared-plugin-edges.test.cjs +207 -0
@@ -0,0 +1,207 @@
1
+ const assert = require("node:assert/strict");
2
+ const test = require("node:test");
3
+ const { manifestHash } = require("@mh-gg/base");
4
+ const { HostPluginRuntime, createMemoryOperationLog, createMemoryRoomStore } = require("@mh-gg/host-runtime");
5
+ const { ensureOperationIdentity } = require("@mh-gg/protocol");
6
+ const { buildExampleMatterhornApp, createExampleActor, createExampleOperationFactory } = require("../../../examples/_shared/matterhornExample/index.cjs");
7
+ const {
8
+ COMMENTS_PLUGIN_ID,
9
+ MEDIA_ROOMS_PLUGIN_ID,
10
+ PRESENCE_PLUGIN_ID,
11
+ SCREEN_SHARE_PLUGIN_ID,
12
+ commentsHostPlugin,
13
+ composeHostPlugins,
14
+ createCollaborationPluginSuite,
15
+ mediaRoomsHostPlugin,
16
+ presenceHostPlugin,
17
+ reusablePluginOperationFactory,
18
+ screenShareHostPlugin
19
+ } = require("../src/index.cjs");
20
+
21
+ const suite = createCollaborationPluginSuite("all");
22
+ const built = buildExampleMatterhornApp({
23
+ slug: "edges",
24
+ packageName: "@mh-gg/base-plugins",
25
+ appId: "com.matterhorn.examples.plugin-edges",
26
+ name: "Plugin Edge Tests",
27
+ hostPlugins: [suite]
28
+ });
29
+
30
+ async function runtime() {
31
+ const rt = new HostPluginRuntime({
32
+ room: { id: "room_edges", appPack: { id: built.appPack.id, version: built.appPack.version, hash: manifestHash(built.appPack), protocolHash: built.appPack.compatibility.appProtocolHash } },
33
+ plugins: suite.plugins,
34
+ store: createMemoryRoomStore(),
35
+ operationLog: createMemoryOperationLog(),
36
+ authenticateActor: async (auth, actor) => {
37
+ assert.equal(auth.signature, "sig");
38
+ return actor;
39
+ }
40
+ });
41
+ await rt.start();
42
+ return rt;
43
+ }
44
+
45
+ function makeOps() {
46
+ return createExampleOperationFactory({ appPack: built.appPack, hostPlugin: commentsHostPlugin, roomId: "room_edges", actor: createExampleActor({ memberId: "admin", role: "admin" }), startTime: 2000 });
47
+ }
48
+
49
+ async function rejected(promise, pattern) {
50
+ const result = await promise;
51
+ assert.equal(result.ok, false);
52
+ assert.match(result.reason, pattern);
53
+ }
54
+
55
+ test("example app builder accepts suite objects and emits deterministic plugin graph metadata", () => {
56
+ assert.deepEqual(built.hostPlugins.map((plugin) => plugin.id), suite.pluginIds);
57
+ assert.deepEqual(built.hostPack.plugins.map((plugin) => plugin.id), suite.pluginIds);
58
+ assert.equal(built.appPack.compatibility.pluginGraphHash, built.hostPack.compatibility.pluginGraphHash);
59
+ assert.deepEqual([...built.playerPack.supports[0].pluginIds].sort(), [...suite.pluginIds].sort());
60
+
61
+ assert.throws(() => buildExampleMatterhornApp({
62
+ slug: "bad",
63
+ packageName: "@mh-gg/example-bad",
64
+ appId: "com.matterhorn.bad",
65
+ name: "Bad",
66
+ hostPlugins: [suite, commentsHostPlugin]
67
+ }), /Duplicate host plugin/);
68
+ });
69
+
70
+ test("reusable operation factory targets selected plugins while preserving sequence and auth defaults", () => {
71
+ const make = reusablePluginOperationFactory({ appPack: built.appPack, roomId: "room_edges", actor: createExampleActor({ memberId: "mina", role: "member" }), startSeq: 10, startTime: 5000 });
72
+ const comment = make("comments", "comments.add", { scopeType: "card", scopeId: "card_1", body: "Hi" });
73
+ const presence = make(presenceHostPlugin, "presence.update", { status: "online" });
74
+
75
+ assert.equal(comment.pluginId, COMMENTS_PLUGIN_ID);
76
+ assert.equal(comment.seq, 10);
77
+ assert.equal(comment.createdAt, 5010);
78
+ assert.equal(comment.auth.credentialId, "cred_mina");
79
+ assert.equal(presence.pluginId, PRESENCE_PLUGIN_ID);
80
+ assert.equal(presence.seq, 11);
81
+ });
82
+
83
+ test("comments public views hide deleted comments but preserve moderator thread queries", async () => {
84
+ const rt = await runtime();
85
+ const op = makeOps();
86
+ await rt.handleOperation(op("comments.add", { scopeType: "deal", scopeId: "deal_1", body: "Needs sponsor review" }, { id: "comment_1", pluginId: COMMENTS_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member", displayName: "Mina" }) }));
87
+ const commentId = Object.keys((await rt.getState()).plugins[COMMENTS_PLUGIN_ID].comments)[0];
88
+ await rt.handleOperation(op("comments.delete", { commentId, reason: "Resolved elsewhere" }, { id: "comment_2", pluginId: COMMENTS_PLUGIN_ID, actor: createExampleActor({ memberId: "admin", role: "admin" }) }));
89
+
90
+ const publicView = await rt.publicView(createExampleActor({ memberId: "guest", role: "guest" }));
91
+ assert.deepEqual(Object.keys(publicView.plugins[COMMENTS_PLUGIN_ID].comments), []);
92
+
93
+ const unresolved = await rt.query(COMMENTS_PLUGIN_ID, "unresolvedThreads", {}, createExampleActor({ memberId: "admin", role: "admin" }));
94
+ assert.equal(unresolved.length, 1);
95
+ assert.equal(unresolved[0].scopeType, "deal");
96
+ });
97
+
98
+ test("presence clear only allows self or moderators", async () => {
99
+ const rt = await runtime();
100
+ const op = makeOps();
101
+ await rt.handleOperation(op("presence.update", { status: "online", activity: "writing tests" }, { id: "presence_1", pluginId: PRESENCE_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member", displayName: "Mina" }) }));
102
+ await rejected(rt.handleOperation(op("presence.clear", { memberId: "mina" }, { id: "presence_2", pluginId: PRESENCE_PLUGIN_ID, actor: createExampleActor({ memberId: "lee", role: "member" }) })), /Can only clear your own presence/);
103
+ await rt.handleOperation(op("presence.clear", { memberId: "mina" }, { id: "presence_3", pluginId: PRESENCE_PLUGIN_ID, actor: createExampleActor({ memberId: "mod", role: "moderator" }) }));
104
+ assert.deepEqual(await rt.query(PRESENCE_PLUGIN_ID, "onlineMembers", {}, createExampleActor({ role: "admin" })), []);
105
+ });
106
+
107
+ test("presence announcements normalize user text and reject malformed payloads", async () => {
108
+ const rt = await runtime();
109
+ const op = makeOps();
110
+ await rt.handleOperation(op("presence.update", { status: "online", activity: " writing \n tests ", location: " Stage\tleft " }, { id: "presence_harden_1", pluginId: PRESENCE_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member", displayName: " Mina\nAdmin " }) }));
111
+ const pluginState = (await rt.getState()).plugins[PRESENCE_PLUGIN_ID];
112
+
113
+ assert.equal(pluginState.members.mina.name, "Mina Admin");
114
+ assert.equal(pluginState.members.mina.activity, "writing tests");
115
+ assert.equal(pluginState.members.mina.location, "Stage left");
116
+ assert.equal(pluginState.activity[pluginState.activity.length - 1].message, "Mina Admin is online");
117
+
118
+ await rejected(rt.handleOperation(op("presence.update", { status: "online", activity: { text: "typing" } }, { id: "presence_harden_2", pluginId: PRESENCE_PLUGIN_ID, actor: createExampleActor({ memberId: "lee", role: "member" }) })), /activity must be a string/);
119
+ await rejected(rt.handleOperation(op("presence.update", { status: "online" }, { id: "presence_harden_3", pluginId: PRESENCE_PLUGIN_ID, actor: createExampleActor({ memberId: "__proto__", role: "member" }) })), /actor\.memberId is reserved/);
120
+ });
121
+
122
+ test("presence pings preserve busy state and invisible members are hidden from peers", async () => {
123
+ const rt = await runtime();
124
+ const op = makeOps();
125
+ const now = Date.now();
126
+ await rt.handleOperation(op("presence.update", { status: "busy", activity: "screening vendors" }, { id: "presence_busy", pluginId: PRESENCE_PLUGIN_ID, createdAt: now, actor: createExampleActor({ memberId: "mina", role: "member", displayName: "Mina" }) }));
127
+ await rt.handleOperation(op("presence.ping", {}, { id: "presence_ping", pluginId: PRESENCE_PLUGIN_ID, createdAt: now + 1, actor: createExampleActor({ memberId: "mina", role: "member", displayName: "Mina" }) }));
128
+
129
+ const pluginState = (await rt.getState()).plugins[PRESENCE_PLUGIN_ID];
130
+ assert.equal(pluginState.members.mina.status, "busy");
131
+ assert.equal(typeof pluginState.members.mina.lastPingAt, "number");
132
+ assert.equal((await rt.query(PRESENCE_PLUGIN_ID, "onlineMembers", {}, createExampleActor({ memberId: "admin", role: "admin" })))[0].status, "busy");
133
+
134
+ await rt.handleOperation(op("presence.update", { status: "invisible" }, { id: "presence_invisible", pluginId: PRESENCE_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member", displayName: "Mina" }) }));
135
+ const peerView = await rt.publicView(createExampleActor({ memberId: "lee", role: "member" }));
136
+ assert.equal(peerView.plugins[PRESENCE_PLUGIN_ID].members.mina, undefined);
137
+ const selfView = await rt.publicView(createExampleActor({ memberId: "mina", role: "member" }));
138
+ assert.equal(selfView.plugins[PRESENCE_PLUGIN_ID].members.mina.status, "invisible");
139
+ });
140
+
141
+ test("presence ping freshness marks stale visible members offline", () => {
142
+ const view = presenceHostPlugin.getPublicView({ actor: createExampleActor({ memberId: "admin", role: "admin" }), now: 100_000 }, {
143
+ members: { mina: { memberId: "mina", name: "Mina", status: "online", updatedAt: 1, lastPingAt: 1 } },
144
+ activity: []
145
+ });
146
+ assert.equal(view.members.mina.status, "offline");
147
+ assert.equal(view.members.mina.declaredStatus, "online");
148
+ assert.equal(view.members.mina.visible, false);
149
+ assert.equal(view.onlineCount, 0);
150
+ });
151
+
152
+ test("presence state parser rejects spoofed or reserved member records", () => {
153
+ assert.throws(() => presenceHostPlugin.schemas.state.parse({
154
+ members: { mina: { memberId: "lee", name: "Mina", status: "online", updatedAt: 1 } },
155
+ activity: []
156
+ }), /must match its key/);
157
+ assert.throws(() => presenceHostPlugin.schemas.state.parse(JSON.parse('{"members":{"__proto__":{"memberId":"__proto__","name":"Bad","status":"online","updatedAt":1}},"activity":[]}')), /reserved/);
158
+ });
159
+
160
+ test("media rooms support updates, leave, archive, and guest-safe directory views", async () => {
161
+ const rt = await runtime();
162
+ const op = makeOps();
163
+ await rejected(rt.handleOperation(op("media.room.create", { name: "Member-created", allowsVideo: true }, { id: "media_member_create", pluginId: MEDIA_ROOMS_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member" }) })), /Moderators only|requires moderator/);
164
+ await rt.handleOperation(op("media.room.create", { name: "Standup", allowsVideo: true, scopeType: "kanban-card", scopeId: "card_7" }, { id: "media_1", pluginId: MEDIA_ROOMS_PLUGIN_ID }));
165
+ const roomId = Object.keys((await rt.getState()).plugins[MEDIA_ROOMS_PLUGIN_ID].rooms)[0];
166
+ await rt.handleOperation(op("media.room.join", { roomId, media: { audio: true, video: false } }, { id: "media_2", pluginId: MEDIA_ROOMS_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member", displayName: "Mina" }) }));
167
+ await rt.handleOperation(op("media.room.update", { roomId, locked: true, spotlightMemberId: "mina", name: "Daily Standup" }, { id: "media_3", pluginId: MEDIA_ROOMS_PLUGIN_ID }));
168
+ let rooms = await rt.query(MEDIA_ROOMS_PLUGIN_ID, "roomDirectory", {}, createExampleActor({ role: "guest" }));
169
+ assert.equal(rooms[0].name, "Daily Standup");
170
+ assert.equal(rooms[0].participantCount, 1);
171
+ assert.equal(rooms[0].locked, true);
172
+
173
+ await rt.handleOperation(op("media.room.leave", { roomId }, { id: "media_4", pluginId: MEDIA_ROOMS_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member" }) }));
174
+ await rt.handleOperation(op("media.room.archive", { roomId }, { id: "media_5", pluginId: MEDIA_ROOMS_PLUGIN_ID }));
175
+ rooms = await rt.query(MEDIA_ROOMS_PLUGIN_ID, "roomDirectory", {}, createExampleActor({ role: "guest" }));
176
+ assert.equal(rooms.length, 0);
177
+ });
178
+
179
+ test("screen-share plugin scopes active shares and enforces presenter stop rules", async () => {
180
+ const rt = await runtime();
181
+ const op = makeOps();
182
+ await rt.handleOperation(op("media.room.create", { name: "Review", allowsVideo: true }, { id: "media_room", pluginId: MEDIA_ROOMS_PLUGIN_ID }));
183
+ const roomId = Object.keys((await rt.getState()).plugins[MEDIA_ROOMS_PLUGIN_ID].rooms)[0];
184
+ await rt.handleOperation(op("screenshare.start", { roomId, scopeType: "wiki-page", scopeId: "page_1", title: "Architecture" }, { id: "share_1", pluginId: SCREEN_SHARE_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member", displayName: "Mina" }) }));
185
+ const shares = await rt.query(SCREEN_SHARE_PLUGIN_ID, "activeShares", { scopeType: "wiki-page", scopeId: "page_1" }, createExampleActor({ role: "member" }));
186
+ assert.equal(shares.length, 1);
187
+ assert.equal(shares[0].presenterName, "Mina");
188
+
189
+ await rejected(rt.handleOperation(op("screenshare.stop", { shareId: shares[0].id }, { id: "share_2", pluginId: SCREEN_SHARE_PLUGIN_ID, actor: createExampleActor({ memberId: "lee", role: "member" }) })), /Only presenter or moderators/);
190
+ await rt.handleOperation(op("screenshare.stop", { shareId: shares[0].id }, { id: "share_3", pluginId: SCREEN_SHARE_PLUGIN_ID, actor: createExampleActor({ memberId: "mina", role: "member" }) }));
191
+ assert.deepEqual(await rt.query(SCREEN_SHARE_PLUGIN_ID, "activeShares", { scopeType: "wiki-page", scopeId: "page_1" }, createExampleActor({ role: "member" })), []);
192
+ });
193
+
194
+ test("composition helper dedupes primitive plugins with reusable suite entries", () => {
195
+ const plugins = composeHostPlugins(commentsHostPlugin, createCollaborationPluginSuite(["comments", "presence"]), presenceHostPlugin);
196
+ assert.deepEqual(plugins.map((plugin) => plugin.id), [COMMENTS_PLUGIN_ID, PRESENCE_PLUGIN_ID]);
197
+ assert.equal(plugins[0].version, commentsHostPlugin.version);
198
+ assert.equal(plugins[1].version, presenceHostPlugin.version);
199
+ assert.equal(mediaRoomsHostPlugin.id, MEDIA_ROOMS_PLUGIN_ID);
200
+ assert.equal(screenShareHostPlugin.id, SCREEN_SHARE_PLUGIN_ID);
201
+ });
202
+
203
+ test("scope keys are collision-resistant for delimiter-bearing scope values", () => {
204
+ const { scopeKey } = require("../src/shared/index.cjs");
205
+ assert.notEqual(scopeKey("deal:abc", "thread"), scopeKey("deal", "abc:thread"));
206
+ assert.equal(scopeKey("deal", "abc"), "deal:abc");
207
+ });