@microverse.ts/host-surface 0.1.0 → 0.3.0

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 (53) hide show
  1. package/README.md +53 -22
  2. package/dist/application/ports/ScriptReferenceResolverPort.d.ts +13 -0
  3. package/dist/application/ports/ScriptReferenceResolverPort.d.ts.map +1 -0
  4. package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts +4 -4
  5. package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts.map +1 -1
  6. package/dist/application/useCases/compileHostSurface.d.ts +7 -6
  7. package/dist/application/useCases/compileHostSurface.d.ts.map +1 -1
  8. package/dist/domain/componentSlotPrelude.d.ts +11 -0
  9. package/dist/domain/componentSlotPrelude.d.ts.map +1 -0
  10. package/dist/domain/componentTypeSpec.d.ts +12 -0
  11. package/dist/domain/componentTypeSpec.d.ts.map +1 -0
  12. package/dist/domain/hostSurfaceManifest.d.ts +3 -2
  13. package/dist/domain/hostSurfaceManifest.d.ts.map +1 -1
  14. package/dist/domain/hostSurfaceSpecTypes.d.ts +89 -0
  15. package/dist/domain/hostSurfaceSpecTypes.d.ts.map +1 -0
  16. package/dist/domain/hostSurfaceTypes.d.ts +16 -92
  17. package/dist/domain/hostSurfaceTypes.d.ts.map +1 -1
  18. package/dist/domain/luaGlobalHook.d.ts +1 -1
  19. package/dist/domain/safeObjectKey.d.ts +1 -1
  20. package/dist/domain/safeObjectKey.d.ts.map +1 -1
  21. package/dist/domain/scriptCatalogManifest.d.ts +14 -0
  22. package/dist/domain/scriptCatalogManifest.d.ts.map +1 -0
  23. package/dist/domain/scriptContextSymbol.d.ts +6 -0
  24. package/dist/domain/scriptContextSymbol.d.ts.map +1 -0
  25. package/dist/domain/scriptProfileSpec.d.ts +31 -0
  26. package/dist/domain/scriptProfileSpec.d.ts.map +1 -0
  27. package/dist/domain/scriptPropertyMergeEnv.d.ts +6 -0
  28. package/dist/domain/scriptPropertyMergeEnv.d.ts.map +1 -0
  29. package/dist/domain/surfaceCapabilities.d.ts +1 -1
  30. package/dist/domain/surfaceCapabilities.d.ts.map +1 -1
  31. package/dist/domain/surfaceMethodDef.d.ts +1 -1
  32. package/dist/domain/surfaceMethodDef.d.ts.map +1 -1
  33. package/dist/index.d.ts +30 -14
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +846 -258
  36. package/dist/index.js.map +1 -1
  37. package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts +1 -1
  38. package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts.map +1 -1
  39. package/dist/infrastructure/adapters/augmentHostWithScriptContext.d.ts +4 -0
  40. package/dist/infrastructure/adapters/augmentHostWithScriptContext.d.ts.map +1 -0
  41. package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts +1 -1
  42. package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts.map +1 -1
  43. package/dist/infrastructure/builders/bridgeMergeEnv.d.ts +6 -8
  44. package/dist/infrastructure/builders/bridgeMergeEnv.d.ts.map +1 -1
  45. package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts +5 -5
  46. package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts.map +1 -1
  47. package/dist/infrastructure/builders/filterBridgeDeclarations.d.ts +11 -0
  48. package/dist/infrastructure/builders/filterBridgeDeclarations.d.ts.map +1 -0
  49. package/dist/infrastructure/builders/surfaceBuilder.d.ts +16 -11
  50. package/dist/infrastructure/builders/surfaceBuilder.d.ts.map +1 -1
  51. package/dist/infrastructure/components/hostScriptSession.d.ts +42 -72
  52. package/dist/infrastructure/components/hostScriptSession.d.ts.map +1 -1
  53. package/package.json +10 -9
package/dist/index.js CHANGED
@@ -1,18 +1,404 @@
1
1
  import { buildDeclarativeBridgeTable } from "@microverse.ts/runtime-bridge";
2
- import { validateWithZodSchema } from "@microverse.ts/runtime-zod";
2
+ import { applyScriptPropertyChanges, assertValidScriptPropertyBag, cloneScriptPropertyBag, createMicroverseScript, createScriptInstanceContext, diffScriptProperties } from "@microverse.ts/runtime-core";
3
+ import { createCapabilityId } from "@microverse.ts/runtime-capabilities";
3
4
  import { z } from "zod";
