@mh-gg/host-runtime 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 (71) hide show
  1. package/package.json +23 -0
  2. package/src/constants.cjs +9 -0
  3. package/src/errors.cjs +39 -0
  4. package/src/host/installAppPack.cjs +35 -0
  5. package/src/host/migrations.cjs +21 -0
  6. package/src/host/runtimeFromPack.cjs +25 -0
  7. package/src/host/startRoomHost.cjs +95 -0
  8. package/src/index.cjs +23 -0
  9. package/src/memory.cjs +81 -0
  10. package/src/plugins/definition.cjs +19 -0
  11. package/src/plugins/install.cjs +90 -0
  12. package/src/plugins/migrations.cjs +27 -0
  13. package/src/plugins/operationDescriptors.cjs +138 -0
  14. package/src/runtime/HostPluginRuntime.cjs +85 -0
  15. package/src/runtime/authorityReplay/applyAuthority.cjs +146 -0
  16. package/src/runtime/authorityReplay/applyContent.cjs +49 -0
  17. package/src/runtime/authorityReplay/index.cjs +70 -0
  18. package/src/runtime/authorityReplay/state.cjs +56 -0
  19. package/src/runtime/context.cjs +65 -0
  20. package/src/runtime/coreOperations.cjs +169 -0
  21. package/src/runtime/corePayloads.cjs +66 -0
  22. package/src/runtime/directMessages/commit.cjs +50 -0
  23. package/src/runtime/directMessages/constants.cjs +20 -0
  24. package/src/runtime/directMessages/helpers.cjs +59 -0
  25. package/src/runtime/directMessages/payloads.cjs +74 -0
  26. package/src/runtime/directMessages/state.cjs +168 -0
  27. package/src/runtime/directMessages.cjs +6 -0
  28. package/src/runtime/lifecycle.cjs +166 -0
  29. package/src/runtime/memberProfiles.cjs +74 -0
  30. package/src/runtime/methods.cjs +10 -0
  31. package/src/runtime/operations.cjs +166 -0
  32. package/src/runtime/queries.cjs +146 -0
  33. package/src/runtime/readTags.cjs +171 -0
  34. package/src/runtime/scopedRoleOperations.cjs +97 -0
  35. package/src/runtime/snowflake.cjs +43 -0
  36. package/src/security/authority/constants.cjs +10 -0
  37. package/src/security/authority/resolve/operations.cjs +7 -0
  38. package/src/security/authority/resolve/policy.cjs +7 -0
  39. package/src/security/authority/resolve/voids.cjs +8 -0
  40. package/src/security/authorization/coreGate.cjs +63 -0
  41. package/src/security/authorization/schemaActions.cjs +75 -0
  42. package/src/security/roleKeys/authenticator.cjs +36 -0
  43. package/src/security/roleKeys/authorities/index.cjs +4 -0
  44. package/src/security/roleKeys/authorities/shapes.cjs +98 -0
  45. package/src/security/roleKeys/authorities/signing.cjs +121 -0
  46. package/src/security/roleKeys/constants.cjs +15 -0
  47. package/src/security/roleKeys/fingerprints.cjs +24 -0
  48. package/src/security/roleKeys/grants.cjs +93 -0
  49. package/src/security/roleKeys/index.cjs +10 -0
  50. package/src/security/roleKeys/roles.cjs +21 -0
  51. package/src/security/roleKeys/signatures.cjs +126 -0
  52. package/src/security/roles.cjs +10 -0
  53. package/src/security/roomDeviceKeys.cjs +41 -0
  54. package/src/security/scopedRoles/access.cjs +123 -0
  55. package/src/security/scopedRoles/constants.cjs +23 -0
  56. package/src/security/scopedRoles/metadata.cjs +39 -0
  57. package/src/security/scopedRoles/normalize.cjs +179 -0
  58. package/src/security/scopedRoles/publicView.cjs +31 -0
  59. package/src/security/scopedRoles/stateOps.cjs +167 -0
  60. package/src/security/scopedRoles.cjs +7 -0
  61. package/src/security/standingAuthority.cjs +76 -0
  62. package/src/shared.cjs +14 -0
  63. package/src/state.cjs +54 -0
  64. package/test/authority-ordering-hardening.test.cjs +101 -0
  65. package/test/authorization-gate.test.cjs +610 -0
  66. package/test/cascading-authority.test.cjs +390 -0
  67. package/test/grant-authority-security.test.cjs +305 -0
  68. package/test/matterhorn-host-runtime.test.cjs +1629 -0
  69. package/test/operation-descriptor-policy.test.cjs +140 -0
  70. package/test/role-key-auth.test.cjs +289 -0
  71. package/test/security-isolation.test.cjs +112 -0
