@mh-gg/schema 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 (68) hide show
  1. package/package.json +39 -0
  2. package/schemas/app-composition.schema.json +684 -0
  3. package/schemas/example-app.schema.json +64 -0
  4. package/schemas/example-demo.schema.json +45 -0
  5. package/schemas/micro-plugin.schema.json +20 -0
  6. package/src/actions.cjs +31 -0
  7. package/src/app.d.ts +119 -0
  8. package/src/builder/app.cjs +157 -0
  9. package/src/builder/effects.cjs +116 -0
  10. package/src/builder/guards.cjs +35 -0
  11. package/src/builder/index.cjs +11 -0
  12. package/src/builder/matterhornApp/build.cjs +137 -0
  13. package/src/builder/matterhornApp/bundle.cjs +89 -0
  14. package/src/builder/matterhornApp/demo.cjs +161 -0
  15. package/src/builder/matterhornApp/demoAliases.cjs +50 -0
  16. package/src/builder/matterhornApp/descriptors.cjs +59 -0
  17. package/src/builder/matterhornApp/exports.cjs +95 -0
  18. package/src/builder/matterhornApp/frontend.cjs +80 -0
  19. package/src/builder/matterhornApp/plugins.cjs +95 -0
  20. package/src/builder/matterhornApp/shared.cjs +105 -0
  21. package/src/builder/matterhornApp.cjs +7 -0
  22. package/src/builder/model.cjs +172 -0
  23. package/src/builder/notifications.cjs +51 -0
  24. package/src/builder/refs.cjs +33 -0
  25. package/src/builder/schema.cjs +101 -0
  26. package/src/builder/streamKey.cjs +27 -0
  27. package/src/composition.cjs +157 -0
  28. package/src/configured.cjs +41 -0
  29. package/src/configured.d.ts +1 -0
  30. package/src/imports/loader.cjs +86 -0
  31. package/src/index.cjs +12 -0
  32. package/src/index.d.ts +168 -0
  33. package/src/json.cjs +70 -0
  34. package/src/jsonSchema/validator.cjs +131 -0
  35. package/src/microPlugin.cjs +112 -0
  36. package/src/model/collections.cjs +57 -0
  37. package/src/model/effects.cjs +149 -0
  38. package/src/model/expressions.cjs +85 -0
  39. package/src/model/guards.cjs +139 -0
  40. package/src/model/index.cjs +9 -0
  41. package/src/model/partitionOperands.cjs +89 -0
  42. package/src/model/partitionValidator.cjs +95 -0
  43. package/src/model/payload.cjs +93 -0
  44. package/src/model/plugin.cjs +146 -0
  45. package/src/notifications.cjs +41 -0
  46. package/src/notifications.d.ts +63 -0
  47. package/src/registry.cjs +63 -0
  48. package/src/runtime-exports.d.ts +65 -0
  49. package/src/streamKey.d.ts +15 -0
  50. package/src/types/actionTypes.cjs +164 -0
  51. package/src/types/coreActionPayloadSchemas.cjs +35 -0
  52. package/src/types/coreFeatures.cjs +29 -0
  53. package/src/types/entities.cjs +131 -0
  54. package/src/types/entityAliases.cjs +84 -0
  55. package/src/types/generator.cjs +145 -0
  56. package/src/types/index.cjs +5 -0
  57. package/src/types/interfaceBlock.cjs +41 -0
  58. package/src/types/pluginEntities.cjs +83 -0
  59. package/src/types/schema.cjs +148 -0
  60. package/src/types/standardPluginEntityTypes.cjs +135 -0
  61. package/test/content-lww-contract.test.cjs +104 -0
  62. package/test/partitionValidator.test.cjs +92 -0
  63. package/test/schema-array-effects.test.cjs +116 -0
  64. package/test/schema-builder-types.test.cjs +383 -0
  65. package/test/schema-composition.test.cjs +144 -0
  66. package/test/schema-configured-builders.test.cjs +286 -0
  67. package/test/schema-imports.test.cjs +66 -0
  68. package/test/schema-model-interpreter.test.cjs +273 -0
