@microverse.ts/host-surface 0.1.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/dist/application/ports/SchemaValidationPort.d.ts +9 -0
  4. package/dist/application/ports/SchemaValidationPort.d.ts.map +1 -0
  5. package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts +9 -0
  6. package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts.map +1 -0
  7. package/dist/application/useCases/compileHostSurface.d.ts +13 -0
  8. package/dist/application/useCases/compileHostSurface.d.ts.map +1 -0
  9. package/dist/domain/capabilityRegistrySymbol.d.ts +14 -0
  10. package/dist/domain/capabilityRegistrySymbol.d.ts.map +1 -0
  11. package/dist/domain/hostSurfaceManifest.d.ts +8 -0
  12. package/dist/domain/hostSurfaceManifest.d.ts.map +1 -0
  13. package/dist/domain/hostSurfaceTypes.d.ts +135 -0
  14. package/dist/domain/hostSurfaceTypes.d.ts.map +1 -0
  15. package/dist/domain/inferMethodAsync.d.ts +10 -0
  16. package/dist/domain/inferMethodAsync.d.ts.map +1 -0
  17. package/dist/domain/luaGlobalHook.d.ts +12 -0
  18. package/dist/domain/luaGlobalHook.d.ts.map +1 -0
  19. package/dist/domain/luaTypeAtoms.d.ts +4 -0
  20. package/dist/domain/luaTypeAtoms.d.ts.map +1 -0
  21. package/dist/domain/safeObjectKey.d.ts +7 -0
  22. package/dist/domain/safeObjectKey.d.ts.map +1 -0
  23. package/dist/domain/surfaceCapabilities.d.ts +21 -0
  24. package/dist/domain/surfaceCapabilities.d.ts.map +1 -0
  25. package/dist/domain/surfaceCapabilityString.d.ts +6 -0
  26. package/dist/domain/surfaceCapabilityString.d.ts.map +1 -0
  27. package/dist/domain/surfaceMethodDef.d.ts +25 -0
  28. package/dist/domain/surfaceMethodDef.d.ts.map +1 -0
  29. package/dist/domain/zodLuaType.d.ts +18 -0
  30. package/dist/domain/zodLuaType.d.ts.map +1 -0
  31. package/dist/domain/zodToLuaTypeRef.d.ts +17 -0
  32. package/dist/domain/zodToLuaTypeRef.d.ts.map +1 -0
  33. package/dist/index.d.ts +21 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +848 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts +11 -0
  38. package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts.map +1 -0
  39. package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts +3 -0
  40. package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts.map +1 -0
  41. package/dist/infrastructure/builders/bridgeMergeEnv.d.ts +11 -0
  42. package/dist/infrastructure/builders/bridgeMergeEnv.d.ts.map +1 -0
  43. package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts +41 -0
  44. package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts.map +1 -0
  45. package/dist/infrastructure/builders/surfaceBuilder.d.ts +34 -0
  46. package/dist/infrastructure/builders/surfaceBuilder.d.ts.map +1 -0
  47. package/dist/infrastructure/components/hostScriptSession.d.ts +94 -0
  48. package/dist/infrastructure/components/hostScriptSession.d.ts.map +1 -0
  49. package/package.json +49 -0