@@ -0,0 +1,305 @@
1
+ const assert = require("node:assert/strict");
2
+ const test = require("node:test");
3
+
4
+ const { manifestHash } = require("@mh-gg/base");
5
+ const {
6
+ createOperationGrantAuthority,
7
+ createOperationRoleKeyGrant,
8
+ createRoleKeyAuthenticator,
9
+ createSignedOperationRoleKeyGrant,
10
+ generateOperationGrantAuthorityKeyPair,
11
+ generateOperationRoleKeyPair,
12
+ publicOperationGrantAuthority,
13
+ signOperationRoleKeyGrant,
14
+ signOperationWithRoleKey,
15
+ verifyOperationRoleKey,
16
+ verifyOperationRoleKeyGrant
17
+ } = require("../src/index.cjs");
18
+ const { kanbanAppPack, KANBAN_PLUGIN_ID } = require("../../../examples/kanban/src/sdk-app.mjs");
19
+
20
+ const ROOM_ID = "room_grant_authority";
21
+ const APP_HASH = manifestHash(kanbanAppPack);
22
+
23
+ function actor(role = "member", memberId = "bob") {
24
+ return { memberId, deviceId: `dev_${memberId}`, role };
25
+ }
26
+
27
+ function operation(overrides = {}) {
28
+ return {
29
+ id: overrides.id || "op_1",
30
+ roomId: ROOM_ID,
31
+ appPackId: kanbanAppPack.id,
32
+ appPackHash: APP_HASH,
33
+ pluginId: KANBAN_PLUGIN_ID,
34
+ type: "card.create",
35
+ actor: actor("member", "bob"),
36
+ seq: 1,
37
+ createdAt: 1000,
38
+ payload: { listId: "list_1", title: "Secure write" },
39
+ auth: { credentialId: "cred_user" },
40
+ ...overrides
41
+ };
42
+ }
43
+
44
+ test("role-key grants must chain to a trusted grant authority when required", () => {
45
+ const authorityKeys = generateOperationGrantAuthorityKeyPair({ issuer: "room-owner" });
46
+ const authority = createOperationGrantAuthority({
47
+ issuer: authorityKeys.issuer,
48
+ roomId: ROOM_ID,
49
+ appPackId: kanbanAppPack.id,
50
+ appPackHash: APP_HASH,
51
+ publicKeyPem: authorityKeys.publicKeyPem,
52
+ issuedAt: 10
53
+ });
54
+ const userKey = generateOperationRoleKeyPair({ role: "user" });
55
+ const unsignedGrant = createOperationRoleKeyGrant({
56
+ roomId: ROOM_ID,
57
+ appPackId: kanbanAppPack.id,
58
+ appPackHash: APP_HASH,
59
+ credentialId: "cred_user",
60
+ memberId: "bob",
61
+ deviceId: "dev_bob",
62
+ role: "user",
63
+ publicKeyPem: userKey.publicKeyPem,
64
+ issuedAt: 100
65
+ });
66
+
67
+ assert.equal(verifyOperationRoleKeyGrant(unsignedGrant, [authority], { requireSignedGrants: true }).ok, false);
68
+
69
+ const signedGrant = signOperationRoleKeyGrant(unsignedGrant, {
70
+ authority,
71
+ privateKeyPem: authorityKeys.privateKeyPem,
72
+ signedAt: 150
73
+ });
74
+ assert.equal(verifyOperationRoleKeyGrant(signedGrant, [authority], { roomId: ROOM_ID, appPackId: kanbanAppPack.id, appPackHash: APP_HASH, now: 5000, requireSignedGrants: true }).ok, true);
75
+
76
+ const signedOperation = signOperationWithRoleKey(operation(), { grant: signedGrant, privateKeyPem: userKey.privateKeyPem, issuedAt: 1000 });
77
+ assert.equal(verifyOperationRoleKey(signedOperation, [signedGrant], {
78
+ roomId: ROOM_ID,
79
+ appPackId: kanbanAppPack.id,
80
+ appPackHash: APP_HASH,
81
+ grantAuthorities: [authority],
82
+ requireSignedGrants: true,
83
+ now: 5000
84
+ }).ok, true);
85
+
86
+ const forgedAuthorityKeys = generateOperationGrantAuthorityKeyPair({ issuer: "evil" });
87
+ const forgedAuthority = createOperationGrantAuthority({
88
+ issuer: forgedAuthorityKeys.issuer,
89
+ roomId: ROOM_ID,
90
+ appPackId: kanbanAppPack.id,
91
+ appPackHash: APP_HASH,
92
+ publicKeyPem: forgedAuthorityKeys.publicKeyPem,
93
+ issuedAt: 10
94
+ });
95
+ const forgedGrant = signOperationRoleKeyGrant(unsignedGrant, { authority: forgedAuthority, privateKeyPem: forgedAuthorityKeys.privateKeyPem, signedAt: 150 });
96
+ const forgedOp = signOperationWithRoleKey(operation({ id: "op_forged" }), { grant: forgedGrant, privateKeyPem: userKey.privateKeyPem, issuedAt: 1000 });
97
+ assert.match(verifyOperationRoleKey(forgedOp, [forgedGrant], {
98
+ roomId: ROOM_ID,
99
+ appPackId: kanbanAppPack.id,
100
+ appPackHash: APP_HASH,
101
+ grantAuthorities: [authority],
102
+ requireSignedGrants: true,
103
+ now: 5000
104
+ }).error, /authority|signature|signed/i);
105
+
106
+ const publicAuthority = publicOperationGrantAuthority({ ...authority, privateKeyPem: authorityKeys.privateKeyPem });
107
+ assert.equal(publicAuthority.privateKeyPem, undefined);
108
+ assert.equal(publicAuthority.publicKeyPem.includes("PUBLIC KEY"), true);
109
+ });
110
+
111
+ test("role-key authenticator rejects unsigned relay grants by default when grant authorities are configured", async () => {
112
+ const authorityKeys = generateOperationGrantAuthorityKeyPair({ issuer: "room-owner" });
113
+ const authority = createOperationGrantAuthority({
114
+ issuer: authorityKeys.issuer,
115
+ roomId: ROOM_ID,
116
+ appPackId: kanbanAppPack.id,
117
+ appPackHash: APP_HASH,
118
+ publicKeyPem: authorityKeys.publicKeyPem
119
+ });
120
+ const userKey = generateOperationRoleKeyPair({ role: "user" });
121
+ const unsignedGrant = createOperationRoleKeyGrant({
122
+ roomId: ROOM_ID,
123
+ appPackId: kanbanAppPack.id,
124
+ appPackHash: APP_HASH,
125
+ credentialId: "cred_user",
126
+ memberId: "bob",
127
+ deviceId: "dev_bob",
128
+ role: "user",
129
+ publicKeyPem: userKey.publicKeyPem,
130
+ issuedAt: 100
131
+ });
132
+ const auth = createRoleKeyAuthenticator({
133
+ roomId: ROOM_ID,
134
+ appPackId: kanbanAppPack.id,
135
+ appPackHash: APP_HASH,
136
+ grants: [unsignedGrant],
137
+ grantAuthorities: [authority],
138
+ requireSignedGrants: true,
139
+ now: () => 5000
140
+ });
141
+ const signedOperation = signOperationWithRoleKey(operation(), { grant: unsignedGrant, privateKeyPem: userKey.privateKeyPem, issuedAt: 1000 });
142
+ await assert.rejects(() => auth(signedOperation.auth, signedOperation.actor, signedOperation), /grant/i);
143
+ });
144
+
145
+ test("role-key authenticator requires signed grants in production without authorities", async () => {
146
+ const userKey = generateOperationRoleKeyPair({ role: "user" });
147
+ const unsignedGrant = createOperationRoleKeyGrant({
148
+ roomId: ROOM_ID,
149
+ appPackId: kanbanAppPack.id,
150
+ appPackHash: APP_HASH,
151
+ credentialId: "cred_user_prod",
152
+ memberId: "bob",
153
+ deviceId: "dev_bob",
154
+ role: "user",
155
+ publicKeyPem: userKey.publicKeyPem,
156
+ issuedAt: 100
157
+ });
158
+ const signedOperation = signOperationWithRoleKey(operation({ id: "op_unsigned_prod" }), {
159
+ grant: unsignedGrant,
160
+ privateKeyPem: userKey.privateKeyPem,
161
+ issuedAt: 1000
162
+ });
163
+
164
+ const productionAuth = createRoleKeyAuthenticator({
165
+ roomId: ROOM_ID,
166
+ appPackId: kanbanAppPack.id,
167
+ appPackHash: APP_HASH,
168
+ grants: [unsignedGrant],
169
+ productionRuntime: true,
170
+ now: () => 5000
171
+ });
172
+ await assert.rejects(() => productionAuth(signedOperation.auth, signedOperation.actor, signedOperation), /signed|authority/i);
173
+
174
+ const devAuth = createRoleKeyAuthenticator({
175
+ roomId: ROOM_ID,
176
+ appPackId: kanbanAppPack.id,
177
+ appPackHash: APP_HASH,
178
+ grants: [unsignedGrant],
179
+ productionRuntime: true,
180
+ devUnsafeUnsignedOperationRoleKeyGrants: true,
181
+ now: () => 5000
182
+ });
183
+ assert.equal((await devAuth(signedOperation.auth, signedOperation.actor, signedOperation)).memberId, "bob");
184
+ });
185
+
186
+
187
+ test("createSignedOperationRoleKeyGrant creates a verifiable public grant in one step", () => {
188
+ const authorityKeys = generateOperationGrantAuthorityKeyPair({ issuer: "room-owner" });
189
+ const roleKey = generateOperationRoleKeyPair({ role: "admin" });
190
+ const authority = createOperationGrantAuthority({
191
+ issuer: authorityKeys.issuer,
192
+ roomId: ROOM_ID,
193
+ appPackId: kanbanAppPack.id,
194
+ appPackHash: APP_HASH,
195
+ publicKeyPem: authorityKeys.publicKeyPem,
196
+ issuedAt: 10
197
+ });
198
+ const grant = createSignedOperationRoleKeyGrant({
199
+ roomId: ROOM_ID,
200
+ appPackId: kanbanAppPack.id,
201
+ appPackHash: APP_HASH,
202
+ credentialId: "cred_admin",
203
+ memberId: "alice",
204
+ deviceId: "dev_alice",
205
+ role: "admin",
206
+ publicKeyPem: roleKey.publicKeyPem,
207
+ issuedAt: 100
208
+ }, {
209
+ authority,
210
+ privateKeyPem: authorityKeys.privateKeyPem,
211
+ signedAt: 150
212
+ });
213
+
214
+ assert.equal(grant.grantSignature.issuer, "room-owner");
215
+ assert.equal(verifyOperationRoleKeyGrant(grant, [authority], { requireSignedGrants: true, now: 5000 }).ok, true);
216
+ });
217
+
218
+ test("grant authority helpers validate scope, arrays, expiry, and public collection forms", () => {
219
+ const {
220
+ authorityArray,
221
+ authorityFingerprint,
222
+ isOperationGrantAuthority,
223
+ publicOperationGrantAuthorities
224
+ } = require("../src/index.cjs");
225
+ assert.throws(() => authorityFingerprint(""), /public key/);
226
+ const authorityKeys = generateOperationGrantAuthorityKeyPair();
227
+ assert.equal(authorityKeys.issuer, "matterhorn-grant-authority");
228
+ assert.throws(() => createOperationGrantAuthority({ issuer: "bad", publicKeyPem: authorityKeys.publicKeyPem, allowedGrantRoles: [""] }), /allowedGrantRoles/);
229
+ const authority = createOperationGrantAuthority({
230
+ issuer: "room-owner",
231
+ roomId: ROOM_ID,
232
+ appPackId: kanbanAppPack.id,
233
+ appPackHash: APP_HASH,
234
+ publicKeyPem: authorityKeys.publicKeyPem,
235
+ allowedGrantRoles: ["member", "admin", "member"],
236
+ issuedAt: 100,
237
+ expiresAt: 10000
238
+ });
239
+ assert.deepEqual(authority.allowedGrantRoles, ["user", "admin"]);
240
+ assert.equal(isOperationGrantAuthority(authority, { roomId: ROOM_ID, appPackId: kanbanAppPack.id, appPackHash: APP_HASH }), true);
241
+ assert.equal(isOperationGrantAuthority(authority, { roomId: "other" }), false);
242
+ assert.equal(isOperationGrantAuthority({ ...authority, publicKeyFingerprint: "sha256-nope" }), false);
243
+ assert.equal(isOperationGrantAuthority({ ...authority, expiresAt: "later" }), false);
244
+ assert.equal(isOperationGrantAuthority({ ...authority, roomId: "" }), false);
245
+ assert.equal(isOperationGrantAuthority({ ...authority, appPackId: "" }), false);
246
+ assert.equal(isOperationGrantAuthority({ ...authority, appPackHash: "" }), false);
247
+ assert.equal(isOperationGrantAuthority({ ...authority, allowedGrantRoles: ["guest"] }), false);
248
+ assert.deepEqual(authorityArray(authority), [authority]);
249
+ assert.deepEqual(authorityArray(new Map([["owner", authority]])), [authority]);
250
+ assert.deepEqual(authorityArray({ owner: authority }), [authority]);
251
+ assert.deepEqual(publicOperationGrantAuthorities({ owner: { ...authority, privateKeyPem: authorityKeys.privateKeyPem } })[0].privateKeyPem, undefined);
252
+ });
253
+
254
+ test("signed role-key grants reject untrusted, expired, mismatched, and tampered authorities", () => {
255
+ const authorityKeys = generateOperationGrantAuthorityKeyPair({ issuer: "room-owner" });
256
+ const authority = createOperationGrantAuthority({
257
+ issuer: authorityKeys.issuer,
258
+ roomId: ROOM_ID,
259
+ appPackId: kanbanAppPack.id,
260
+ appPackHash: APP_HASH,
261
+ publicKeyPem: authorityKeys.publicKeyPem,
262
+ allowedGrantRoles: ["admin"],
263
+ issuedAt: 100,
264
+ expiresAt: 10000
265
+ });
266
+ const key = generateOperationRoleKeyPair({ role: "admin" });
267
+ const grant = createOperationRoleKeyGrant({
268
+ roomId: ROOM_ID,
269
+ appPackId: kanbanAppPack.id,
270
+ appPackHash: APP_HASH,
271
+ credentialId: "cred_admin_scope",
272
+ memberId: "alice",
273
+ deviceId: "dev_alice",
274
+ role: "admin",
275
+ publicKeyPem: key.publicKeyPem,
276
+ issuedAt: 200
277
+ });
278
+
279
+ assert.throws(() => signOperationRoleKeyGrant(grant, { authority: { bad: true }, privateKeyPem: authorityKeys.privateKeyPem }), /grant authority/);
280
+ assert.throws(() => signOperationRoleKeyGrant(grant, { authority, privateKeyPem: "" }), /privateKeyPem/);
281
+ assert.throws(() => signOperationRoleKeyGrant({ bad: true }, { authority, privateKeyPem: authorityKeys.privateKeyPem }), /valid operation role-key grant/);
282
+ assert.throws(() => signOperationRoleKeyGrant({ ...grant, roomId: "other" }, { authority, privateKeyPem: authorityKeys.privateKeyPem }), /different room/);
283
+ assert.throws(() => signOperationRoleKeyGrant({ ...grant, appPackId: "other" }, { authority, privateKeyPem: authorityKeys.privateKeyPem }), /different app pack/);
284
+ assert.throws(() => signOperationRoleKeyGrant({ ...grant, appPackHash: "sha256-other" }, { authority, privateKeyPem: authorityKeys.privateKeyPem }), /different app pack hash/);
285
+ const userGrant = createOperationRoleKeyGrant({ ...grant, credentialId: "cred_user_scope", role: "user" });
286
+ assert.throws(() => signOperationRoleKeyGrant(userGrant, { authority, privateKeyPem: authorityKeys.privateKeyPem }), /not allowed/);
287
+
288
+ const signed = signOperationRoleKeyGrant(grant, { authority, privateKeyPem: authorityKeys.privateKeyPem, signedAt: 250, expiresAt: 8000 });
289
+ assert.equal(verifyOperationRoleKeyGrant(signed, [authority], { requireSignedGrants: true, now: 5000 }).ok, true);
290
+ assert.match(verifyOperationRoleKeyGrant(signed, [authority], { requireSignedGrants: true, now: 11000 }).error, /authority has expired/);
291
+ assert.match(verifyOperationRoleKeyGrant(signed, [{ ...authority, expiresAt: 20000 }], { requireSignedGrants: true, now: 9000 }).error, /signature has expired/);
292
+ assert.match(verifyOperationRoleKeyGrant(signOperationRoleKeyGrant(grant, { authority, privateKeyPem: authorityKeys.privateKeyPem, signedAt: 50 }), [authority], { requireSignedGrants: true, now: 5000 }).error, /predates authority/);
293
+ const laterGrant = { ...grant, issuedAt: 400 };
294
+ assert.match(verifyOperationRoleKeyGrant(signOperationRoleKeyGrant(laterGrant, { authority, privateKeyPem: authorityKeys.privateKeyPem, signedAt: 300 }), [authority], { requireSignedGrants: true, now: 5000 }).error, /predates grant/);
295
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, kind: "bad" } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /kind/);
296
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, version: 2 } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /version/);
297
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, alg: "rsa" } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /algorithm/);
298
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, issuer: "" } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /issuer/);
299
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, authorityFingerprint: "" } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /fingerprint/);
300
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, signedAt: "later" } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /time/);
301
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, expiresAt: "later" } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /expiry/);
302
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, signature: "" } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /signature is invalid/);
303
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, publicKeyPem: generateOperationRoleKeyPair({ role: "admin" }).publicKeyPem }, [authority], { requireSignedGrants: true, now: 5000 }).error, /invalid|fingerprint/);
304
+ assert.match(verifyOperationRoleKeyGrant({ ...signed, grantSignature: { ...signed.grantSignature, signature: "not-base64" } }, [authority], { requireSignedGrants: true, now: 5000 }).error, /invalid|failed|DECODER/);
305
+ });