4
- import { InMemoryCapabilityRegistry, createAllowlist, createCapabilityId } from "@microverse.ts/runtime-capabilities";
5
- import { createMicroverseScript } from "@microverse.ts/runtime-core";
5
+ import { validateWithZodSchema } from "@microverse.ts/runtime-zod";
6
+ //#region src/domain/scriptContextSymbol.ts
7
+ var MICROVERSE_SCRIPT_CONTEXT = Symbol("microverse.scriptContext");
8
+ //#endregion
9
+ //#region src/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.ts
10
+ function isThenable(value) {
11
+ if (value === null || value === void 0) return false;
12
+ if (typeof value !== "object" && typeof value !== "function") return false;
13
+ return typeof value.then === "function";
14
+ }
15
+ /**
16
+ * Builds declarative bridge declarations from a host surface spec, using the schema validation port for Lua ↔ host payloads.
17
+ */
18
+ function createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec) {
19
+ const out = [];
20
+ for (const bridgeName of Object.keys(spec)) {
21
+ const methods = spec[bridgeName];
22
+ out.push({
23
+ name: bridgeName,
24
+ perEntity: true,
25
+ createApi: (host, slotKey) => {
26
+ const api = {};
27
+ for (const methodName of Object.keys(methods)) {
28
+ const entry = methods[methodName];
29
+ api[methodName] = (...args) => {
30
+ const payload = args.length >= 2 ? args[1] : args[0];
31
+ const parsedIn = schemaValidation.validateWithZodSchema(entry.input, payload);
32
+ if (parsedIn._tag === "err") throw new Error(parsedIn.error);
33
+ const script = host[MICROVERSE_SCRIPT_CONTEXT] ?? createScriptInstanceContext({
34
+ instanceId: String(slotKey),
35
+ scriptId: "unknown",
36
+ slotKey: String(slotKey)
37
+ });
38
+ const raw = entry.handler({
39
+ host,
40
+ slotKey: String(slotKey),
41
+ script
42
+ }, parsedIn.value);
43
+ if (isThenable(raw)) return raw.then((resolved) => {
44
+ const parsedOut = schemaValidation.validateWithZodSchema(entry.output, resolved);
45
+ if (parsedOut._tag === "err") throw new Error(parsedOut.error);
46
+ return parsedOut.value;
47
+ });
48
+ const parsedOut = schemaValidation.validateWithZodSchema(entry.output, raw);
49
+ if (parsedOut._tag === "err") throw new Error(parsedOut.error);
50
+ return parsedOut.value;
51
+ };
52
+ }
53
+ return Object.freeze(api);
54
+ }
55
+ });
56
+ }
57
+ return out;
58
+ }
59
+ //#endregion
60
+ //#region src/infrastructure/builders/filterBridgeDeclarations.ts
61
+ /**
62
+ * Keeps bridge declarations (and methods) whose capability is in the allow set.
63
+ */
64
+ function filterBridgeDeclarationsByCapabilities(declarations, spec, capabilities) {
65
+ const allowed = new Set(capabilities.map((c) => String(c)));
66
+ const bridgeNames = /* @__PURE__ */ new Set();
67
+ for (const bridgeName of Object.keys(spec)) {
68
+ const methods = spec[bridgeName];
69
+ for (const methodName of Object.keys(methods)) {
70
+ const entry = methods[methodName];
71
+ if (allowed.has(String(entry.capability))) {
72
+ bridgeNames.add(bridgeName);
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ return declarations.filter((d) => bridgeNames.has(d.name)).map((d) => ({
78
+ ...d,
79
+ createApi: (host, slotKey) => {
80
+ const api = d.createApi(host, slotKey);
81
+ const methods = spec[d.name];
82
+ const filtered = {};
83
+ for (const methodName of Object.keys(api)) {
84
+ const entry = methods[methodName];
85
+ if (entry !== void 0 && allowed.has(String(entry.capability))) filtered[methodName] = api[methodName];
86
+ }
87
+ return Object.freeze(filtered);
88
+ }
89
+ }));
90
+ }
91
+ function createFilteredBridgeDeclarations(schemaValidation, spec, capabilities) {
92
+ return filterBridgeDeclarationsByCapabilities(createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec), spec, capabilities);
93
+ }
94
+ //#endregion
6
95
  //#region src/infrastructure/builders/bridgeMergeEnv.ts
7
96
  /**
8
- * Builds a frozen `mergeEnv` table: bridge name API object, ready for `MicroverseSlot.run({ mergeEnv })`.
9
- *
10
- * @param host - Your host context, already extended with the capability registry symbol from `@microverse.ts/host-surface`.
11
- * @param slotKey - Same slot key passed to `buildDeclarativeBridgeTable` (string form of `MicroverseId` is fine).
12
- * @param surface - Result of {@link defineHostSurface} (implements {@link HostSurfaceCore}).
97
+ * Builds a frozen bridge table for a component profile capability set.
98
+ */
99
+ function buildBridgeMergeEnvForProfile(schemaValidation, host, slotKey, spec, capabilities) {
100
+ return buildDeclarativeBridgeTable(host, slotKey, [...createFilteredBridgeDeclarations(schemaValidation, spec, capabilities)]);
101
+ }
102
+ //#endregion
103
+ //#region src/domain/safeObjectKey.ts
104
+ /** Keys that must not be used as dynamic property names on ordinary objects. */
105
+ var FORBIDDEN_OBJECT_KEYS = new Set([
106
+ "__proto__",
107
+ "constructor",
108
+ "prototype"
109
+ ]);
110
+ /**
111
+ * Rejects bridge/method names that could trigger prototype pollution when used as object keys.
13
112
  */
14
- function buildBridgeMergeEnvForHost(host, slotKey, surface) {
15
- return buildDeclarativeBridgeTable(host, slotKey, [...surface.toBridgeDeclarations()]);
113
+ function assertSafeObjectKey(kind, name) {
114
+ if (FORBIDDEN_OBJECT_KEYS.has(name)) throw new Error(`Invalid surface ${kind} name "${name}": reserved key`);
115
+ }
116
+ /** Record with no inherited prototype — safe for dynamic string keys at runtime. */
117
+ function createNullPrototypeRecord() {
118
+ return Object.create(null);
119
+ }
120
+ //#endregion
121
+ //#region src/domain/surfaceCapabilities.ts
122
+ /** Runtime list of capability ids declared on a compiled surface spec. */
123
+ function collectCapabilitiesFromHostSurfaceSpec(spec) {
124
+ const out = [];
125
+ const seen = /* @__PURE__ */ new Set();
126
+ for (const bridgeName of Object.keys(spec)) {
127
+ const methods = spec[bridgeName];
128
+ for (const methodName of Object.keys(methods)) {
129
+ const id = methods[methodName].capability;
130
+ const key = String(id);
131
+ if (!seen.has(key)) {
132
+ seen.add(key);
133
+ out.push(id);
134
+ }
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ /** Narrows `capabilities` to those declared on the surface (runtime check). */
140
+ function pickSurfaceCapabilities(surfaceCapabilities, ...capabilities) {
141
+ const allowed = new Set(surfaceCapabilities.map((c) => String(c)));
142
+ for (const id of capabilities) if (!allowed.has(String(id))) throw new Error(`capability not declared on host surface: ${String(id)} (surface has: ${[...allowed].join(", ")})`);
143
+ return capabilities;
144
+ }
145
+ z.object({});
146
+ var EMPTY_STATE = z.object({});
147
+ function scriptProfileComponentClassName(profileName) {
148
+ return `${profileName}Component`;
149
+ }
150
+ function scriptProfilePropsAlias(profileName) {
151
+ return `${profileName}Props`;
152
+ }
153
+ function scriptProfileStateAlias(profileName) {
154
+ return `${profileName}State`;
155
+ }
156
+ function scriptProfileBridgesClassName(profileName) {
157
+ return `${profileName}Bridges`;
158
+ }
159
+ /** Bridge table names whose methods include at least one capability from the profile. */
160
+ function bridgeNamesForCapabilities(spec, capabilities) {
161
+ const allowed = new Set(capabilities.map((c) => String(c)));
162
+ const names = [];
163
+ for (const bridgeName of Object.keys(spec)) {
164
+ const methods = spec[bridgeName];
165
+ for (const methodName of Object.keys(methods)) {
166
+ const entry = methods[methodName];
167
+ if (allowed.has(String(entry.capability))) {
168
+ names.push(bridgeName);
169
+ break;
170
+ }
171
+ }
172
+ }
173
+ return names.sort((a, b) => a.localeCompare(b));
174
+ }
175
+ function resolveScriptProfile(registry, name, spec) {
176
+ const def = registry[name];
177
+ if (def === void 0) throw new Error(`unknown script profile: ${name}`);
178
+ const capabilities = def.capabilities.map((c) => createCapabilityId(c));
179
+ let props = def.props;
180
+ let state = def.state ?? EMPTY_STATE;
181
+ const hooks = [...def.hooks ?? []];
182
+ let parentName = def.extends;
183
+ const visited = new Set([name]);
184
+ while (parentName !== void 0) {
185
+ if (visited.has(parentName)) throw new Error(`script profile inheritance cycle: ${name}`);
186
+ visited.add(parentName);
187
+ const parent = registry[parentName];
188
+ if (parent === void 0) throw new Error(`script profile "${name}" extends unknown profile "${parentName}"`);
189
+ const parentCaps = parent.capabilities.map((c) => createCapabilityId(c));
190
+ for (const cap of parentCaps) if (!capabilities.some((c) => String(c) === String(cap))) capabilities.push(cap);
191
+ props = parent.props.merge(props);
192
+ state = (parent.state ?? EMPTY_STATE).merge(state);
193
+ const parentHooks = parent.hooks ?? [];
194
+ for (const h of parentHooks) if (!hooks.includes(h)) hooks.push(h);
195
+ parentName = parent.extends;
196
+ }
197
+ hooks.sort((a, b) => a.localeCompare(b));
198
+ const bridgeNames = bridgeNamesForCapabilities(spec, capabilities);
199
+ return {
200
+ name,
201
+ extends: def.extends,
202
+ capabilities,
203
+ props,
204
+ state,
205
+ hooks,
206
+ bridgeNames,
207
+ references: def.references
208
+ };
209
+ }
210
+ function buildResolvedScriptProfileRegistry(registry, spec) {
211
+ const out = {};
212
+ for (const name of Object.keys(registry)) out[name] = resolveScriptProfile(registry, name, spec);
213
+ return out;
214
+ }
215
+ function validateScriptProfileRegistry(registry, spec, componentHooks, opts) {
216
+ if ((opts?.requireAtLeastOne ?? false) && Object.keys(registry).length === 0) throw new Error("host surface: at least one script profile or .componentType() is required");
217
+ const surfaceCaps = new Set(collectCapabilitiesFromHostSurfaceSpec(spec).map((c) => String(c)));
218
+ const hookKinds = componentHooks !== void 0 ? new Set(Object.keys(componentHooks)) : /* @__PURE__ */ new Set();
219
+ for (const name of Object.keys(registry)) {
220
+ assertSafeObjectKey("componentType", name);
221
+ const def = registry[name];
222
+ if (def.extends !== void 0) assertSafeObjectKey("componentType", def.extends);
223
+ for (const cap of def.capabilities) {
224
+ const id = createCapabilityId(cap);
225
+ if (!surfaceCaps.has(String(id))) throw new Error(`script profile "${name}": capability not declared on surface: ${String(cap)}`);
226
+ }
227
+ for (const hook of def.hooks ?? []) if (!hookKinds.has(hook)) throw new Error(`script profile "${name}": hook "${hook}" not declared in .componentHooks()`);
228
+ if (def.extends !== void 0 && registry[def.extends] === void 0) throw new Error(`script profile "${name}" extends unknown profile "${def.extends}"`);
229
+ }
230
+ for (const name of Object.keys(registry)) resolveScriptProfile(registry, name, spec);
231
+ }
232
+ //#endregion
233
+ //#region src/domain/scriptCatalogManifest.ts
234
+ /** LuaCATS aliases per catalog scriptId → resolved component class (for `---@type` in `.lua` files). */
235
+ function buildScriptCatalogLuaDefManifest(entries) {
236
+ return {
237
+ schemaVersion: 1,
238
+ output: "generated/scriptCatalog.d.lua",
239
+ classes: [],
240
+ aliases: entries.filter((entry) => entry.localComponentClass !== true).slice().sort((a, b) => a.scriptId.localeCompare(b.scriptId)).map((entry) => {
241
+ const componentClass = scriptProfileComponentClassName(entry.profileId);
242
+ return {
243
+ name: scriptCatalogComponentAlias(entry.scriptId),
244
+ definition: componentClass
245
+ };
246
+ }),
247
+ globals: [],
248
+ luaHooks: []
249
+ };
250
+ }
251
+ function scriptCatalogComponentAlias(scriptId) {
252
+ return `${scriptId.replace(/[^A-Za-z0-9_]/g, "_")}ScriptComponent`;
253
+ }
254
+ //#endregion
255
+ //#region src/domain/componentSlotPrelude.ts
256
+ /** @see MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE */
257
+ var MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE = `
258
+ rawset(_ENV, "__microverse_component_rawProps", {})
259
+ rawset(_ENV, "__microverse_component_dirty", {})
260
+
261
+ local function rawProps()
262
+ return rawget(_ENV, "__microverse_component_rawProps")
263
+ end
264
+
265
+ local function dirty()
266
+ return rawget(_ENV, "__microverse_component_dirty")
267
+ end
268
+
269
+ local PropertiesMT = {
270
+ __index = function(t, k)
271
+ return rawget(t, "__raw")[k]
272
+ end,
273
+ __newindex = function(t, k, v)
274
+ rawget(t, "__raw")[k] = v
275
+ local d = dirty()
276
+ d[k] = v
277
+ end,
278
+ }
279
+
280
+ local ReferencesMT = {
281
+ __index = function(t, k)
282
+ local wrap = rawget(_ENV, "__microverse_reference_wrap")
283
+ local raw = rawget(t, "__raw")
284
+ local val = raw[k]
285
+ if type(wrap) == "function" then
286
+ return wrap(k, val)
287
+ end
288
+ return val
289
+ end,
290
+ }
291
+
292
+ function __microverse_lua_build_component_impl(bridges)
293
+ local impl = { state = {}, bridges = type(bridges) == "table" and bridges or {} }
294
+ local proxy = { __raw = rawProps() }
295
+ setmetatable(proxy, PropertiesMT)
296
+ impl.properties = proxy
297
+ local refProxy = { __raw = rawProps() }
298
+ setmetatable(refProxy, ReferencesMT)
299
+ impl.references = refProxy
300
+ local base = rawget(_ENV, "__microverse_component_hook_base")
301
+ if type(base) == "table" then
302
+ setmetatable(impl, { __index = base })
303
+ end
304
+ rawset(_ENV, "__microverse_lua_ComponentImpl", impl)
305
+ return impl
306
+ end
307
+
308
+ rawset(_ENV, "__microverse_lua_build_component_impl", __microverse_lua_build_component_impl)
309
+
310
+ rawset(_ENV, "__microverse_lua_component_apply_incoming", function()
311
+ local incoming = rawget(_ENV, "__microverseIncomingProps")
312
+ if type(incoming) ~= "table" then
313
+ return
314
+ end
315
+ local rp = rawProps()
316
+ local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")
317
+ if type(impl) ~= "table" or type(impl.properties) ~= "table" then
318
+ for k, v in pairs(incoming) do
319
+ rp[k] = v
320
+ end
321
+ return
322
+ end
323
+ for k, v in pairs(incoming) do
324
+ impl.properties[k] = v
325
+ end
326
+ end)
327
+
328
+ rawset(_ENV, "__microverse_lua_component_flush_to_sink", function()
329
+ local push = rawget(_ENV, "__microverseFlushPush")
330
+ local d = dirty()
331
+ for k, v in pairs(d) do
332
+ if type(push) == "function" then
333
+ push(k, v)
334
+ end
335
+ d[k] = nil
336
+ end
337
+ end)
338
+ `.trim();
339
+ /** Lua prelude that registers `TypeName:extend()` singletons for each component type. */
340
+ function profileBridgeSlotKey(typeName, bridgeName) {
341
+ return `__mv_${typeName}_${bridgeName}`;
342
+ }
343
+ function profileBridgeNamesMergeEnvKey(typeName) {
344
+ return `__microverse_profile_bridge_names_${typeName}`;
345
+ }
346
+ function buildComponentTypeBridgeNamesPreludeLua(componentTypes) {
347
+ const lines = [];
348
+ for (const typeName of Object.keys(componentTypes).sort((a, b) => a.localeCompare(b))) {
349
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(typeName)) throw new Error(`unsafe component type name for bridge names prelude: ${typeName}`);
350
+ const entries = componentTypes[typeName].bridgeNames.map((n) => JSON.stringify(n)).join(", ");
351
+ lines.push(`rawset(_ENV, ${JSON.stringify(profileBridgeNamesMergeEnvKey(typeName))}, { ${entries} })`);
352
+ }
353
+ return lines.join("\n");
354
+ }
355
+ function buildComponentTypeSingletonsPreludeLua(typeNames) {
356
+ const lines = [
357
+ "local function __microverse_collect_profile_bridges(typeName)",
358
+ " local names = rawget(_ENV, \"__microverse_profile_bridge_names_\" .. typeName)",
359
+ " local bridges = {}",
360
+ " if type(names) ~= \"table\" then",
361
+ " return bridges",
362
+ " end",
363
+ " for _, name in ipairs(names) do",
364
+ " if type(name) == \"string\" then",
365
+ " local b = rawget(_ENV, \"__mv_\" .. typeName .. \"_\" .. name)",
366
+ " if type(b) == \"table\" then",
367
+ " bridges[name] = b",
368
+ " end",
369
+ " end",
370
+ " end",
371
+ " return bridges",
372
+ "end",
373
+ "local function __microverse_make_type_extend(typeName)",
374
+ " return function(self)",
375
+ " local ext = rawget(_ENV, \"__microverse_lua_extend_component\")",
376
+ " if type(ext) == \"function\" then",
377
+ " ext(typeName)",
378
+ " end",
379
+ " local build = rawget(_ENV, \"__microverse_lua_build_component_impl\")",
380
+ " if type(build) ~= \"function\" then",
381
+ " error(\"component type extend: build impl missing\")",
382
+ " end",
383
+ " return build(__microverse_collect_profile_bridges(typeName))",
384
+ " end",
385
+ "end"
386
+ ];
387
+ for (const name of typeNames) {
388
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`unsafe component type name for Lua singleton: ${name}`);
389
+ lines.push(`rawset(_ENV, ${JSON.stringify(name)}, { extend = __microverse_make_type_extend(${JSON.stringify(name)}) })`);
390
+ }
391
+ return lines.join("\n");
392
+ }
393
+ /** Applies host-selected profile (same as `Type:extend()` without requiring Lua to call it). */
394
+ function buildApplyHostScriptProfileChunkLua(profileName) {
395
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(profileName)) throw new Error(`unsafe script profile name: ${profileName}`);
396
+ return [
397
+ `local T = ${profileName}`,
398
+ "if type(T) == \"table\" and type(T.extend) == \"function\" then",
399
+ " T:extend()",
400
+ "end"
401
+ ].join("\n");
16
402
  }
17
403
  //#endregion
18
404
  //#region src/infrastructure/adapters/zodSchemaValidationAdapter.ts
@@ -159,29 +545,33 @@ function asyncHandleClassName(bridgeName, methodName) {
159
545
  const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
160
546
  return `${cap(bridgeName)}${cap(methodName)}Handle`;
161
547
  }
162
- function buildWorkflowHookManifestFields(kinds, workflowHooks, selfType, fieldDescriptionSuffix) {
548
+ /** LuaCATS class for runtime bridge key `audit` → `Audit` (distinct from field name for LuaLS). */
549
+ function bridgeLuaClassName(bridgeTableName) {
550
+ return bridgeTableName.charAt(0).toUpperCase() + bridgeTableName.slice(1);
551
+ }
552
+ function buildComponentEventManifestFields(kinds, componentClassName, componentHooks) {
163
553
  const out = [];
164
554
  for (const kind of kinds) {
165
- if (!(workflowHooks[kind] instanceof z.ZodObject)) throw new Error(`defineHostSurface workflowHooks: "${kind}" must be a z.object(...)`);
166
- const payloadName = `MicroverseWorkflowEvt_${kind}`;
555
+ if (!(componentHooks[kind] instanceof z.ZodObject)) throw new Error(`defineHostSurface componentHooks: "${kind}" must be a z.object(...)`);
556
+ const payloadName = `MicroverseEvt_${kind}`;
167
557
  const hookName = luaGlobalHookName(kind);
168
558
  out.push({
169
559
  name: hookName,
170
- description: `${fieldDescriptionSuffix} Payload: \`${payloadName}\`.`,
171
- luaType: `fun(self: ${selfType}, evt: ${payloadName})`
560
+ description: `Host invokes when \`${kind}\` is emitted. Payload: \`${payloadName}\`.`,
561
+ luaType: `fun(self: ${componentClassName}, evt: ${payloadName})`
172
562
  });
173
563
  }
174
564
  return out;
175
565
  }
176
- function pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes) {
566
+ function pushComponentEventPayloadClasses(kinds, componentHooks, classes) {
177
567
  for (const kind of kinds) {
178
- const schema = workflowHooks[kind];
179
- if (!(schema instanceof z.ZodObject)) throw new Error(`defineHostSurface workflowHooks: "${kind}" must be a z.object(...)`);
180
- const name = `MicroverseWorkflowEvt_${kind}`;
568
+ const schema = componentHooks[kind];
569
+ if (!(schema instanceof z.ZodObject)) throw new Error(`defineHostSurface componentHooks: "${kind}" must be a z.object(...)`);
570
+ const name = `MicroverseEvt_${kind}`;
181
571
  const shape = schema.shape;
182
572
  classes.push({
183
573
  name,
184
- description: `Workflow hook payload for \`${luaGlobalHookName(kind)}\` (Zod → LuaCATS fields).`,
574
+ description: `Domain event payload for \`${luaGlobalHookName(kind)}\` (Zod → LuaCATS fields).`,
185
575
  fields: Object.keys(shape).map((k) => ({
186
576
  name: k,
187
577
  luaType: zodToLuaTypeRef(shape[k])
@@ -190,9 +580,115 @@ function pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes) {
190
580
  });
191
581
  }
192
582
  }
193
- function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks) {
583
+ function pushBaseComponentClass(classes) {
584
+ classes.push({
585
+ name: "Component",
586
+ description: "Base component lifecycle (extended by typed component profiles).",
587
+ fields: [
588
+ {
589
+ name: "init",
590
+ luaType: "fun(self: Component)",
591
+ description: "Called once after mount and initial props are applied."
592
+ },
593
+ {
594
+ name: "onPropsChanged",
595
+ luaType: "fun(self: Component, key: string, newValue: any)",
596
+ description: "Called when the host patches a property key."
597
+ },
598
+ {
599
+ name: "onDestroy",
600
+ luaType: "fun(self: Component)",
601
+ description: "Called before the script instance slot is disposed."
602
+ }
603
+ ],
604
+ emitSingleton: false
605
+ });
606
+ }
607
+ function pushProfileBridgesClass(typeName, bridgeNames, classes) {
608
+ if (bridgeNames.length === 0) return;
609
+ const bridgesName = scriptProfileBridgesClassName(typeName);
610
+ classes.push({
611
+ name: bridgesName,
612
+ description: `Host bridges for \`${typeName}\` components (\`self.bridges\` after \`${typeName}:extend()\`).`,
613
+ fields: bridgeNames.map((name) => ({
614
+ name,
615
+ luaType: bridgeLuaClassName(name)
616
+ })),
617
+ emitSingleton: false
618
+ });
619
+ }
620
+ function pushComponentTypeManifest(classes, aliases, typeName, profile, componentHooks) {
621
+ const propsAlias = scriptProfilePropsAlias(typeName);
622
+ const stateAlias = scriptProfileStateAlias(typeName);
623
+ aliases.set(propsAlias, zodToLuaTypeRef(profile.props));
624
+ aliases.set(stateAlias, zodToLuaTypeRef(profile.state));
625
+ pushProfileBridgesClass(typeName, profile.bridgeNames, classes);
626
+ const componentClass = scriptProfileComponentClassName(typeName);
627
+ const bridgesClass = profile.bridgeNames.length > 0 ? scriptProfileBridgesClassName(typeName) : "table";
628
+ const extendsClass = profile.extends !== void 0 ? scriptProfileComponentClassName(profile.extends) : "Component";
629
+ const fields = [
630
+ {
631
+ name: "properties",
632
+ luaType: propsAlias,
633
+ description: "Host-synced props (proxy)."
634
+ },
635
+ {
636
+ name: "state",
637
+ luaType: stateAlias,
638
+ description: "Lua-local state."
639
+ },
640
+ {
641
+ name: "bridges",
642
+ luaType: bridgesClass,
643
+ description: "Host bridges allowed for this component type."
644
+ },
645
+ {
646
+ name: "init",
647
+ luaType: `fun(self: ${componentClass})`,
648
+ description: "Called once after mount and initial props are applied."
649
+ },
650
+ {
651
+ name: "onPropsChanged",
652
+ luaType: `fun(self: ${componentClass}, key: string, newValue: any)`,
653
+ description: "Called when the host patches a property key."
654
+ },
655
+ {
656
+ name: "onDestroy",
657
+ luaType: `fun(self: ${componentClass})`,
658
+ description: "Called before the script instance slot is disposed."
659
+ }
660
+ ];
661
+ if (componentHooks !== void 0 && profile.hooks.length > 0) fields.push(...buildComponentEventManifestFields(profile.hooks, componentClass, componentHooks));
662
+ classes.push({
663
+ name: componentClass,
664
+ extendsClass,
665
+ description: `Component instance from \`local C = ${typeName}:extend()\`.`,
666
+ fields,
667
+ emitSingleton: false
668
+ });
669
+ classes.push({
670
+ name: typeName,
671
+ description: `Factory for \`${componentClass}\` in this slot.`,
672
+ methods: [{
673
+ name: "extend",
674
+ description: "Creates the active component with profile-scoped bridges.",
675
+ params: [],
676
+ returns: componentClass
677
+ }],
678
+ emitSingleton: true
679
+ });
680
+ }
681
+ function pushComponentManifestClasses(classes, aliases, componentTypes, componentHooks) {
682
+ const eventKinds = componentHooks !== void 0 ? Object.keys(componentHooks).sort((a, b) => a.localeCompare(b)) : [];
683
+ if (componentHooks !== void 0) pushComponentEventPayloadClasses(eventKinds, componentHooks, classes);
684
+ pushBaseComponentClass(classes);
685
+ const typeNames = Object.keys(componentTypes).sort((a, b) => a.localeCompare(b));
686
+ for (const typeName of typeNames) pushComponentTypeManifest(classes, aliases, typeName, componentTypes[typeName], componentHooks);
687
+ }
688
+ function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, componentHooks, componentTypes) {
194
689
  const classes = [];
195
- for (const bridgeName of Object.keys(spec)) {
690
+ const bridgeNames = Object.keys(spec).sort((a, b) => a.localeCompare(b));
691
+ for (const bridgeName of bridgeNames) {
196
692
  const methods = spec[bridgeName];
197
693
  const manifestMethods = [];
198
694
  for (const methodName of Object.keys(methods)) {
@@ -228,35 +724,16 @@ function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks) {
228
724
  });
229
725
  }
230
726
  classes.push({
231
- name: bridgeName,
232
- methods: manifestMethods
727
+ name: bridgeLuaClassName(bridgeName),
728
+ methods: manifestMethods,
729
+ emitSingleton: false
233
730
  });
234
731
  }
235
732
  const fromLuaType = collectLuaTypeAliasesFromHostSpec(spec);
236
733
  const fromOverrides = inferLuaTypeAliasesFromHostSpec(spec);
237
734
  const merged = new Map([...fromLuaType.map((a) => [a.name, a.definition]), ...fromOverrides.map((a) => [a.name, a.definition])]);
238
735
  if (opts.luaTypeAliases !== void 0) for (const [k, v] of Object.entries(opts.luaTypeAliases)) merged.set(k, v);
239
- if (workflowHooks !== void 0) {
240
- const kinds = Object.keys(workflowHooks).sort((a, b) => a.localeCompare(b));
241
- pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes);
242
- const abstractFields = buildWorkflowHookManifestFields(kinds, workflowHooks, "Workflow", "Host invokes this method on your table (from `workflow:extend()`) when the matching domain event fires.");
243
- classes.push({
244
- name: "Workflow",
245
- description: "Abstract workflow handler type. Call `local w = workflow:extend()` then define `function w:onOrderPlaced(evt) … end` (etc.). Each Lua slot has its own `workflow` helper and handler table.",
246
- fields: abstractFields,
247
- emitSingleton: false
248
- });
249
- classes.push({
250
- name: "workflow",
251
- description: "Per-slot helper injected by the host (not a TS bridge). Creates the active handler table for this sandbox slot.",
252
- methods: [{
253
- name: "extend",
254
- description: "Returns a new handler table with default no-op hooks; registers it for host → Lua dispatch in this slot.",
255
- params: [],
256
- returns: "Workflow"
257
- }]
258
- });
259
- }
736
+ if (componentTypes !== void 0) pushComponentManifestClasses(classes, merged, componentTypes, componentHooks);
260
737
  const aliases = merged.size === 0 ? void 0 : [...merged.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
261
738
  name,
262
739
  definition
@@ -396,111 +873,39 @@ function unwrapInputSchema(schema) {
396
873
  return cur;
397
874
  }
398
875
  //#endregion
399
- //#region src/domain/surfaceCapabilities.ts
400
- /** Runtime list of capability ids declared on a compiled surface spec. */
401
- function collectCapabilitiesFromHostSurfaceSpec(spec) {
402
- const out = [];
403
- const seen = /* @__PURE__ */ new Set();
404
- for (const bridgeName of Object.keys(spec)) {
405
- const methods = spec[bridgeName];
406
- for (const methodName of Object.keys(methods)) {
407
- const id = methods[methodName].capability;
408
- const key = String(id);
409
- if (!seen.has(key)) {
410
- seen.add(key);
411
- out.push(id);
412
- }
413
- }
414
- }
415
- return out;
416
- }
417
- /** Narrows `capabilities` to those declared on the surface (runtime check). */
418
- function pickSurfaceCapabilities(surfaceCapabilities, ...capabilities) {
419
- const allowed = new Set(surfaceCapabilities.map((c) => String(c)));
420
- for (const id of capabilities) if (!allowed.has(String(id))) throw new Error(`capability not declared on host surface: ${String(id)} (surface has: ${[...allowed].join(", ")})`);
421
- return capabilities;
422
- }
423
- //#endregion
424
- //#region src/domain/capabilityRegistrySymbol.ts
425
- /**
426
- * Well-known symbol key used to attach a {@link CapabilityRegistryPort} on the host object
427
- * while surface bridge methods run. Populated by {@link augmentHostWithCapabilityRegistry} or
428
- * internally by {@link HostScriptSession}.
429
- */
430
- var MICROVERSE_CAPABILITY_REGISTRY = Symbol.for("microverse:capabilityRegistry");
431
- //#endregion
432
- //#region src/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.ts
433
- function isThenable(value) {
434
- if (value === null || value === void 0) return false;
435
- if (typeof value !== "object" && typeof value !== "function") return false;
436
- return typeof value.then === "function";
437
- }
438
- /**
439
- * Builds declarative bridge declarations from a host surface spec, using the schema validation port for Lua ↔ host payloads.
440
- */
441
- function createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec) {
442
- const out = [];
443
- for (const bridgeName of Object.keys(spec)) {
444
- const methods = spec[bridgeName];
445
- out.push({
446
- name: bridgeName,
447
- perEntity: true,
448
- createApi: (host, slotKey) => {
449
- const api = {};
450
- for (const methodName of Object.keys(methods)) {
451
- const entry = methods[methodName];
452
- api[methodName] = (...args) => {
453
- const payload = args.length >= 2 ? args[1] : args[0];
454
- const registry = host[MICROVERSE_CAPABILITY_REGISTRY];
455
- const capability = entry.capability;
456
- if (!registry.isAllowed(capability)) throw new Error(`capability denied: ${String(capability)}`);
457
- const parsedIn = schemaValidation.validateWithZodSchema(entry.input, payload);
458
- if (parsedIn._tag === "err") throw new Error(parsedIn.error);
459
- const raw = entry.handler({
460
- host,
461
- slotKey: String(slotKey)
462
- }, parsedIn.value);
463
- if (isThenable(raw)) return raw.then((resolved) => {
464
- const parsedOut = schemaValidation.validateWithZodSchema(entry.output, resolved);
465
- if (parsedOut._tag === "err") throw new Error(parsedOut.error);
466
- return parsedOut.value;
467
- });
468
- const parsedOut = schemaValidation.validateWithZodSchema(entry.output, raw);
469
- if (parsedOut._tag === "err") throw new Error(parsedOut.error);
470
- return parsedOut.value;
471
- };
472
- }
473
- return Object.freeze(api);
474
- }
475
- });
476
- }
477
- return out;
478
- }
479
- //#endregion
480
876
  //#region src/application/useCases/compileHostSurface.ts
481
- function buildHostSurfaceCore(schemaValidation, spec, workflowHooks) {
877
+ function buildHostSurfaceCore(schemaValidation, spec, componentTypeRegistry, componentHooks) {
878
+ validateScriptProfileRegistry(componentTypeRegistry, spec, componentHooks, { requireAtLeastOne: false });
879
+ const componentTypes = buildResolvedScriptProfileRegistry(componentTypeRegistry, spec);
482
880
  const capabilities = collectCapabilitiesFromHostSurfaceSpec(spec);
483
881
  return {
882
+ getHostSurfaceSpec: () => spec,
484
883
  toBridgeDeclarations: () => createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec),
485
- toLuaDefManifest: (opts) => buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks),
884
+ toLuaDefManifest: (opts) => buildLuaDefManifestFromHostSurfaceSpec(spec, opts, componentHooks, componentTypes),
486
885
  capabilities,
886
+ componentTypes,
887
+ getComponentType: (name) => {
888
+ const profile = componentTypes[name];
889
+ if (profile === void 0) throw new Error(`unknown component type: ${name}`);
890
+ return profile;
891
+ },
487
892
  pickCapabilities: (...picked) => pickSurfaceCapabilities(capabilities, ...picked)
488
893
  };
489
894
  }
490
- function compileHostSurface(ports, spec, workflowHooks) {
895
+ function compileHostSurface(ports, spec, componentTypeRegistry, componentHooks) {
491
896
  const [schemaValidation] = ports;
492
- const core = buildHostSurfaceCore(schemaValidation, spec, workflowHooks);
493
- if (workflowHooks === void 0) return core;
897
+ const core = buildHostSurfaceCore(schemaValidation, spec, componentTypeRegistry, componentHooks);
898
+ if (componentHooks === void 0) return core;
494
899
  return {
495
900
  ...core,
496
- workflowHooks
901
+ componentHooks
497
902
  };
498
903
  }
499
904
  /**
500
905
  * Same as {@link compileHostSurface}, but requires every bridge method to be typed with the same `THost`.
501
906
  */
502
- function compileHostSurfaceFor(ports, spec, workflowHooks) {
503
- return workflowHooks === void 0 ? compileHostSurface(ports, spec) : compileHostSurface(ports, spec, workflowHooks);
907
+ function compileHostSurfaceFor(ports, spec, componentTypeRegistry, componentHooks) {
908
+ return componentHooks === void 0 ? compileHostSurface(ports, spec, componentTypeRegistry) : compileHostSurface(ports, spec, componentTypeRegistry, componentHooks);
504
909
  }
505
910
  //#endregion
506
911
  //#region src/domain/inferMethodAsync.ts
@@ -527,35 +932,18 @@ function normalizeMethodDef(def) {
527
932
  };
528
933
  }
529
934
  //#endregion
530
- //#region src/domain/safeObjectKey.ts
531
- /** Keys that must not be used as dynamic property names on ordinary objects. */
532
- var FORBIDDEN_OBJECT_KEYS = new Set([
533
- "__proto__",
534
- "constructor",
535
- "prototype"
536
- ]);
537
- /**
538
- * Rejects bridge/method names that could trigger prototype pollution when used as object keys.
539
- */
540
- function assertSafeObjectKey(kind, name) {
541
- if (FORBIDDEN_OBJECT_KEYS.has(name)) throw new Error(`Invalid surface ${kind} name "${name}": reserved key`);
542
- }
543
- /** Record with no inherited prototype — safe for dynamic string keys at runtime. */
544
- function createNullPrototypeRecord() {
545
- return Object.create(null);
546
- }
547
- //#endregion
548
935
  //#region src/infrastructure/builders/surfaceBuilder.ts
549
936
  /**
550
937
  * Fluent builder for a host surface. Created via {@link defineHostSurfaceFor} / {@link defineHostSurface} factory overloads.
551
938
  */
552
939
  var SurfaceBuilder = class SurfaceBuilder {
553
940
  spec = createNullPrototypeRecord();
554
- workflowHooksSpec;
941
+ componentTypeRegistry = createNullPrototypeRecord();
942
+ componentHooksSpec;
555
943
  ports;
556
- constructor(ports, workflowHooks, initialSpec) {
944
+ constructor(ports, componentHooks, initialSpec, initialComponentTypes) {
557
945
  this.ports = ports;
558
- this.workflowHooksSpec = workflowHooks;
946
+ this.componentHooksSpec = componentHooks;
559
947
  if (initialSpec !== void 0) for (const bridgeName of Object.keys(initialSpec)) {
560
948
  assertSafeObjectKey("bridge", bridgeName);
561
949
  const srcBridge = initialSpec[bridgeName];
@@ -566,19 +954,27 @@ var SurfaceBuilder = class SurfaceBuilder {
566
954
  }
567
955
  this.spec[bridgeName] = bridge;
568
956
  }
957
+ if (initialComponentTypes !== void 0) for (const name of Object.keys(initialComponentTypes)) this.componentTypeRegistry[name] = initialComponentTypes[name];
958
+ }
959
+ /** Declares a typed Lua component profile (props, state, capabilities, hooks). */
960
+ componentType(name, def) {
961
+ assertSafeObjectKey("componentType", name);
962
+ this.componentTypeRegistry[name] = def;
963
+ return this;
569
964
  }
570
965
  /** Opens a Lua bridge table (e.g. `orders`, `greet`). */
571
966
  bridge(name) {
572
967
  return new BridgeBuilder(this, name);
573
968
  }
574
- /** Attaches workflow hook Zod schemas (emitted into `.d.lua` as `on*` methods). */
575
- workflowHooks(hooks) {
576
- return new SurfaceBuilder(this.ports, hooks, this.spec);
969
+ /** Attaches component domain-event Zod schemas (emitted into `.d.lua` as `on*` methods on component types). */
970
+ componentHooks(hooks) {
971
+ return new SurfaceBuilder(this.ports, hooks, this.spec, this.componentTypeRegistry);
577
972
  }
578
973
  /** Compiles the accumulated spec into a {@link HostSurface}. */
579
974
  build() {
580
975
  const spec = this.spec;
581
- return compileHostSurfaceFor(this.ports, spec, this.workflowHooksSpec);
976
+ const registry = this.componentTypeRegistry;
977
+ return compileHostSurfaceFor(this.ports, spec, registry, this.componentHooksSpec);
582
978
  }
583
979
  /** @internal */
584
980
  addMethod(bridgeName, methodName, entry) {
@@ -654,73 +1050,244 @@ function defineHostSurfaceFor() {
654
1050
  return createSurfaceBuilder();
655
1051
  }
656
1052
  //#endregion
657
- //#region src/infrastructure/adapters/augmentHostWithCapabilityRegistry.ts
658
- /**
659
- * Returns a shallow copy of `host` with {@link MICROVERSE_CAPABILITY_REGISTRY} set to `registry`.
660
- * Bridge handlers read the registry to enforce per-session capability allowlists.
661
- *
662
- * @param host - Your engine / service context passed into `buildDeclarativeBridgeTable`.
663
- * @param registry - Typically an {@link InMemoryCapabilityRegistry} from `@microverse.ts/runtime-capabilities`.
664
- */
665
- function augmentHostWithCapabilityRegistry(host, registry) {
666
- return Object.assign(host, { [MICROVERSE_CAPABILITY_REGISTRY]: registry });
1053
+ //#region src/domain/scriptPropertyMergeEnv.ts
1054
+ function scriptPropertyBagToMergeEnv(bag) {
1055
+ const out = {};
1056
+ for (const [k, v] of Object.entries(bag)) out[k] = scriptPropertyValueToPlain(v);
1057
+ return out;
1058
+ }
1059
+ function scriptPropertyValueToPlain(value) {
1060
+ if (value === null || typeof value !== "object") return value;
1061
+ if (Array.isArray(value)) return value.map((item) => scriptPropertyValueToPlain(item));
1062
+ const out = {};
1063
+ for (const [k, v] of Object.entries(value)) out[k] = scriptPropertyValueToPlain(v);
1064
+ return out;
1065
+ }
1066
+ function mergeEnvSinkToScriptPropertyBag(sink) {
1067
+ const out = {};
1068
+ for (const [k, v] of Object.entries(sink)) {
1069
+ const converted = plainToScriptPropertyValue(v);
1070
+ if (converted !== void 0) out[k] = converted;
1071
+ }
1072
+ return out;
1073
+ }
1074
+ function plainToScriptPropertyValue(value) {
1075
+ if (value === null) return null;
1076
+ const t = typeof value;
1077
+ if (t === "string" || t === "boolean") return value;
1078
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1079
+ if (Array.isArray(value)) {
1080
+ const arr = [];
1081
+ for (const item of value) {
1082
+ const c = plainToScriptPropertyValue(item);
1083
+ if (c !== void 0) arr.push(c);
1084
+ }
1085
+ return arr;
1086
+ }
1087
+ if (t === "object" && value !== null) {
1088
+ const obj = {};
1089
+ for (const [k, v] of Object.entries(value)) {
1090
+ const c = plainToScriptPropertyValue(v);
1091
+ if (c !== void 0) obj[k] = c;
1092
+ }
1093
+ return obj;
1094
+ }
1095
+ }
1096
+ //#endregion
1097
+ //#region src/infrastructure/adapters/augmentHostWithScriptContext.ts
1098
+ function augmentHostWithScriptContext(host, script) {
1099
+ return Object.assign(host, { [MICROVERSE_SCRIPT_CONTEXT]: script });
667
1100
  }
668
1101
  //#endregion
669
1102
  //#region src/infrastructure/components/hostScriptSession.ts
670
- /**
671
- * Binds one **Lua slot** to a {@link HostSurface}: capability allowlist, Zod validation, and `mergeEnv` wiring.
672
- *
673
- * @remarks
674
- * - **Lua → host (bridges):** tables/methods from {@link defineHostSurface} on `_ENV` call into TypeScript with Zod validation.
675
- * - **Host → Lua (hooks):** when the surface defines `workflowHooks`, {@link HostScriptSession.openSession} installs
676
- * a small `workflow` helper (`workflow:extend()` → handler table + slot registration) and
677
- * {@link HostScriptSession.invokeGlobalHookIfPresent} dispatches `on…` methods on that table. Each session has its own
678
- * slot env, so many workflows run concurrently without sharing Lua globals. Without `workflowHooks`, hooks are optional
679
- * globals (`onSmoke`, …) invoked as plain functions.
680
- * - Call {@link HostScriptSession.openSession} once before running chunks or invocations; {@link HostScriptSession.dispose} when the slot is torn down.
681
- *
682
- * @typeParam THost - Same host type as your surface handlers.
683
- * @typeParam THooks - Align with {@link HostSurface} `workflowHooks` on the surface you pass in (or `undefined` when the surface has no workflow hooks).
684
- */
1103
+ var defaultSchemaValidation = createZodSchemaValidationPort();
685
1104
  var HostScriptSession = class {
686
1105
  opts;
687
1106
  sandbox;
688
- registry;
1107
+ hostProps = {};
1108
+ schemaValidation;
1109
+ activeComponentType;
1110
+ hostProfileApplied = false;
1111
+ context;
689
1112
  constructor(opts) {
690
1113
  this.opts = opts;
691
- this.registry = new InMemoryCapabilityRegistry(createAllowlist([...opts.allowedCapabilities]));
1114
+ this.schemaValidation = opts.schemaValidation ?? defaultSchemaValidation;
1115
+ this.context = opts.script;
1116
+ if (opts.resolvedProfile !== void 0) this.activeComponentType = opts.resolvedProfile.name;
1117
+ else if (opts.profileId !== void 0) this.activeComponentType = opts.profileId;
692
1118
  }
693
- /**
694
- * Allocates the underlying {@link MicroverseSlot} for this `slotKey` on the shared runtime.
695
- */
696
1119
  openSession = async () => {
697
1120
  this.sandbox = await this.opts.runtime.createMicroverse({ slotKey: this.opts.slotKey });
698
- const hooks = readWorkflowHooks(this.opts.surface);
699
- if (hooks !== void 0) {
700
- const prelude = buildWorkflowStubPreludeLua(hooks);
701
- await this.requireMicroverseSlot().run({
702
- script: createMicroverseScript(prelude),
1121
+ const sb = this.requireMicroverseSlot();
1122
+ if (this.opts.enableComponentRuntime !== false) {
1123
+ await sb.run({
1124
+ script: createMicroverseScript(MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE),
703
1125
  mergeEnv: this.mergeEnv(),
704
1126
  timeout: this.opts.defaultTimeout
705
1127
  });
1128
+ const activeProfile = this.tryGetActiveProfile();
1129
+ const singletonName = this.profileSingletonName();
1130
+ const typeNames = this.singletonTypeNames();
1131
+ if (typeNames.length > 0) {
1132
+ const bridgeRegistry = activeProfile !== void 0 ? { [activeProfile.name]: activeProfile } : this.opts.surface.componentTypes;
1133
+ await sb.run({
1134
+ script: createMicroverseScript([buildComponentTypeBridgeNamesPreludeLua(bridgeRegistry), buildComponentTypeSingletonsPreludeLua(typeNames)].join("\n")),
1135
+ mergeEnv: this.mergeEnv(),
1136
+ timeout: this.opts.defaultTimeout
1137
+ });
1138
+ if (singletonName !== void 0) {
1139
+ await sb.run({
1140
+ script: createMicroverseScript(buildApplyHostScriptProfileChunkLua(singletonName)),
1141
+ mergeEnv: this.mergeEnv(),
1142
+ timeout: this.opts.defaultTimeout
1143
+ });
1144
+ this.hostProfileApplied = true;
1145
+ }
1146
+ }
1147
+ const hooks = readComponentHooks(this.opts.surface);
1148
+ if (hooks !== void 0) {
1149
+ const prelude = buildComponentEventStubPreludeLua(hooks);
1150
+ await sb.run({
1151
+ script: createMicroverseScript(prelude),
1152
+ mergeEnv: this.mergeEnv(),
1153
+ timeout: this.opts.defaultTimeout
1154
+ });
1155
+ }
706
1156
  }
707
1157
  };
708
- /**
709
- * Exposes the in-memory registry (e.g. to mutate allowlists in advanced tests).
710
- */
711
- getCapabilityRegistry = () => this.registry;
712
1158
  requireMicroverseSlot() {
713
1159
  if (this.sandbox === void 0) throw new Error("HostScriptSession: openSession() was not called");
714
1160
  return this.sandbox;
715
1161
  }
716
1162
  mergeEnv() {
717
- return buildBridgeMergeEnvForHost(augmentHostWithCapabilityRegistry(this.opts.host, this.registry), String(this.opts.slotKey), this.opts.surface);
1163
+ const host = augmentHostWithScriptContext(this.opts.host, this.opts.script);
1164
+ const spec = this.opts.surface.getHostSurfaceSpec();
1165
+ const env = { __microverse_lua_extend_component: (typeName) => {
1166
+ if (this.opts.resolvedProfile !== void 0 && typeName === this.profileSingletonName()) {
1167
+ this.activeComponentType = this.opts.resolvedProfile.name;
1168
+ return;
1169
+ }
1170
+ this.opts.surface.getComponentType(typeName);
1171
+ this.activeComponentType = typeName;
1172
+ } };
1173
+ const activeProfile = this.tryGetActiveProfile();
1174
+ const typeNames = activeProfile !== void 0 ? [activeProfile.name] : Object.keys(this.opts.surface.componentTypes);
1175
+ for (const typeName of typeNames) {
1176
+ const profile = activeProfile !== void 0 && typeName === activeProfile.name ? activeProfile : this.opts.surface.getComponentType(typeName);
1177
+ const bridgeTable = buildBridgeMergeEnvForProfile(this.schemaValidation, host, String(this.opts.slotKey), spec, profile.capabilities);
1178
+ for (const bridgeName of profile.bridgeNames) env[profileBridgeSlotKey(typeName, bridgeName)] = bridgeTable[bridgeName];
1179
+ }
1180
+ const profileForRefs = this.tryGetActiveProfile();
1181
+ if (profileForRefs?.references !== void 0 && this.opts.referenceResolver !== void 0) {
1182
+ const profile = profileForRefs;
1183
+ env.__microverse_reference_wrap = (field, raw) => this.wrapReference(field, raw, profile);
1184
+ }
1185
+ return env;
718
1186
  }
719
- /**
720
- * Executes Lua source in the slot environment with surface bridges on `_ENV`.
721
- *
722
- * @param source - Full Lua chunk (compiled with `load(..., "t", env)` in the Wasm adapter).
723
- */
1187
+ wrapReference(field, raw, profile) {
1188
+ const resolver = this.opts.referenceResolver;
1189
+ const refs = profile.references;
1190
+ if (resolver === void 0 || refs === void 0) return raw;
1191
+ const def = refs[field];
1192
+ if (def === void 0) return raw;
1193
+ const rawId = raw === null || raw === void 0 ? null : typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" ? String(raw) : null;
1194
+ return resolver.wrap({
1195
+ slotKey: String(this.opts.slotKey),
1196
+ field,
1197
+ raw: rawId,
1198
+ kind: def.kind,
1199
+ componentType: referenceComponentType(def)
1200
+ });
1201
+ }
1202
+ emitAudit(event) {
1203
+ this.opts.onScriptAudit?.(event);
1204
+ }
1205
+ getActiveProfileName() {
1206
+ return this.activeComponentType ?? this.opts.profileId;
1207
+ }
1208
+ profileSingletonName() {
1209
+ return this.opts.profileSingleton ?? this.opts.profileId ?? this.opts.resolvedProfile?.name;
1210
+ }
1211
+ singletonTypeNames() {
1212
+ const singleton = this.profileSingletonName();
1213
+ const fromSurface = Object.keys(this.opts.surface.componentTypes);
1214
+ const names = new Set(fromSurface);
1215
+ if (singleton !== void 0) names.add(singleton);
1216
+ return [...names].sort((a, b) => a.localeCompare(b));
1217
+ }
1218
+ tryGetActiveProfile() {
1219
+ if (this.opts.resolvedProfile !== void 0) return this.opts.resolvedProfile;
1220
+ const name = this.getActiveProfileName();
1221
+ if (name === void 0) return;
1222
+ try {
1223
+ return this.opts.surface.getComponentType(name);
1224
+ } catch {
1225
+ return;
1226
+ }
1227
+ }
1228
+ validatePropsBag(bag) {
1229
+ const profile = this.tryGetActiveProfile();
1230
+ if (profile === void 0) throw new Error("HostScriptSession: set profileId or resolvedProfile before props or host sync");
1231
+ const parsed = profile.props.parse(bag);
1232
+ assertValidScriptPropertyBag(parsed);
1233
+ return parsed;
1234
+ }
1235
+ getProps = () => ({ ...this.hostProps });
1236
+ setProps = async (bag) => {
1237
+ const next = this.validatePropsBag(bag);
1238
+ const changed = diffScriptProperties(this.hostProps, next);
1239
+ applyScriptPropertyChanges(this.hostProps, next, changed);
1240
+ await this.requireMicroverseSlot().run({
1241
+ script: createMicroverseScript("local f = rawget(_ENV, \"__microverse_lua_component_apply_incoming\")\nif type(f) == \"function\" then f() end"),
1242
+ mergeEnv: {
1243
+ ...this.mergeEnv(),
1244
+ __microverseIncomingProps: scriptPropertyBagToMergeEnv(this.hostProps)
1245
+ },
1246
+ timeout: this.opts.defaultTimeout
1247
+ });
1248
+ if (changed.length > 0) {
1249
+ this.emitAudit({
1250
+ kind: "propsPatched",
1251
+ context: this.opts.script,
1252
+ changedKeys: changed
1253
+ });
1254
+ for (const key of changed) {
1255
+ const value = next[key];
1256
+ if (value !== void 0) await this.invokeComponentHook("onPropsChanged", key, value);
1257
+ }
1258
+ }
1259
+ };
1260
+ patchProps = async (partial) => {
1261
+ await this.setProps({
1262
+ ...this.hostProps,
1263
+ ...partial
1264
+ });
1265
+ };
1266
+ flushDirtyProps = async () => {
1267
+ if (this.opts.enableComponentRuntime === false) return null;
1268
+ const collected = {};
1269
+ await this.requireMicroverseSlot().run({
1270
+ script: createMicroverseScript("local f = rawget(_ENV, \"__microverse_lua_component_flush_to_sink\")\nif type(f) == \"function\" then f() end"),
1271
+ mergeEnv: {
1272
+ ...this.mergeEnv(),
1273
+ __microverseFlushPush: (key, value) => {
1274
+ const converted = plainToScriptPropertyValue(value);
1275
+ if (converted !== void 0) collected[key] = converted;
1276
+ }
1277
+ },
1278
+ timeout: this.opts.defaultTimeout
1279
+ });
1280
+ const keys = Object.keys(collected);
1281
+ if (keys.length === 0) return null;
1282
+ const dirty = collected;
1283
+ for (const key of keys) this.hostProps[key] = dirty[key];
1284
+ this.emitAudit({
1285
+ kind: "propsFlushed",
1286
+ context: this.opts.script,
1287
+ dirtyKeys: keys
1288
+ });
1289
+ return dirty;
1290
+ };
724
1291
  runChunk = async (source) => {
725
1292
  return this.requireMicroverseSlot().run({
726
1293
  script: createMicroverseScript(source),
@@ -728,37 +1295,44 @@ var HostScriptSession = class {
728
1295
  timeout: this.opts.defaultTimeout
729
1296
  });
730
1297
  };
731
- /**
732
- * Host → Lua: when the surface has `workflowHooks`, invokes `impl:onHook(evt)` on the handler table registered by
733
- * `workflow:extend()` in this slot. Otherwise invokes `_ENV[globalName](evt)` when that entry is a function.
734
- *
735
- * @param globalName - Hook method name, e.g. `onOrderPlaced` (same as {@link luaGlobalHookName}).
736
- * @param payload - Table fields: only string, finite number, or boolean (Lua literals).
737
- */
738
- invokeGlobalHookIfPresent = (async (globalName, payload) => {
739
- assertSafeLuaGlobalName(globalName);
1298
+ invokeComponentHook = async (hookName, ...args) => {
1299
+ assertSafeLuaGlobalName(hookName);
1300
+ this.emitAudit({
1301
+ kind: "hookInvoked",
1302
+ context: this.opts.script,
1303
+ hookName
1304
+ });
740
1305
  const sb = this.requireMicroverseSlot();
741
- const tbl = luaTableLiteralFromPlainRecord(payload);
742
- const src = readWorkflowHooks(this.opts.surface) !== void 0 ? buildWorkflowHookInvokeLuaSource(globalName, tbl) : buildGlobalHookInvokeLuaSource(globalName, tbl);
1306
+ const argLiterals = args.map((a) => {
1307
+ if (typeof a === "object" && a !== null && !Array.isArray(a)) return luaTableLiteralFromPlainRecord(a);
1308
+ return luaValueLiteral(a);
1309
+ }).join(", ");
1310
+ const src = [
1311
+ `local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")`,
1312
+ `if type(impl) == "table" then`,
1313
+ ` local m = rawget(impl, ${JSON.stringify(hookName)})`,
1314
+ ` if type(m) == "function" then`,
1315
+ argLiterals.length > 0 ? ` m(impl, ${argLiterals})` : ` m(impl)`,
1316
+ ` end`,
1317
+ `end`
1318
+ ].join("\n");
743
1319
  return sb.run({
744
1320
  script: createMicroverseScript(src),
745
1321
  mergeEnv: this.mergeEnv(),
746
1322
  timeout: this.opts.defaultTimeout
747
1323
  });
1324
+ };
1325
+ invokeComponentEventHook = (async (hookName, payload) => {
1326
+ assertSafeLuaGlobalName(hookName);
1327
+ return this.invokeComponentHook(hookName, payload);
748
1328
  });
749
- /**
750
- * Host → Lua: invokes `_ENV[tableName][methodName](literalTable)` where `literalTable` is built only from
751
- * string, finite number, or boolean fields in `payload`.
752
- *
753
- * @param tableName - Global table name in the slot env.
754
- * @param methodName - Function field on that table.
755
- * @param payload - Plain serializable fields for the Lua table literal.
756
- */
757
1329
  call = async (tableName, methodName, payload) => {
758
1330
  const sb = this.requireMicroverseSlot();
759
1331
  const tbl = luaTableLiteralFromUnknownRecord(payload);
760
1332
  const src = [
761
- `local t = _ENV[${JSON.stringify(tableName)}]`,
1333
+ `local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")`,
1334
+ `local bridges = type(impl) == "table" and impl.bridges or nil`,
1335
+ `local t = type(bridges) == "table" and bridges[${JSON.stringify(tableName)}] or nil`,
762
1336
  `local f = type(t) == "table" and t[${JSON.stringify(methodName)}] or nil`,
763
1337
  `if type(f) == "function" then`,
764
1338
  ` f(${tbl})`,
@@ -770,61 +1344,55 @@ var HostScriptSession = class {
770
1344
  timeout: this.opts.defaultTimeout
771
1345
  });
772
1346
  };
773
- /**
774
- * Releases the slot in the runtime adapter and clears the session handle.
775
- */
776
1347
  dispose = async () => {
777
1348
  if (this.sandbox !== void 0) {
1349
+ if (this.opts.enableComponentRuntime !== false) await this.invokeComponentHook("onDestroy");
778
1350
  await this.sandbox.dispose();
779
1351
  this.sandbox = void 0;
780
1352
  }
781
1353
  };
1354
+ /** Seeds host props bag without Lua sync (call before setProps after mount). */
1355
+ seedHostProps = (bag) => {
1356
+ const cloned = cloneScriptPropertyBag(bag);
1357
+ for (const k of Object.keys(this.hostProps)) delete this.hostProps[k];
1358
+ Object.assign(this.hostProps, cloned);
1359
+ };
1360
+ getHostProfileApplied = () => this.hostProfileApplied;
782
1361
  };
1362
+ function referenceComponentType(def) {
1363
+ if (def.kind === "entityComponentRef" || def.kind === "entityComponentRefArray") return def.componentType;
1364
+ }
783
1365
  function assertSafeLuaGlobalName(name) {
784
1366
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`unsafe Lua global name: ${name}`);
785
1367
  }
786
- function readWorkflowHooks(surface) {
787
- if (!("workflowHooks" in surface)) return;
788
- return surface.workflowHooks;
1368
+ function readComponentHooks(surface) {
1369
+ if (!("componentHooks" in surface)) return;
1370
+ return surface.componentHooks;
789
1371
  }
790
- function buildWorkflowStubPreludeLua(hooks) {
1372
+ function buildComponentEventStubPreludeLua(hooks) {
791
1373
  return [
792
1374
  "local Base = {}",
793
1375
  "for _, name in ipairs({",
794
1376
  ...Object.keys(hooks).sort((a, b) => a.localeCompare(b)).map((kind) => {
795
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(kind)) throw new Error(`unsafe workflow hook kind: ${kind}`);
1377
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(kind)) throw new Error(`unsafe component hook kind: ${kind}`);
796
1378
  return JSON.stringify(luaGlobalHookName(kind));
797
1379
  }).map((h) => ` ${h},`),
798
1380
  "}) do",
799
1381
  " rawset(Base, name, function() end)",
800
1382
  "end",
801
- "rawset(_ENV, \"workflow\", {",
802
- " extend = function(_)",
803
- " local w = setmetatable({}, { __index = Base })",
804
- " rawset(_ENV, \"__microverse_lua_WorkflowImpl\", w)",
805
- " return w",
806
- " end,",
807
- "})"
1383
+ "rawset(_ENV, \"__microverse_component_hook_base\", Base)"
808
1384
  ].join("\n");
809
1385
  }
810
- function buildWorkflowHookInvokeLuaSource(methodName, evtLiteral) {
811
- return [
812
- `local impl = rawget(_ENV, "__microverse_lua_WorkflowImpl")`,
813
- `if type(impl) == "table" then`,
814
- ` local m = rawget(impl, ${JSON.stringify(methodName)})`,
815
- ` if type(m) == "function" then`,
816
- ` m(impl, ${evtLiteral})`,
817
- ` end`,
818
- `end`
819
- ].join("\n");
820
- }
821
- function buildGlobalHookInvokeLuaSource(globalName, evtLiteral) {
822
- return [
823
- `local f = rawget(_ENV, ${JSON.stringify(globalName)})`,
824
- `if type(f) == "function" then`,
825
- ` f(${evtLiteral})`,
826
- `end`
827
- ].join("\n");
1386
+ function luaValueLiteral(value) {
1387
+ if (typeof value === "string") return JSON.stringify(value);
1388
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1389
+ if (typeof value === "boolean") return value ? "true" : "false";
1390
+ if (value === null) return "nil";
1391
+ if (Array.isArray(value)) return `{ ${value.map((v) => luaValueLiteral(v)).join(", ")} }`;
1392
+ const record = value;
1393
+ const parts = [];
1394
+ for (const k of Object.keys(record)) parts.push(`${k} = ${luaValueLiteral(record[k])}`);
1395
+ return `{ ${parts.join(", ")} }`;
828
1396
  }
829
1397
  function luaTableLiteralFromPlainRecord(o) {
830
1398
  const parts = [];
@@ -843,6 +1411,26 @@ function luaTableLiteralFromUnknownRecord(o) {
843
1411
  return `{ ${parts.join(", ")} }`;
844
1412
  }
845
1413
  //#endregion
846
- export { BridgeBuilder, HostScriptSession, MICROVERSE_CAPABILITY_REGISTRY, SurfaceBuilder, augmentHostWithCapabilityRegistry, buildBridgeMergeEnvForHost, collectCapabilitiesFromHostSurfaceSpec, compileHostSurface, compileHostSurfaceFor, createBridgeDeclarationsFromHostSurfaceSpec, defineHostSurface, defineHostSurfaceFor, luaGlobalHookName, luaType, pickSurfaceCapabilities, zodToLuaTypeRef };
1414
+ //#region src/domain/capabilityRegistrySymbol.ts
1415
+ /**
1416
+ * Well-known symbol key used to attach a {@link CapabilityRegistryPort} on the host object
1417
+ * while surface bridge methods run. Populated by {@link augmentHostWithCapabilityRegistry} or
1418
+ * internally by {@link HostScriptSession}.
1419
+ */
1420
+ var MICROVERSE_CAPABILITY_REGISTRY = Symbol.for("microverse:capabilityRegistry");
1421
+ //#endregion
1422
+ //#region src/infrastructure/adapters/augmentHostWithCapabilityRegistry.ts
1423
+ /**
1424
+ * Returns a shallow copy of `host` with {@link MICROVERSE_CAPABILITY_REGISTRY} set to `registry`.
1425
+ * Bridge handlers read the registry to enforce per-session capability allowlists.
1426
+ *
1427
+ * @param host - Your engine / service context passed into `buildDeclarativeBridgeTable`.
1428
+ * @param registry - Typically an {@link InMemoryCapabilityRegistry} from `@microverse.ts/runtime-capabilities`.
1429
+ */
1430
+ function augmentHostWithCapabilityRegistry(host, registry) {
1431
+ return Object.assign(host, { [MICROVERSE_CAPABILITY_REGISTRY]: registry });
1432
+ }
1433
+ //#endregion
1434
+ export { BridgeBuilder, HostScriptSession, MICROVERSE_CAPABILITY_REGISTRY, MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE, MICROVERSE_SCRIPT_CONTEXT, SurfaceBuilder, augmentHostWithCapabilityRegistry, augmentHostWithScriptContext, buildBridgeMergeEnvForProfile, buildComponentTypeSingletonsPreludeLua, buildResolvedScriptProfileRegistry, buildScriptCatalogLuaDefManifest, collectCapabilitiesFromHostSurfaceSpec, compileHostSurface, compileHostSurfaceFor, createBridgeDeclarationsFromHostSurfaceSpec, defineHostSurface, defineHostSurfaceFor, luaGlobalHookName, luaType, mergeEnvSinkToScriptPropertyBag, pickSurfaceCapabilities, resolveScriptProfile, scriptCatalogComponentAlias, scriptProfileBridgesClassName, scriptProfileComponentClassName, scriptProfilePropsAlias, scriptProfileStateAlias, scriptPropertyBagToMergeEnv, scriptPropertyValueToPlain, zodToLuaTypeRef };
847
1435
 
848
1436
  //# sourceMappingURL=index.js.map