package/dist/index.js ADDED
@@ -0,0 +1,848 @@
1
+ import { buildDeclarativeBridgeTable } from "@microverse.ts/runtime-bridge";
2
+ import { validateWithZodSchema } from "@microverse.ts/runtime-zod";
3
+ import { z } from "zod";
4
+ import { InMemoryCapabilityRegistry, createAllowlist, createCapabilityId } from "@microverse.ts/runtime-capabilities";
5
+ import { createMicroverseScript } from "@microverse.ts/runtime-core";
6
+ //#region src/infrastructure/builders/bridgeMergeEnv.ts
7
+ /**
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}).
13
+ */
14
+ function buildBridgeMergeEnvForHost(host, slotKey, surface) {
15
+ return buildDeclarativeBridgeTable(host, slotKey, [...surface.toBridgeDeclarations()]);
16
+ }
17
+ //#endregion
18
+ //#region src/infrastructure/adapters/zodSchemaValidationAdapter.ts
19
+ function createZodSchemaValidationPort() {
20
+ return { validateWithZodSchema };
21
+ }
22
+ //#endregion
23
+ //#region src/domain/luaGlobalHook.ts
24
+ /**
25
+ * Builds the `on{Kind}` method name dispatched on the slot’s workflow handler for a PascalCase event `kind`
26
+ * (e.g. `InventoryLow` → `onInventoryLow`).
27
+ * Throws if `kind` is not a safe Lua identifier fragment.
28
+ */
29
+ function luaGlobalHookName(kind) {
30
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(kind)) throw new Error(`unsafe Lua identifier for event kind: ${kind}`);
31
+ return `on${kind}`;
32
+ }
33
+ //#endregion
34
+ //#region src/domain/luaTypeAtoms.ts
35
+ /** Lua / LuaCATS type atoms: never emit `---@alias` for these names from `lua.paramTypes` / `lua.returns` tokens. */
36
+ var LUA_TYPE_ATOMS = new Set([
37
+ "any",
38
+ "boolean",
39
+ "false",
40
+ "integer",
41
+ "nil",
42
+ "never",
43
+ "number",
44
+ "self",
45
+ "string",
46
+ "table",
47
+ "true",
48
+ "unknown"
49
+ ]);
50
+ function isLuaTypeAtom(name) {
51
+ return LUA_TYPE_ATOMS.has(name);
52
+ }
53
+ //#endregion
54
+ //#region src/domain/zodLuaType.ts
55
+ var aliasBySchema = /* @__PURE__ */ new WeakMap();
56
+ /**
57
+ * Registers a nominal LuaCATS name for a Zod schema. {@link zodToLuaTypeRef} emits the name;
58
+ * `.d.lua` generation adds a matching `---@alias` with the structural shape.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * export const orderDto = luaType('OrderDto', z.object({ id: z.string() }));
63
+ * // bridge: output: orderDto.optional() → returns `OrderDto|nil` in manifest
64
+ * ```
65
+ */
66
+ function luaType(name, schema) {
67
+ if (!/^[A-Za-z_]\w*$/.test(name)) throw new Error(`luaType: invalid alias name: ${name}`);
68
+ aliasBySchema.set(schema, name);
69
+ return schema;
70
+ }
71
+ /** @internal Root schema that carries a {@link luaType} registration (after unwrapping optional/nullable/…). */
72
+ function getLuaTypeRegistrationRoot(schema) {
73
+ let cur = schema;
74
+ for (;;) {
75
+ if (aliasBySchema.get(cur) !== void 0) return cur;
76
+ const next = unwrapOneLayer(cur);
77
+ if (next === void 0) return;
78
+ cur = next;
79
+ }
80
+ }
81
+ /** @internal Nominal LuaCATS name for this schema, if registered via {@link luaType}. */
82
+ function resolveZodLuaTypeAlias(schema) {
83
+ const root = getLuaTypeRegistrationRoot(schema);
84
+ return root === void 0 ? void 0 : aliasBySchema.get(root);
85
+ }
86
+ function getRegisteredLuaTypeName(root) {
87
+ return aliasBySchema.get(root);
88
+ }
89
+ function unwrapOneLayer(schema) {
90
+ if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) return schema.unwrap();
91
+ if (schema instanceof z.ZodDefault) return schema.removeDefault();
92
+ if (schema instanceof z.ZodReadonly) return schema.unwrap();
93
+ if (schema instanceof z.ZodEffects) return schema.innerType();
94
+ if (schema instanceof z.ZodPipeline) return schema._def.in;
95
+ if (schema instanceof z.ZodLazy) return schema.schema;
96
+ if (schema instanceof z.ZodBranded) return schema.unwrap();
97
+ }
98
+ //#endregion
99
+ //#region src/domain/zodToLuaTypeRef.ts
100
+ /**
101
+ * Maps a Zod schema to a LuaCATS type reference string for manifest emission.
102
+ *
103
+ * @remarks
104
+ * Register nominal types with {@link luaType} on shared Zod schemas (e.g. `orderDto`, `orderId`).
105
+ * Prefer that over per-method `lua.paramTypes` / `lua.returns` escape hatches on bridge methods.
106
+ */
107
+ function zodToLuaTypeRef(schema, options) {
108
+ const emitAliasNames = options?.emitAliasNames !== false;
109
+ if (schema instanceof z.ZodOptional) return `${zodToLuaTypeRef(schema.unwrap(), options)}|nil`;
110
+ if (schema instanceof z.ZodNullable) return `${zodToLuaTypeRef(schema.unwrap(), options)}|nil`;
111
+ if (schema instanceof z.ZodDefault) return zodToLuaTypeRef(schema.removeDefault(), options);
112
+ if (schema instanceof z.ZodReadonly) return zodToLuaTypeRef(schema.unwrap(), options);
113
+ if (schema instanceof z.ZodCatch) return zodToLuaTypeRef(schema._def.innerType, options);
114
+ if (schema instanceof z.ZodPipeline) return zodToLuaTypeRef(schema._def.out, options);
115
+ if (schema instanceof z.ZodEffects) return zodToLuaTypeRef(schema.innerType(), options);
116
+ if (schema instanceof z.ZodLazy) return zodToLuaTypeRef(schema.schema, options);
117
+ if (schema instanceof z.ZodBranded) return zodToLuaTypeRef(schema.unwrap(), options);
118
+ if (emitAliasNames) {
119
+ const alias = resolveZodLuaTypeAlias(schema);
120
+ if (alias !== void 0) return alias;
121
+ }
122
+ if (schema instanceof z.ZodString) return "string";
123
+ if (schema instanceof z.ZodNumber) return zodNumberToLua(schema);
124
+ if (schema instanceof z.ZodBoolean) return "boolean";
125
+ if (schema instanceof z.ZodBigInt) return "integer";
126
+ if (schema instanceof z.ZodUndefined) return "nil";
127
+ if (schema instanceof z.ZodNull) return "nil";
128
+ if (schema instanceof z.ZodVoid) return "nil";
129
+ if (schema instanceof z.ZodUnknown || schema instanceof z.ZodAny) return "unknown";
130
+ if (schema instanceof z.ZodLiteral) {
131
+ const v = schema.value;
132
+ if (typeof v === "string") return `"${v}"`;
133
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
134
+ return "unknown";
135
+ }
136
+ if (schema instanceof z.ZodEnum) return schema.options.map((o) => `"${String(o)}"`).join("|");
137
+ if (schema instanceof z.ZodNativeEnum) return "string|number";
138
+ if (schema instanceof z.ZodArray) return "table";
139
+ if (schema instanceof z.ZodRecord) return "table";
140
+ if (schema instanceof z.ZodObject) {
141
+ const shape = schema.shape;
142
+ const keys = Object.keys(shape);
143
+ if (keys.length === 0) return "{}";
144
+ return `{ ${keys.map((k) => `${k}: ${zodToLuaTypeRef(shape[k], options)}`).join("; ")} }`;
145
+ }
146
+ if (schema instanceof z.ZodUnion) return schema.options.map((o) => zodToLuaTypeRef(o, options)).join("|");
147
+ if (schema instanceof z.ZodDiscriminatedUnion) return schema.options.map((o) => zodToLuaTypeRef(o, options)).join("|");
148
+ if (schema instanceof z.ZodIntersection) return "table";
149
+ if (schema instanceof z.ZodTuple) return "table";
150
+ return "unknown";
151
+ }
152
+ function zodNumberToLua(schema) {
153
+ if (schema._def.checks.some((c) => c.kind === "int")) return "integer";
154
+ return "number";
155
+ }
156
+ //#endregion
157
+ //#region src/domain/hostSurfaceManifest.ts
158
+ function asyncHandleClassName(bridgeName, methodName) {
159
+ const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
160
+ return `${cap(bridgeName)}${cap(methodName)}Handle`;
161
+ }
162
+ function buildWorkflowHookManifestFields(kinds, workflowHooks, selfType, fieldDescriptionSuffix) {
163
+ const out = [];
164
+ 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}`;
167
+ const hookName = luaGlobalHookName(kind);
168
+ out.push({
169
+ name: hookName,
170
+ description: `${fieldDescriptionSuffix} Payload: \`${payloadName}\`.`,
171
+ luaType: `fun(self: ${selfType}, evt: ${payloadName})`
172
+ });
173
+ }
174
+ return out;
175
+ }
176
+ function pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes) {
177
+ 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}`;
181
+ const shape = schema.shape;
182
+ classes.push({
183
+ name,
184
+ description: `Workflow hook payload for \`${luaGlobalHookName(kind)}\` (Zod → LuaCATS fields).`,
185
+ fields: Object.keys(shape).map((k) => ({
186
+ name: k,
187
+ luaType: zodToLuaTypeRef(shape[k])
188
+ })),
189
+ emitSingleton: false
190
+ });
191
+ }
192
+ }
193
+ function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks) {
194
+ const classes = [];
195
+ for (const bridgeName of Object.keys(spec)) {
196
+ const methods = spec[bridgeName];
197
+ const manifestMethods = [];
198
+ for (const methodName of Object.keys(methods)) {
199
+ const entry = methods[methodName];
200
+ const resultLua = entry.lua?.returns ?? zodToLuaTypeRef(entry.output);
201
+ if (entry.async === true) {
202
+ const handleName = asyncHandleClassName(bridgeName, methodName);
203
+ const payloadParams = zodInputToManifestParams(entry.input, entry.lua?.paramTypes) ?? [];
204
+ manifestMethods.push({
205
+ name: methodName,
206
+ description: entry.description,
207
+ callStyle: "asyncBridge",
208
+ params: [...payloadParams, {
209
+ name: "onComplete",
210
+ luaType: `fun(result: ${resultLua})|nil`
211
+ }],
212
+ returns: handleName
213
+ });
214
+ classes.push({
215
+ name: handleName,
216
+ description: `Async handle for \`${bridgeName}:${methodName}\`. Call \`:await()\` for the resolved value.`,
217
+ fields: [{
218
+ name: "await",
219
+ luaType: `fun(self: ${handleName}): ${resultLua}`
220
+ }],
221
+ emitSingleton: false
222
+ });
223
+ } else manifestMethods.push({
224
+ name: methodName,
225
+ description: entry.description,
226
+ params: zodInputToManifestParams(entry.input, entry.lua?.paramTypes),
227
+ returns: resultLua
228
+ });
229
+ }
230
+ classes.push({
231
+ name: bridgeName,
232
+ methods: manifestMethods
233
+ });
234
+ }
235
+ const fromLuaType = collectLuaTypeAliasesFromHostSpec(spec);
236
+ const fromOverrides = inferLuaTypeAliasesFromHostSpec(spec);
237
+ const merged = new Map([...fromLuaType.map((a) => [a.name, a.definition]), ...fromOverrides.map((a) => [a.name, a.definition])]);
238
+ 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
+ }
260
+ const aliases = merged.size === 0 ? void 0 : [...merged.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
261
+ name,
262
+ definition
263
+ }));
264
+ return {
265
+ schemaVersion: 1,
266
+ output: opts.output,
267
+ headerNote: opts.headerNote,
268
+ aliases,
269
+ classes
270
+ };
271
+ }
272
+ function nominalTokensFromLuaReturnString(retLua) {
273
+ const out = [];
274
+ const seen = /* @__PURE__ */ new Set();
275
+ for (const segment of retLua.split("|")) {
276
+ const s = segment.trim();
277
+ if (!/^[A-Za-z_]\w*$/.test(s)) continue;
278
+ if (isLuaTypeAtom(s)) continue;
279
+ if (seen.has(s)) continue;
280
+ seen.add(s);
281
+ out.push(s);
282
+ }
283
+ return out;
284
+ }
285
+ function unwrapOutputBaseForAlias(schema) {
286
+ let cur = schema;
287
+ for (;;) {
288
+ if (cur instanceof z.ZodOptional || cur instanceof z.ZodNullable) {
289
+ cur = cur.unwrap();
290
+ continue;
291
+ }
292
+ if (cur instanceof z.ZodDefault) {
293
+ cur = cur.removeDefault();
294
+ continue;
295
+ }
296
+ if (cur instanceof z.ZodReadonly) {
297
+ cur = cur.unwrap();
298
+ continue;
299
+ }
300
+ if (cur instanceof z.ZodEffects) {
301
+ cur = cur.innerType();
302
+ continue;
303
+ }
304
+ if (cur instanceof z.ZodPipeline) {
305
+ cur = cur._def.out;
306
+ continue;
307
+ }
308
+ break;
309
+ }
310
+ return cur;
311
+ }
312
+ function collectLuaTypeAliasesFromHostSpec(spec) {
313
+ const byName = /* @__PURE__ */ new Map();
314
+ const seenRoots = /* @__PURE__ */ new Set();
315
+ const consider = (schema) => {
316
+ const root = getLuaTypeRegistrationRoot(schema);
317
+ if (root === void 0 || seenRoots.has(root)) return;
318
+ const name = getRegisteredLuaTypeName(root);
319
+ if (name === void 0) return;
320
+ seenRoots.add(root);
321
+ const definition = zodToLuaTypeRef(root, { emitAliasNames: false });
322
+ if (definition !== name) byName.set(name, definition);
323
+ };
324
+ const walk = (schema) => {
325
+ consider(schema);
326
+ const base = unwrapInputSchema(schema);
327
+ if (base instanceof z.ZodObject) {
328
+ const shape = base.shape;
329
+ for (const field of Object.values(shape)) consider(field);
330
+ }
331
+ };
332
+ for (const bridgeName of Object.keys(spec)) {
333
+ const methods = spec[bridgeName];
334
+ for (const methodName of Object.keys(methods)) {
335
+ const entry = methods[methodName];
336
+ walk(entry.input);
337
+ walk(entry.output);
338
+ }
339
+ }
340
+ return [...byName.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
341
+ name,
342
+ definition
343
+ }));
344
+ }
345
+ function inferLuaTypeAliasesFromHostSpec(spec) {
346
+ const byName = /* @__PURE__ */ new Map();
347
+ for (const bridgeName of Object.keys(spec)) {
348
+ const methods = spec[bridgeName];
349
+ for (const methodName of Object.keys(methods)) {
350
+ const entry = methods[methodName];
351
+ const luaParams = entry.lua?.paramTypes;
352
+ if (luaParams !== void 0) {
353
+ const baseInput = unwrapInputSchema(entry.input);
354
+ if (baseInput instanceof z.ZodObject) {
355
+ const shape = baseInput.shape;
356
+ for (const [key, L] of Object.entries(luaParams)) {
357
+ if (typeof L !== "string") continue;
358
+ if (!/^[A-Za-z_]\w*$/.test(L) || isLuaTypeAtom(L)) continue;
359
+ const field = shape[key];
360
+ if (field === void 0) continue;
361
+ const def = zodToLuaTypeRef(field);
362
+ if (L !== def) byName.set(L, def);
363
+ }
364
+ }
365
+ }
366
+ const retLua = entry.lua?.returns;
367
+ if (typeof retLua === "string" && retLua.length > 0) for (const T of nominalTokensFromLuaReturnString(retLua)) {
368
+ const def = zodToLuaTypeRef(unwrapOutputBaseForAlias(entry.output));
369
+ if (T !== def) byName.set(T, def);
370
+ }
371
+ }
372
+ }
373
+ return [...byName.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
374
+ name,
375
+ definition
376
+ }));
377
+ }
378
+ function zodInputToManifestParams(input, luaParamTypes) {
379
+ const base = unwrapInputSchema(input);
380
+ if (base instanceof z.ZodObject) {
381
+ const shape = base.shape;
382
+ return Object.keys(shape).map((name) => ({
383
+ name,
384
+ luaType: luaParamTypes?.[name] ?? zodToLuaTypeRef(shape[name])
385
+ }));
386
+ }
387
+ return [{
388
+ name: "value",
389
+ luaType: luaParamTypes?.value ?? zodToLuaTypeRef(base)
390
+ }];
391
+ }
392
+ function unwrapInputSchema(schema) {
393
+ let cur = schema;
394
+ if (cur instanceof z.ZodEffects) cur = cur.innerType();
395
+ if (cur instanceof z.ZodPipeline) cur = cur._def.in;
396
+ return cur;
397
+ }
398
+ //#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
+ //#region src/application/useCases/compileHostSurface.ts
481
+ function buildHostSurfaceCore(schemaValidation, spec, workflowHooks) {
482
+ const capabilities = collectCapabilitiesFromHostSurfaceSpec(spec);
483
+ return {
484
+ toBridgeDeclarations: () => createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec),
485
+ toLuaDefManifest: (opts) => buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks),
486
+ capabilities,
487
+ pickCapabilities: (...picked) => pickSurfaceCapabilities(capabilities, ...picked)
488
+ };
489
+ }
490
+ function compileHostSurface(ports, spec, workflowHooks) {
491
+ const [schemaValidation] = ports;
492
+ const core = buildHostSurfaceCore(schemaValidation, spec, workflowHooks);
493
+ if (workflowHooks === void 0) return core;
494
+ return {
495
+ ...core,
496
+ workflowHooks
497
+ };
498
+ }
499
+ /**
500
+ * Same as {@link compileHostSurface}, but requires every bridge method to be typed with the same `THost`.
501
+ */
502
+ function compileHostSurfaceFor(ports, spec, workflowHooks) {
503
+ return workflowHooks === void 0 ? compileHostSurface(ports, spec) : compileHostSurface(ports, spec, workflowHooks);
504
+ }
505
+ //#endregion
506
+ //#region src/domain/inferMethodAsync.ts
507
+ /** Infers {@link HostSurfaceMethodEntry.async} from handler shape unless overridden explicitly. */
508
+ function inferMethodAsync(def) {
509
+ if (def.async === true) return true;
510
+ if (def.async === false) return false;
511
+ return def.handler.constructor.name === "AsyncFunction";
512
+ }
513
+ //#endregion
514
+ //#region src/domain/surfaceMethodDef.ts
515
+ /**
516
+ * Converts a {@link SurfaceMethodDef} into a compiled {@link HostSurfaceMethodEntry}.
517
+ */
518
+ function normalizeMethodDef(def) {
519
+ return {
520
+ capability: createCapabilityId(def.requires),
521
+ input: def.input,
522
+ output: def.output,
523
+ handler: def.handler,
524
+ async: inferMethodAsync(def),
525
+ ...def.description !== void 0 ? { description: def.description } : {},
526
+ ...def.lua !== void 0 ? { lua: def.lua } : {}
527
+ };
528
+ }
529
+ //#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
+ //#region src/infrastructure/builders/surfaceBuilder.ts
549
+ /**
550
+ * Fluent builder for a host surface. Created via {@link defineHostSurfaceFor} / {@link defineHostSurface} factory overloads.
551
+ */
552
+ var SurfaceBuilder = class SurfaceBuilder {
553
+ spec = createNullPrototypeRecord();
554
+ workflowHooksSpec;
555
+ ports;
556
+ constructor(ports, workflowHooks, initialSpec) {
557
+ this.ports = ports;
558
+ this.workflowHooksSpec = workflowHooks;
559
+ if (initialSpec !== void 0) for (const bridgeName of Object.keys(initialSpec)) {
560
+ assertSafeObjectKey("bridge", bridgeName);
561
+ const srcBridge = initialSpec[bridgeName];
562
+ const bridge = createNullPrototypeRecord();
563
+ for (const methodName of Object.keys(srcBridge)) {
564
+ assertSafeObjectKey("method", methodName);
565
+ bridge[methodName] = srcBridge[methodName];
566
+ }
567
+ this.spec[bridgeName] = bridge;
568
+ }
569
+ }
570
+ /** Opens a Lua bridge table (e.g. `orders`, `greet`). */
571
+ bridge(name) {
572
+ return new BridgeBuilder(this, name);
573
+ }
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);
577
+ }
578
+ /** Compiles the accumulated spec into a {@link HostSurface}. */
579
+ build() {
580
+ const spec = this.spec;
581
+ return compileHostSurfaceFor(this.ports, spec, this.workflowHooksSpec);
582
+ }
583
+ /** @internal */
584
+ addMethod(bridgeName, methodName, entry) {
585
+ assertSafeObjectKey("bridge", bridgeName);
586
+ assertSafeObjectKey("method", methodName);
587
+ let bridge = this.spec[bridgeName];
588
+ if (bridge === void 0) {
589
+ bridge = createNullPrototypeRecord();
590
+ this.spec[bridgeName] = bridge;
591
+ }
592
+ bridge[methodName] = entry;
593
+ return this;
594
+ }
595
+ };
596
+ /**
597
+ * Per-bridge step in the fluent surface DSL. Return to {@link SurfaceBuilder} via `.method(…)`.
598
+ */
599
+ var BridgeBuilder = class {
600
+ parent;
601
+ bridgeName;
602
+ constructor(parent, bridgeName) {
603
+ this.parent = parent;
604
+ this.bridgeName = bridgeName;
605
+ }
606
+ /** Registers one method on the current bridge and returns the root builder for chaining. */
607
+ method(name, def) {
608
+ return this.parent.addMethod(this.bridgeName, name, normalizeMethodDef(def));
609
+ }
610
+ };
611
+ //#endregion
612
+ //#region src/infrastructure/builders/defineHostSurfaceFacade.ts
613
+ var defaultPorts = [createZodSchemaValidationPort()];
614
+ function createSurfaceBuilder() {
615
+ return new SurfaceBuilder(defaultPorts);
616
+ }
617
+ /**
618
+ * Declares a **host surface** via the fluent builder (`bridge` → `method` → `build`).
619
+ *
620
+ * @example
621
+ * ```ts
622
+ * const surface = defineHostSurface()
623
+ * .bridge('time')
624
+ * .method('delta', {
625
+ * requires: 'engine:time',
626
+ * input: z.object({}),
627
+ * output: z.number(),
628
+ * handler: ({ host }) => host.clock.dt,
629
+ * })
630
+ * .build();
631
+ * ```
632
+ */
633
+ function defineHostSurface() {
634
+ return createSurfaceBuilder();
635
+ }
636
+ /**
637
+ * Same as {@link defineHostSurface}, but every `handler` is typed against a single `THost`
638
+ * (the engine context passed to {@link HostScriptSession} / {@link MicroverseLua.create}).
639
+ *
640
+ * @example
641
+ * ```ts
642
+ * const surface = defineHostSurfaceFor<MyHost>()
643
+ * .bridge('greet')
644
+ * .method('hello', {
645
+ * requires: 'demo:greet',
646
+ * input: z.object({ name: z.string() }),
647
+ * output: z.string(),
648
+ * handler: ({ host }, { name }) => `Hello, ${name}`,
649
+ * })
650
+ * .build();
651
+ * ```
652
+ */
653
+ function defineHostSurfaceFor() {
654
+ return createSurfaceBuilder();
655
+ }
656
+ //#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 });
667
+ }
668
+ //#endregion
669
+ //#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
+ */
685
+ var HostScriptSession = class {
686
+ opts;
687
+ sandbox;
688
+ registry;
689
+ constructor(opts) {
690
+ this.opts = opts;
691
+ this.registry = new InMemoryCapabilityRegistry(createAllowlist([...opts.allowedCapabilities]));
692
+ }
693
+ /**
694
+ * Allocates the underlying {@link MicroverseSlot} for this `slotKey` on the shared runtime.
695
+ */
696
+ openSession = async () => {
697
+ 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),
703
+ mergeEnv: this.mergeEnv(),
704
+ timeout: this.opts.defaultTimeout
705
+ });
706
+ }
707
+ };
708
+ /**
709
+ * Exposes the in-memory registry (e.g. to mutate allowlists in advanced tests).
710
+ */
711
+ getCapabilityRegistry = () => this.registry;
712
+ requireMicroverseSlot() {
713
+ if (this.sandbox === void 0) throw new Error("HostScriptSession: openSession() was not called");
714
+ return this.sandbox;
715
+ }
716
+ mergeEnv() {
717
+ return buildBridgeMergeEnvForHost(augmentHostWithCapabilityRegistry(this.opts.host, this.registry), String(this.opts.slotKey), this.opts.surface);
718
+ }
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
+ */
724
+ runChunk = async (source) => {
725
+ return this.requireMicroverseSlot().run({
726
+ script: createMicroverseScript(source),
727
+ mergeEnv: this.mergeEnv(),
728
+ timeout: this.opts.defaultTimeout
729
+ });
730
+ };
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);
740
+ const sb = this.requireMicroverseSlot();
741
+ const tbl = luaTableLiteralFromPlainRecord(payload);
742
+ const src = readWorkflowHooks(this.opts.surface) !== void 0 ? buildWorkflowHookInvokeLuaSource(globalName, tbl) : buildGlobalHookInvokeLuaSource(globalName, tbl);
743
+ return sb.run({
744
+ script: createMicroverseScript(src),
745
+ mergeEnv: this.mergeEnv(),
746
+ timeout: this.opts.defaultTimeout
747
+ });
748
+ });
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
+ call = async (tableName, methodName, payload) => {
758
+ const sb = this.requireMicroverseSlot();
759
+ const tbl = luaTableLiteralFromUnknownRecord(payload);
760
+ const src = [
761
+ `local t = _ENV[${JSON.stringify(tableName)}]`,
762
+ `local f = type(t) == "table" and t[${JSON.stringify(methodName)}] or nil`,
763
+ `if type(f) == "function" then`,
764
+ ` f(${tbl})`,
765
+ `end`
766
+ ].join("\n");
767
+ return sb.run({
768
+ script: createMicroverseScript(src),
769
+ mergeEnv: this.mergeEnv(),
770
+ timeout: this.opts.defaultTimeout
771
+ });
772
+ };
773
+ /**
774
+ * Releases the slot in the runtime adapter and clears the session handle.
775
+ */
776
+ dispose = async () => {
777
+ if (this.sandbox !== void 0) {
778
+ await this.sandbox.dispose();
779
+ this.sandbox = void 0;
780
+ }
781
+ };
782
+ };
783
+ function assertSafeLuaGlobalName(name) {
784
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`unsafe Lua global name: ${name}`);
785
+ }
786
+ function readWorkflowHooks(surface) {
787
+ if (!("workflowHooks" in surface)) return;
788
+ return surface.workflowHooks;
789
+ }
790
+ function buildWorkflowStubPreludeLua(hooks) {
791
+ return [
792
+ "local Base = {}",
793
+ "for _, name in ipairs({",
794
+ ...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}`);
796
+ return JSON.stringify(luaGlobalHookName(kind));
797
+ }).map((h) => ` ${h},`),
798
+ "}) do",
799
+ " rawset(Base, name, function() end)",
800
+ "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
+ "})"
808
+ ].join("\n");
809
+ }
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");
828
+ }
829
+ function luaTableLiteralFromPlainRecord(o) {
830
+ const parts = [];
831
+ for (const [k, v] of Object.entries(o)) if (typeof v === "string") parts.push(`${k} = ${JSON.stringify(v)}`);
832
+ else if (typeof v === "number" && Number.isFinite(v)) parts.push(`${k} = ${v}`);
833
+ else if (typeof v === "boolean") parts.push(`${k} = ${v ? "true" : "false"}`);
834
+ else throw new Error(`HostScriptSession: unsupported value type for key ${k}`);
835
+ return `{ ${parts.join(", ")} }`;
836
+ }
837
+ function luaTableLiteralFromUnknownRecord(o) {
838
+ const parts = [];
839
+ for (const [k, v] of Object.entries(o)) if (typeof v === "string") parts.push(`${k} = ${JSON.stringify(v)}`);
840
+ else if (typeof v === "number" && Number.isFinite(v)) parts.push(`${k} = ${v}`);
841
+ else if (typeof v === "boolean") parts.push(`${k} = ${v ? "true" : "false"}`);
842
+ else throw new Error(`HostScriptSession.call: unsupported value type for key ${k}`);
843
+ return `{ ${parts.join(", ")} }`;
844
+ }
845
+ //#endregion
846
+ export { BridgeBuilder, HostScriptSession, MICROVERSE_CAPABILITY_REGISTRY, SurfaceBuilder, augmentHostWithCapabilityRegistry, buildBridgeMergeEnvForHost, collectCapabilitiesFromHostSurfaceSpec, compileHostSurface, compileHostSurfaceFor, createBridgeDeclarationsFromHostSurfaceSpec, defineHostSurface, defineHostSurfaceFor, luaGlobalHookName, luaType, pickSurfaceCapabilities, zodToLuaTypeRef };
847
+
848
+ //# sourceMappingURL=index.js.map