@@ -0,0 +1,137 @@
1
+ const {
2
+ computeAppProtocolHash,
3
+ definePlayerPlugin,
4
+ hashCanonical,
5
+ manifestHash,
6
+ resolvePluginGraph
7
+ } = require('@mh-gg/base');
8
+ const { createActionDispatchersFromComposition } = require('../../actions.cjs');
9
+ const {
10
+ appCompositionSchemaHash,
11
+ resolveCompositionHostPluginEntries,
12
+ validateAppCompositionSchema
13
+ } = require('../../composition.cjs');
14
+ const { schemaDefinedHostPluginEntry } = require('../../model/plugin.cjs');
15
+ const { matterhornAppDescriptorFromBuilt } = require('./descriptors.cjs');
16
+ const { normalizeHostPluginEntries, primaryHostPluginDescriptorForBuilt } = require('./plugins.cjs');
17
+ const { DEFAULT_PUBLISHER, DEFAULT_TRUST, mergedOptions, slugFromAppId } = require('./shared.cjs');
18
+
19
+ function buildMatterhornApp(app, options = {}) {
20
+ const merged = mergedOptions(app, options);
21
+ const composition = validateAppCompositionSchema(merged.compositionSchema || app.toJSON(), { registry: merged.registry });
22
+ const appId = merged.appId || composition.app.id;
23
+ const version = merged.version || composition.app.version || '1.0.0';
24
+ const name = merged.name || composition.app.name || appId;
25
+ const slug = merged.slug || slugFromAppId(appId);
26
+ const packageName = merged.packageName || slug;
27
+ const compositionHostPlugins = resolveCompositionHostPluginEntries(composition, merged.registry);
28
+ const primarySchemaPlugin = !merged.hostPlugin ? schemaDefinedHostPluginEntry(composition, { packageName }) : undefined;
29
+ const hostPluginEntries = normalizeHostPluginEntries({
30
+ ...merged,
31
+ packageName,
32
+ hostPlugins: [primarySchemaPlugin, ...(merged.hostPlugins || []), ...compositionHostPlugins].filter(Boolean)
33
+ }, version);
34
+ const graph = resolvePluginGraph(hostPluginEntries.map((entry) => entry.plugin));
35
+ const hostPluginDescriptors = graph.descriptors;
36
+ const appProtocolHash = computeAppProtocolHash({ appPackId: appId, appVersion: version, hostPlugins: hostPluginDescriptors });
37
+ const pluginGraphHash = graph.pluginGraphHash;
38
+ const publisher = merged.publisher || DEFAULT_PUBLISHER;
39
+ const trust = merged.trust || DEFAULT_TRUST;
40
+ const compositionHash = appCompositionSchemaHash(composition);
41
+
42
+ const hostPack = {
43
+ kind: 'matterhorn.host-pack',
44
+ id: `${appId}.host`,
45
+ appPackId: appId,
46
+ version,
47
+ plugins: hostPluginEntries.map((entry) => ({
48
+ id: entry.plugin.id,
49
+ version: entry.plugin.version,
50
+ source: entry.source || `workspace:${entry.packageName}#${entry.exportName}`,
51
+ integrity: hashCanonical({ package: entry.packageName, export: entry.exportName, version: entry.plugin.version || version }),
52
+ ...(entry.dependsOn && entry.dependsOn.length ? { dependsOn: entry.dependsOn } : {}),
53
+ ...(entry.conflictsWith && entry.conflictsWith.length ? { conflictsWith: entry.conflictsWith } : {})
54
+ })),
55
+ compatibility: { appProtocolHash, pluginGraphHash },
56
+ runtime: { minMatterhornVersion: '0.1.0', sandbox: 'process' },
57
+ capabilities: merged.hostCapabilities || { required: ['room.state', 'room.roles'], optional: ['relay.event-cache'] },
58
+ composition: {
59
+ schemaHash: compositionHash,
60
+ primaryPlugin: composition.primaryPlugin,
61
+ plugins: composition.plugins,
62
+ sharedScopes: composition.sharedScopes || {},
63
+ views: composition.views,
64
+ actions: composition.actions,
65
+ ...(composition.notifications === undefined ? {} : { notifications: composition.notifications })
66
+ },
67
+ trust
68
+ };
69
+
70
+ const pluginIds = hostPluginDescriptors.map((descriptor) => descriptor.id);
71
+ const support = { appPackId: appId, appPackRange: `^${version}`, appProtocolHash, pluginIds, compositionSchemaHash: compositionHash };
72
+ const playerPack = {
73
+ kind: 'matterhorn.player-pack',
74
+ id: merged.playerPackId || `${appId}.player`,
75
+ name: `${name} Player`,
76
+ version,
77
+ publisher,
78
+ entrypoints: merged.entrypoints || {
79
+ default: `https://apps.matterhorn.gg/${slug}/player/${version}/`,
80
+ admin: `https://apps.matterhorn.gg/${slug}/admin/${version}/`
81
+ },
82
+ supports: [support],
83
+ recommendedFor: merged.recommendedFor || { devices: ['desktop', 'tablet'], roles: ['admin', 'member'] },
84
+ mode: 'external',
85
+ trust: {
86
+ integrity: hashCanonical({ package: packageName, export: merged.playerPluginExport || 'playerPlugin', version }),
87
+ signatures: trust.signatures
88
+ }
89
+ };
90
+
91
+ const builtForPrimary = { config: { ...merged, hostPlugin: merged.hostPlugin }, compositionSchema: composition, hostPluginDescriptors, hostPlugins: hostPluginEntries.map((entry) => entry.plugin) };
92
+ const primaryHostPluginDescriptor = primaryHostPluginDescriptorForBuilt(builtForPrimary);
93
+ const appPack = {
94
+ kind: 'matterhorn.app-pack',
95
+ id: appId,
96
+ name,
97
+ version,
98
+ publisher,
99
+ matterhornVersion: '>=0.1 <0.2',
100
+ hostPack: { url: `workspace:${packageName}#${merged.hostPackExport || 'hostPack'}`, integrity: manifestHash(hostPack) },
101
+ playerPacks: [{ id: playerPack.id, name: playerPack.name, url: `workspace:${packageName}#${merged.playerPackExport || 'playerPack'}`, integrity: manifestHash(playerPack), recommendedFor: playerPack.recommendedFor }],
102
+ compatibility: {
103
+ appProtocolHash,
104
+ operationSchemaHash: primaryHostPluginDescriptor.operationSchemaHash,
105
+ stateSchemaHash: primaryHostPluginDescriptor.stateSchemaHash,
106
+ pluginGraphHash,
107
+ compositionSchemaHash: compositionHash
108
+ },
109
+ composition,
110
+ capabilities: merged.appCapabilities || { required: ['room.state', 'room.roles', 'relay.route'], optional: ['relay.event-cache'] },
111
+ trust: { createdAt: merged.createdAt || '2026-05-27T00:00:00Z', signatures: trust.signatures }
112
+ };
113
+
114
+ const generatedActions = createActionDispatchersFromComposition(composition, { registry: merged.registry });
115
+ const playerPlugin = definePlayerPlugin({
116
+ id: merged.playerPluginId || `${appId}.player-plugin`,
117
+ version,
118
+ meta: { name: `${name} Player Plugin`, publisher },
119
+ supports: [support],
120
+ routes: merged.routes || composition.routes || [{ path: '/', component: `${name.replace(/[^A-Za-z0-9]/g, '')}Page`, requiredPlugins: pluginIds }],
121
+ actions: { ...generatedActions, ...(merged.actions || {}) },
122
+ optimisticReducers: merged.optimisticReducers || {},
123
+ navigation: { defaultRoute: '/', adminRoute: '/admin', ...(merged.navigation || {}) },
124
+ deviceHints: { desktop: true, tablet: true, ...(merged.deviceHints || {}) }
125
+ });
126
+
127
+ return { appPack, appProtocolHash, hostPack, hostPluginDescriptors, hostPlugins: hostPluginEntries.map((entry) => entry.plugin), playerPack, playerPlugin, pluginGraphHash, compositionSchema: composition, publisher, trust, config: { ...merged, appId, version, name, slug, packageName } };
128
+ }
129
+
130
+ function createMatterhornAppDescriptor(app, options = {}) {
131
+ return matterhornAppDescriptorFromBuilt(buildMatterhornApp(app, options));
132
+ }
133
+
134
+ module.exports = {
135
+ buildMatterhornApp,
136
+ createMatterhornAppDescriptor
137
+ };
@@ -0,0 +1,89 @@
1
+ const path = require('node:path');
2
+ const fs = require('node:fs');
3
+ const { cloneJson } = require('../../json.cjs');
4
+ const { buildMatterhornApp } = require('./build.cjs');
5
+ const { matterhornAppDescriptorFromBuilt, schemaArtifactsForBuilt } = require('./descriptors.cjs');
6
+ const { APP_MATTERHORN_META } = require('./shared.cjs');
7
+
8
+ const BUNDLE_DESCRIPTOR_KEYS = new Set([
9
+ 'id',
10
+ 'name',
11
+ 'version',
12
+ 'appPack',
13
+ 'hostPack',
14
+ 'hostPlugins',
15
+ 'playerPacks',
16
+ 'playerPlugins',
17
+ 'deployment',
18
+ 'frontend',
19
+ 'matterhorn'
20
+ ]);
21
+
22
+ function bundleDescriptorExtras(matterhornApp) {
23
+ const extras = {};
24
+ for (const [key, value] of Object.entries(matterhornApp)) {
25
+ if (!BUNDLE_DESCRIPTOR_KEYS.has(key) && value !== undefined) extras[key] = cloneJson(value);
26
+ }
27
+ return extras;
28
+ }
29
+
30
+ function cleanBundlePluginRef(plugin) {
31
+ const ref = {
32
+ kind: 'matterhorn.registry-host-plugin',
33
+ key: plugin.key,
34
+ id: plugin.id,
35
+ version: plugin.version
36
+ };
37
+ if (plugin.config !== undefined) ref.config = cloneJson(plugin.config);
38
+ return ref;
39
+ }
40
+
41
+ function bundleHostPluginRefs(built) {
42
+ const primaryId = built.compositionSchema.primaryPlugin.id;
43
+ const refs = [{
44
+ kind: 'matterhorn.schema-model-host-plugin',
45
+ id: primaryId,
46
+ version: built.compositionSchema.primaryPlugin.version,
47
+ composition: cloneJson(built.compositionSchema)
48
+ }];
49
+ for (const plugin of built.compositionSchema.plugins || []) refs.push(cleanBundlePluginRef(plugin));
50
+ return refs;
51
+ }
52
+
53
+ function createMatterhornAppBundle(app, options = {}) {
54
+ const built = buildMatterhornApp(app, options);
55
+ const matterhornApp = matterhornAppDescriptorFromBuilt(built);
56
+ return Object.freeze({
57
+ kind: 'matterhorn.app-bundle',
58
+ schemaVersion: 1,
59
+ id: matterhornApp.id,
60
+ name: matterhornApp.name,
61
+ version: matterhornApp.version,
62
+ appPack: matterhornApp.appPack,
63
+ hostPack: matterhornApp.hostPack,
64
+ hostPlugins: bundleHostPluginRefs(built),
65
+ playerPacks: matterhornApp.playerPacks,
66
+ deployment: matterhornApp.deployment,
67
+ ...(matterhornApp.frontend ? { frontend: matterhornApp.frontend } : {}),
68
+ ...(matterhornApp.matterhorn ? { matterhorn: matterhornApp.matterhorn } : {}),
69
+ ...bundleDescriptorExtras(matterhornApp),
70
+ compositionSchema: cloneJson(built.compositionSchema),
71
+ artifacts: schemaArtifactsForBuilt(built)
72
+ });
73
+ }
74
+
75
+ function emitMatterhornAppBundle(app, options = {}) {
76
+ const bundle = createMatterhornAppBundle(app, options);
77
+ const meta = app?.[APP_MATTERHORN_META] || {};
78
+ const outDir = path.resolve(options.outDir || options.packageRoot || meta.packageRoot || process.cwd());
79
+ const fileName = options.fileName || `${bundle.id}.json`;
80
+ const filePath = path.join(outDir, fileName);
81
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
82
+ fs.writeFileSync(filePath, `${JSON.stringify(bundle, null, 2)}\n`);
83
+ return Object.freeze({ bundlePath: filePath, bundle });
84
+ }
85
+
86
+ module.exports = {
87
+ createMatterhornAppBundle,
88
+ emitMatterhornAppBundle
89
+ };
@@ -0,0 +1,161 @@
1
+ const { manifestHash } = require('@mh-gg/base');
2
+ const { createSignedPartyEvent, nostrEventToPartyEvent, partyEventToNostrEvent } = require('@mh-gg/event');
3
+ const {
4
+ HostPluginRuntime,
5
+ createMemoryOperationLog,
6
+ createMemoryRoomStore
7
+ } = require('@mh-gg/host-runtime');
8
+ const { ensureOperationIdentity } = require('@mh-gg/protocol');
9
+ const { cloneJson } = require('../../json.cjs');
10
+ const {
11
+ createRecordPrefixes,
12
+ rememberCreatedDemoAlias,
13
+ rewriteDemoOperation
14
+ } = require('./demoAliases.cjs');
15
+
16
+ const DEMO_FILE_SIGNER = Object.freeze({
17
+ privateKey: '0000000000000000000000000000000000000000000000000000000000000001',
18
+ pubkey: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
19
+ });
20
+
21
+ function actorForDemo(value = {}) {
22
+ const role = value.role || 'admin';
23
+ const memberId = value.memberId || role;
24
+ return {
25
+ memberId,
26
+ deviceId: value.deviceId || `dev_${memberId}`,
27
+ role,
28
+ displayName: value.displayName || memberId,
29
+ ...value
30
+ };
31
+ }
32
+
33
+ function pluginIdFromDemoRef(ref, app) {
34
+ if (!ref || ref === 'primary' || ref === '$primary') return app.hostPlugin.id;
35
+ const plugin = app.appPack.composition.plugins.find((item) => item.key === ref || item.id === ref);
36
+ return plugin?.id || ref;
37
+ }
38
+
39
+ function payloadForDemoOperation(item, roomId) {
40
+ const payload = cloneJson(item.payload || {});
41
+ if (item.type !== 'file.upload' || !payload.event) return payload;
42
+
43
+ const partyEvent = nostrEventToPartyEvent(payload.event);
44
+ if (partyEvent.kind !== 'file.upload' || partyEvent.partyId === roomId) return payload;
45
+
46
+ const signed = createSignedPartyEvent({
47
+ kind: 'file.upload',
48
+ partyId: roomId,
49
+ identity: DEMO_FILE_SIGNER,
50
+ payload: partyEvent.payload,
51
+ createdAt: partyEvent.createdAt
52
+ });
53
+ return { ...payload, event: partyEventToNostrEvent(signed) };
54
+ }
55
+
56
+ function operationsFromDemoDefinition(demo, app, roomId = demo?.roomId) {
57
+ if (!demo) return [];
58
+ const appPackHash = manifestHash(app.appPack);
59
+ return (demo.operations || []).map((item, index) => {
60
+ const actor = actorForDemo(item.actor);
61
+ const createdAt = item.createdAt || ((demo.startTime || 1700000000000) + index + 1);
62
+ const operation = {
63
+ clientOperationId: item.id,
64
+ roomId,
65
+ appPackId: app.appPack.id,
66
+ appPackHash,
67
+ pluginId: pluginIdFromDemoRef(item.plugin || item.pluginId, app),
68
+ type: item.type,
69
+ actor,
70
+ seq: item.seq || index + 1,
71
+ createdAt,
72
+ hlc: item.hlc,
73
+ payload: payloadForDemoOperation(item, roomId),
74
+ auth: { credentialId: `cred_${actor.memberId}`, signature: 'sig' }
75
+ };
76
+ return ensureOperationIdentity(operation, { now: createdAt, nodeId: actor.deviceId || actor.memberId || 'demo' });
77
+ });
78
+ }
79
+
80
+ function countLive(value) {
81
+ if (Array.isArray(value)) return value.filter((item) => !item?.deletedAt && !item?.archivedAt && !item?.deleted).length;
82
+ if (value && typeof value === 'object') return Object.values(value).filter((item) => !item?.deletedAt && !item?.archivedAt && !item?.deleted).length;
83
+ return 0;
84
+ }
85
+
86
+ function addSharedPluginSummary(summary, state) {
87
+ const plugins = state.plugins || {};
88
+ const comments = plugins['com.matterhorn.examples.plugins.comments'];
89
+ if (comments?.comments) summary.sharedComments = countLive(comments.comments);
90
+ const mediaRooms = plugins['com.matterhorn.examples.plugins.media-rooms'];
91
+ if (mediaRooms?.rooms) summary.mediaRooms = countLive(mediaRooms.rooms);
92
+ const screenShares = plugins['com.matterhorn.examples.plugins.screen-share'];
93
+ if (screenShares?.shares) summary.activeScreenShares = Object.values(screenShares.shares).filter((share) => !share.stoppedAt).length;
94
+ const presence = plugins['com.matterhorn.examples.plugins.presence'];
95
+ if (presence?.members) summary.onlineMembers = countLive(presence.members);
96
+ const embeds = plugins['com.matterhorn.examples.plugins.embeds'];
97
+ if (embeds?.embeds) summary.embeds = countLive(embeds.embeds);
98
+ const reactions = plugins['com.matterhorn.examples.plugins.reactions'];
99
+ if (reactions?.reactions) summary.scopedReactions = countLive(reactions.reactions);
100
+ return summary;
101
+ }
102
+
103
+ function genericSummary(state, pluginId) {
104
+ const pluginState = state.plugins?.[pluginId] || {};
105
+ const summary = { version: state.version };
106
+ for (const [key, value] of Object.entries(pluginState)) {
107
+ if (Array.isArray(value) || (value && typeof value === 'object' && key !== 'details' && key !== 'settings')) summary[key] = countLive(value);
108
+ }
109
+ if (pluginState.messages && typeof pluginState.messages === 'object') {
110
+ const messages = Object.values(pluginState.messages);
111
+ summary.visibleMessages = messages.filter((message) => !message.deletedAt).length;
112
+ summary.pinnedMessages = messages.filter((message) => message.pinnedAt).length;
113
+ }
114
+ if (pluginState.tasks && typeof pluginState.tasks === 'object') {
115
+ const tasks = Object.values(pluginState.tasks);
116
+ summary.totalTasks = tasks.length;
117
+ summary.openTasks = tasks.filter((task) => task.status !== 'done' && !task.archivedAt).length;
118
+ summary.completedTasks = tasks.filter((task) => task.status === 'done' && !task.archivedAt).length;
119
+ }
120
+ if (pluginState.notes && typeof pluginState.notes === 'object') summary.noteCount = countLive(pluginState.notes);
121
+ if (pluginState.details?.title) summary.title = pluginState.details.title;
122
+ return summary;
123
+ }
124
+
125
+ async function replayDemo(app, operations, roomId, summary) {
126
+ const runtime = new HostPluginRuntime({
127
+ room: {
128
+ id: roomId,
129
+ appPack: {
130
+ id: app.appPack.id,
131
+ version: app.appPack.version,
132
+ hash: manifestHash(app.appPack),
133
+ protocolHash: app.appPack.compatibility.appProtocolHash
134
+ }
135
+ },
136
+ plugins: app.hostPlugins,
137
+ store: createMemoryRoomStore(),
138
+ operationLog: createMemoryOperationLog(),
139
+ authenticateActor: async (_auth, actor) => actor
140
+ });
141
+ await runtime.start();
142
+ const aliases = new Map();
143
+ const createPrefixes = createRecordPrefixes(app);
144
+ for (const operation of operations) {
145
+ const operationForReplay = rewriteDemoOperation(operation, aliases);
146
+ const result = await runtime.handleOperation(operationForReplay);
147
+ if (!result.ok) throw new Error(result.reason || result.error || `Demo operation ${operation.type} failed`);
148
+ rememberCreatedDemoAlias(operationForReplay, result, aliases, createPrefixes);
149
+ }
150
+ const state = await runtime.getState();
151
+ const guestView = await runtime.publicView(actorForDemo({ memberId: 'guest', role: 'guest' }));
152
+ const computedSummary = summary ? summary(state) : genericSummary(state, app.hostPlugin.id);
153
+ addSharedPluginSummary(computedSummary, state);
154
+ return { runtime, operations, state, guestView, summary: computedSummary };
155
+ }
156
+
157
+ module.exports = {
158
+ genericSummary,
159
+ operationsFromDemoDefinition,
160
+ replayDemo
161
+ };
@@ -0,0 +1,50 @@
1
+ const { ensureOperationIdentity } = require('@mh-gg/protocol');
2
+
3
+ function createRecordPrefixes(app) {
4
+ const prefixes = new Map();
5
+ const primaryPluginId = app.hostPlugin?.id || app.appPack?.composition?.primaryPlugin?.id;
6
+ const primaryOperations = app.appPack?.composition?.primaryPlugin?.model?.operations || {};
7
+ for (const [type, descriptor] of Object.entries(primaryOperations)) {
8
+ const createEffect = (descriptor.effects || []).find((effect) => effect?.kind === 'createRecord' && effect.idPrefix);
9
+ if (createEffect && primaryPluginId) prefixes.set(`${primaryPluginId}:${type}`, createEffect.idPrefix);
10
+ }
11
+ return prefixes;
12
+ }
13
+
14
+ function replaceDemoAliases(value, aliases) {
15
+ if (typeof value === 'string') return aliases.get(value) || value;
16
+ if (Array.isArray(value)) return value.map((item) => replaceDemoAliases(item, aliases));
17
+ if (value && typeof value === 'object') {
18
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceDemoAliases(item, aliases)]));
19
+ }
20
+ return value;
21
+ }
22
+
23
+ function rememberCreatedDemoAlias(operation, result, aliases, createPrefixes) {
24
+ const prefix = createPrefixes.get(`${operation.pluginId}:${operation.type}`) || inferredCreatePrefix(operation.type);
25
+ const legacyId = operation.clientOperationId;
26
+ const ledgerId = result?.acceptedLedgerId || result?.acceptedSnowflakeId;
27
+ if (!prefix || !legacyId || !ledgerId) return;
28
+ aliases.set(`${prefix}_${legacyId}`, `${prefix}_${ledgerId}`);
29
+ }
30
+
31
+ function inferredCreatePrefix(type) {
32
+ if (typeof type !== 'string') return undefined;
33
+ if (!/(^|\.)(create|add|send|upload)$/.test(type)) return undefined;
34
+ const prefix = type.split('.')[0]?.replace(/[^A-Za-z0-9_-]/g, '_');
35
+ return prefix || undefined;
36
+ }
37
+
38
+ function rewriteDemoOperation(operation, aliases) {
39
+ const rewritten = replaceDemoAliases(operation, aliases);
40
+ return ensureOperationIdentity(rewritten, {
41
+ now: rewritten.createdAt,
42
+ nodeId: rewritten.actor?.deviceId || rewritten.actor?.memberId || 'demo'
43
+ });
44
+ }
45
+
46
+ module.exports = {
47
+ createRecordPrefixes,
48
+ rememberCreatedDemoAlias,
49
+ rewriteDemoOperation
50
+ };
@@ -0,0 +1,59 @@
1
+ const { cloneJson } = require('../../json.cjs');
2
+ const { generateAppTypeDeclaration } = require('../../types/index.cjs');
3
+ const { frontendDescriptor } = require('./frontend.cjs');
4
+ const { typeNamespaceForBuilt } = require('./shared.cjs');
5
+
6
+ function matterhornAppDescriptorFromBuilt(built) {
7
+ const config = built.config;
8
+ const frontend = frontendDescriptor(config);
9
+ const matterhornMetadata = config.matterhornApp || (config.matterhorn ? { matterhorn: config.matterhorn } : {});
10
+ return Object.freeze({
11
+ id: config.appId,
12
+ name: config.name,
13
+ version: config.version,
14
+ appPack: built.appPack,
15
+ hostPack: built.hostPack,
16
+ hostPlugins: built.hostPlugins,
17
+ playerPacks: [built.playerPack],
18
+ playerPlugins: [built.playerPlugin],
19
+ deployment: {
20
+ kind: 'matterhorn.self-contained-app',
21
+ host: { runner: 'matterhorn-example-host' },
22
+ relay: { mode: 'embedded', autoStart: true, acceptsPeerRelays: true },
23
+ frontendDelivery: { mode: frontend?.bundle ? 'relay-chunks' : 'external' },
24
+ ...(config.deployment || {})
25
+ },
26
+ ...(frontend ? { frontend } : {}),
27
+ ...matterhornMetadata
28
+ });
29
+ }
30
+
31
+ function matterhornAppMetadataForBuilt(built) {
32
+ const typeNamespace = typeNamespaceForBuilt(built);
33
+ const generatedTypes = generateAppTypeDeclaration(built.compositionSchema, { namespace: typeNamespace, registry: built.config.registry });
34
+ return {
35
+ typeNamespace,
36
+ generatedTypes
37
+ };
38
+ }
39
+
40
+ function schemaArtifactsForBuilt(built) {
41
+ return Object.freeze({
42
+ composition: cloneJson(built.compositionSchema),
43
+ model: cloneJson(built.compositionSchema.primaryPlugin.model || {}),
44
+ actions: Object.fromEntries((built.compositionSchema.actions || []).map((action) => [action.name, cloneJson({
45
+ plugin: action.plugin,
46
+ type: action.type,
47
+ payloadDefaults: action.payloadDefaults,
48
+ payloadSchema: action.payloadSchema,
49
+ requiredRole: action.requiredRole,
50
+ label: action.label
51
+ })]))
52
+ });
53
+ }
54
+
55
+ module.exports = {
56
+ matterhornAppDescriptorFromBuilt,
57
+ matterhornAppMetadataForBuilt,
58
+ schemaArtifactsForBuilt
59
+ };
@@ -0,0 +1,95 @@
1
+ const { maybeClone, pascalCase } = require('./shared.cjs');
2
+ const { buildMatterhornApp } = require('./build.cjs');
3
+ const { matterhornAppDescriptorFromBuilt, matterhornAppMetadataForBuilt } = require('./descriptors.cjs');
4
+ const { operationsFromDemoDefinition, replayDemo } = require('./demo.cjs');
5
+ const { primaryHostPluginForBuilt } = require('./plugins.cjs');
6
+
7
+ function createMatterhornAppExports(app, options = {}) {
8
+ const built = buildMatterhornApp(app, options);
9
+ const config = built.config;
10
+ const matterhornAppDescriptor = matterhornAppDescriptorFromBuilt(built);
11
+ const hostPlugin = primaryHostPluginForBuilt(built);
12
+ const metadata = matterhornAppMetadataForBuilt(built);
13
+ const generatedTypes = metadata.generatedTypes;
14
+ const schemaArtifacts = Object.freeze({
15
+ schema: app.toJSON(),
16
+ types: generatedTypes,
17
+ files: Object.freeze({
18
+ schema: 'matterhorn.schema.json',
19
+ types: 'matterhorn.types.d.ts'
20
+ })
21
+ });
22
+ const matterhornApp = Object.freeze({
23
+ ...matterhornAppDescriptor,
24
+ types: Object.freeze({ namespace: metadata.typeNamespace, declaration: generatedTypes }),
25
+ artifacts: schemaArtifacts
26
+ });
27
+ const demo = config.demo;
28
+ const exportPrefix = config.exportPrefix || config.slug;
29
+ const constantPrefix = (config.constantPrefix || exportPrefix).replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
30
+ const pascal = pascalCase(exportPrefix);
31
+ const example = Object.freeze({
32
+ id: config.example?.id || `examples/${config.slug}`,
33
+ title: config.example?.title || `${config.name} Matterhorn example`,
34
+ appPack: config.appId,
35
+ hostPlugin: hostPlugin.id
36
+ });
37
+ const appDefinition = Object.freeze({
38
+ slug: config.slug,
39
+ packageName: config.packageName,
40
+ appId: config.appId,
41
+ pluginId: hostPlugin.id,
42
+ version: config.version,
43
+ name: config.name,
44
+ frontend: maybeClone(config.frontend)
45
+ });
46
+ const appExport = {
47
+ built,
48
+ matterhornApp,
49
+ appPack: built.appPack,
50
+ hostPack: built.hostPack,
51
+ hostPlugin,
52
+ hostPlugins: built.hostPlugins,
53
+ hostPluginDescriptors: built.hostPluginDescriptors,
54
+ playerPack: built.playerPack,
55
+ playerPlugin: built.playerPlugin,
56
+ playerPacks: [built.playerPack],
57
+ playerPlugins: [built.playerPlugin],
58
+ compositionSchema: built.compositionSchema,
59
+ example,
60
+ generatedTypes,
61
+ schemaArtifacts,
62
+ appDefinition,
63
+ demoDefinition: demo,
64
+ [`${constantPrefix}_APP_ID`]: config.appId,
65
+ [`${constantPrefix}_PLUGIN_ID`]: hostPlugin.id,
66
+ [`${constantPrefix}_VERSION`]: config.version,
67
+ [`${exportPrefix}AppPack`]: built.appPack,
68
+ [`${exportPrefix}HostPack`]: built.hostPack,
69
+ [`${exportPrefix}HostPlugin`]: hostPlugin,
70
+ [`${exportPrefix}HostPlugins`]: built.hostPlugins,
71
+ [`${exportPrefix}HostPluginDescriptors`]: built.hostPluginDescriptors,
72
+ [`${exportPrefix}PlayerPack`]: built.playerPack,
73
+ [`${exportPrefix}PlayerPacks`]: [built.playerPack],
74
+ [`${exportPrefix}PlayerPlugin`]: built.playerPlugin,
75
+ [`${exportPrefix}PlayerPlugins`]: [built.playerPlugin],
76
+ [`${exportPrefix}CompositionSchema`]: built.compositionSchema,
77
+ [`${exportPrefix}GeneratedTypes`]: generatedTypes,
78
+ [`${exportPrefix}SchemaArtifacts`]: schemaArtifacts
79
+ };
80
+ appExport.createDemoOperations = (roomId = demo?.roomId || `${config.slug}_demo`) => operationsFromDemoDefinition(demo, appExport, roomId);
81
+ appExport[`create${pascal}DemoOperations`] = appExport.createDemoOperations;
82
+ appExport.createDemo = async (demoOptions = {}) => {
83
+ if (!demo) throw new Error(`${config.name} does not define a demo`);
84
+ const roomId = demoOptions.roomId || demo.roomId;
85
+ return replayDemo(appExport, appExport.createDemoOperations(roomId), roomId, config.summary);
86
+ };
87
+ appExport[`create${pascal}Demo`] = appExport.createDemo;
88
+ for (const [alias, target] of Object.entries(config.exportAliases || {})) {
89
+ if (appExport[target] === undefined) throw new Error(`Export alias ${alias} points at missing ${target}`);
90
+ appExport[alias] = appExport[target];
91
+ }
92
+ return Object.freeze(appExport);
93
+ }
94
+
95
+ module.exports = { createMatterhornAppExports };
@@ -0,0 +1,80 @@
1
+ const path = require('node:path');
2
+
3
+ function defaultBundleDevCommand() {
4
+ return {
5
+ command: 'npm',
6
+ args: ['run', 'dev:frontend', '--', '--host', '127.0.0.1', '--port', '${bundlePort}', '--strictPort']
7
+ };
8
+ }
9
+
10
+ function defaultBundleBuildCommand() {
11
+ return { command: 'npm', args: ['run', 'build:frontend'] };
12
+ }
13
+
14
+ function cleanBackgroundColor(value) {
15
+ if (typeof value !== 'string') return undefined;
16
+ const color = value.trim();
17
+ if (!color || color.length > 128) return undefined;
18
+ return color;
19
+ }
20
+
21
+ function cleanIconText(value, max = 64 * 1024) {
22
+ if (typeof value !== 'string') return undefined;
23
+ const cleaned = value.trim();
24
+ if (!cleaned || cleaned.length > max) return undefined;
25
+ return cleaned;
26
+ }
27
+
28
+ function cleanIcon(value) {
29
+ if (typeof value === 'string') {
30
+ const iconPath = cleanIconText(value, 512);
31
+ return iconPath ? { path: iconPath } : undefined;
32
+ }
33
+ if (!value || typeof value !== 'object') return undefined;
34
+ const iconPath = cleanIconText(value.path, 512);
35
+ const svg = cleanIconText(value.svg);
36
+ const label = cleanIconText(value.label, 120);
37
+ if (!iconPath && !svg) return undefined;
38
+ return {
39
+ ...(iconPath ? { path: iconPath } : {}),
40
+ ...(svg ? { svg } : {}),
41
+ ...(label ? { label } : {})
42
+ };
43
+ }
44
+
45
+ function frontendDescriptor(options) {
46
+ const frontend = options.frontend;
47
+ if (frontend === false || frontend === undefined) return undefined;
48
+ const appId = options.appId;
49
+ const slug = options.slug;
50
+ const name = options.name;
51
+ const root = path.resolve(frontend.root || options.packageRoot || process.cwd());
52
+ const defaultPort = frontend.defaultPort || frontend.port || 42730;
53
+ const bundlePort = frontend.bundlePort || frontend.port || frontend.defaultPort || 42730;
54
+ const bundleOverrides = frontend.bundle && typeof frontend.bundle === 'object' ? frontend.bundle : {};
55
+ const devEntry = bundleOverrides.devEntry || frontend.devEntry || 'src/index.ts';
56
+ const backgroundColor = cleanBackgroundColor(frontend.backgroundColor) || cleanBackgroundColor(bundleOverrides.backgroundColor);
57
+ const icon = cleanIcon(frontend.icon) || cleanIcon(bundleOverrides.icon);
58
+ return {
59
+ kind: frontend.kind || 'matterhorn-player-bundle',
60
+ appId,
61
+ root,
62
+ defaultPort,
63
+ locations: frontend.locations || [{ type: 'local-path', ref: './frontend', label: frontend.label || `${name} frontend package` }],
64
+ ...(backgroundColor ? { backgroundColor } : {}),
65
+ ...(icon ? { icon } : {}),
66
+ bundle: {
67
+ mountPath: bundleOverrides.mountPath || frontend.mountPath || `/matterhorn/apps/${appId}/`,
68
+ defaultPort: bundleOverrides.defaultPort || bundlePort,
69
+ devEntry,
70
+ builtEntry: bundleOverrides.builtEntry || frontend.builtEntry || `matterhorn-${slug}.js`,
71
+ healthPath: bundleOverrides.healthPath || frontend.healthPath || devEntry,
72
+ dev: bundleOverrides.dev || frontend.dev || defaultBundleDevCommand(),
73
+ build: bundleOverrides.build || frontend.build || defaultBundleBuildCommand(),
74
+ dist: bundleOverrides.dist || frontend.dist || 'frontend/dist',
75
+ ...(bundleOverrides.prebuilt === undefined ? {} : { prebuilt: bundleOverrides.prebuilt })
76
+ }
77
+ };
78
+ }
79
+
80
+ module.exports = { frontendDescriptor };