@jskit-ai/kernel 0.1.159 → 0.1.160

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 (102) hide show
  1. package/_testable/index.js +1 -3
  2. package/client/componentRegistry.js +44 -0
  3. package/client/componentRegistry.test.js +16 -0
  4. package/client/moduleBootstrap.js +54 -97
  5. package/client/moduleBootstrap.test.js +47 -32
  6. package/client/vite/clientBootstrapPlugin.test.js +2 -2
  7. package/internal/node/installedPackages.js +2 -7
  8. package/internal/node/installedPackages.test.js +26 -5
  9. package/package.json +3 -5
  10. package/server/actions/actionCatalogue.js +162 -0
  11. package/server/actions/actionEvents.js +66 -0
  12. package/server/actions/actionProvider.js +23 -0
  13. package/server/actions/index.js +3 -6
  14. package/server/features/defineFeature.js +131 -0
  15. package/server/features/defineFeature.test.js +238 -0
  16. package/server/features/index.js +1 -0
  17. package/server/http/HttpProvider.js +23 -0
  18. package/server/http/_testable/index.js +0 -1
  19. package/server/http/capabilityHttpRuntime.js +123 -0
  20. package/server/http/capabilityHttpRuntime.test.js +161 -0
  21. package/server/http/directRequestActionExecutor.js +78 -0
  22. package/server/http/index.js +2 -1
  23. package/server/http/lib/index.js +1 -13
  24. package/server/http/lib/routeRegistration.js +9 -25
  25. package/server/platform/index.js +2 -2
  26. package/server/platform/installedRuntime.js +122 -0
  27. package/server/platform/installedRuntime.test.js +103 -0
  28. package/server/platform/providerRuntime/capabilityProviderLoader.js +100 -0
  29. package/server/platform/providerRuntime/capabilityProviderLoader.test.js +80 -0
  30. package/server/platform/providerRuntime/packageCatalog.js +4 -1
  31. package/server/platform/surfaceRuntime.js +1 -15
  32. package/server/runtime/BootstrapProvider.js +69 -0
  33. package/server/runtime/EventProvider.js +74 -0
  34. package/server/runtime/EventProvider.test.js +32 -0
  35. package/server/runtime/entityChangeEvents.js +7 -7
  36. package/server/runtime/entityChangeEvents.test.js +2 -2
  37. package/server/runtime/fastifyBootstrap.js +0 -40
  38. package/server/runtime/index.js +2 -3
  39. package/server/surface/index.js +0 -1
  40. package/shared/actions/actionDefinitions.js +20 -0
  41. package/shared/actions/pipeline.js +22 -0
  42. package/shared/actions/policies.js +1 -1
  43. package/shared/actions/registry.js +3 -0
  44. package/shared/capabilities/defineProvider.js +99 -0
  45. package/shared/capabilities/index.js +2 -0
  46. package/shared/capabilities/runtime.js +243 -0
  47. package/shared/capabilities/runtime.test.js +170 -0
  48. package/test/barrelExposure.test.js +36 -13
  49. package/test/exportsContract.test.js +8 -3
  50. package/server/actions/ActionRuntimeServiceProvider.js +0 -304
  51. package/server/actions/ActionRuntimeServiceProvider.test.js +0 -558
  52. package/server/container/ContainerCoreServiceProvider.js +0 -27
  53. package/server/container/index.js +0 -10
  54. package/server/http/HttpFastifyServiceProvider.js +0 -25
  55. package/server/http/lib/httpRuntime.js +0 -83
  56. package/server/http/lib/kernel.js +0 -15
  57. package/server/http/lib/kernel.test.js +0 -1565
  58. package/server/http/lib/requestActionExecutor.js +0 -265
  59. package/server/http/lib/requestScope.js +0 -57
  60. package/server/kernel/KernelCoreServiceProvider.js +0 -27
  61. package/server/kernel/index.js +0 -10
  62. package/server/platform/PlatformServerRuntimeServiceProvider.js +0 -30
  63. package/server/platform/providerRuntime/providerLoader.js +0 -283
  64. package/server/platform/providerRuntime.js +0 -146
  65. package/server/platform/providerRuntime.test.js +0 -214
  66. package/server/platform/runtime.js +0 -40
  67. package/server/registries/actionSurfaceSourceRegistry.js +0 -148
  68. package/server/registries/bootstrapPayloadContributorRegistry.js +0 -51
  69. package/server/registries/domainEventListenerRegistry.js +0 -58
  70. package/server/registries/index.js +0 -32
  71. package/server/registries/primitives.js +0 -63
  72. package/server/registries/routeVisibilityResolverRegistry.js +0 -84
  73. package/server/registries/serviceRegistrationRegistry.js +0 -488
  74. package/server/runtime/ServerRuntimeCoreServiceProvider.js +0 -64
  75. package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +0 -53
  76. package/server/runtime/apiRoutePolicyParity.test.js +0 -109
  77. package/server/runtime/apiRouteRegistration.js +0 -65
  78. package/server/runtime/bootBootstrapRoutes.js +0 -29
  79. package/server/runtime/bootBootstrapRoutes.test.js +0 -69
  80. package/server/runtime/bootstrapContributors.test.js +0 -166
  81. package/server/runtime/composition.js +0 -142
  82. package/server/runtime/domainEvents.test.js +0 -114
  83. package/server/runtime/domainRules.js +0 -50
  84. package/server/runtime/domainRules.test.js +0 -87
  85. package/server/runtime/runtimeAssembly.js +0 -113
  86. package/server/runtime/runtimeKernel.js +0 -55
  87. package/server/runtime/serviceRegistration.test.js +0 -251
  88. package/server/support/SupportCoreServiceProvider.js +0 -27
  89. package/server/surface/SurfaceRoutingServiceProvider.js +0 -27
  90. package/shared/actions/index.js +0 -2
  91. package/shared/runtime/application.js +0 -324
  92. package/shared/runtime/container.js +0 -261
  93. package/shared/runtime/containerErrors.js +0 -22
  94. package/shared/runtime/index.js +0 -18
  95. package/shared/runtime/kernelErrors.js +0 -25
  96. package/shared/runtime/kernelErrors.test.js +0 -120
  97. package/shared/runtime/serviceProvider.js +0 -13
  98. package/shared/support/containerToken.js +0 -8
  99. package/shared/support/generatedUiContract.js +0 -542
  100. package/shared/support/generatedUiContract.test.js +0 -218
  101. package/shared/support/tokens.js +0 -1
  102. package/shared/support/tokens.test.js +0 -17
@@ -0,0 +1,100 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import { defineProvider } from "../../../shared/capabilities/defineProvider.js";
4
+ import { fileExists, isIdentifier, isInsidePackageRoot } from "./helpers.js";
5
+
6
+ function declaredProviderEntrypoints(packageEntry) {
7
+ const runtime = packageEntry?.packageMetadata?.runtime;
8
+ const server = runtime && typeof runtime === "object" ? runtime.server : null;
9
+ const entries = Array.isArray(server?.providers) ? server.providers : [];
10
+
11
+ return entries.map((value) => {
12
+ const entry = value && typeof value === "object" && !Array.isArray(value) ? value : {};
13
+ if (entry.discover != null) {
14
+ throw new Error(
15
+ `Package ${packageEntry.packageId} runtime.server.providers[] must declare an explicit entrypoint and export.`
16
+ );
17
+ }
18
+ const entrypoint = String(entry.entrypoint || "").trim();
19
+ const exportName = String(entry.export || "").trim();
20
+ if (!entrypoint) {
21
+ throw new Error(`Package ${packageEntry.packageId} runtime.server.providers[] entrypoint is required.`);
22
+ }
23
+ if (!isIdentifier(exportName)) {
24
+ throw new Error(
25
+ `Package ${packageEntry.packageId} runtime.server.providers[] export is invalid: ${exportName || "<missing>"}`
26
+ );
27
+ }
28
+ return Object.freeze({ entrypoint, exportName });
29
+ });
30
+ }
31
+
32
+ function normalizeProviderExports(value, packageId, exportName) {
33
+ const queue = Array.isArray(value) ? [...value] : [value];
34
+ const providers = [];
35
+ while (queue.length > 0) {
36
+ const candidate = queue.shift();
37
+ if (Array.isArray(candidate)) {
38
+ queue.unshift(...candidate);
39
+ continue;
40
+ }
41
+ try {
42
+ providers.push(defineProvider(candidate));
43
+ } catch (error) {
44
+ throw new Error(
45
+ `Package ${packageId} provider export "${exportName}" must contain provider definitions: ${error.message}`,
46
+ { cause: error }
47
+ );
48
+ }
49
+ }
50
+ if (providers.length < 1) {
51
+ throw new Error(`Package ${packageId} provider export "${exportName}" is empty.`);
52
+ }
53
+ return providers;
54
+ }
55
+
56
+ async function loadCapabilityProviders({ packageEntry } = {}) {
57
+ const providers = [];
58
+ for (const declaration of declaredProviderEntrypoints(packageEntry)) {
59
+ const modulePath = path.resolve(packageEntry.packageRoot, declaration.entrypoint);
60
+ if (!isInsidePackageRoot(packageEntry.packageRoot, modulePath)) {
61
+ throw new Error(
62
+ `Package ${packageEntry.packageId} provider entrypoint escapes package root: ${declaration.entrypoint}`
63
+ );
64
+ }
65
+ if (!(await fileExists(modulePath))) {
66
+ throw new Error(
67
+ `Package ${packageEntry.packageId} provider entrypoint not found: ${declaration.entrypoint}`
68
+ );
69
+ }
70
+
71
+ const namespace = await import(pathToFileURL(modulePath).href);
72
+ if (!Object.hasOwn(namespace, declaration.exportName)) {
73
+ throw new Error(
74
+ `Package ${packageEntry.packageId} provider export "${declaration.exportName}" was not found.`
75
+ );
76
+ }
77
+ providers.push(
78
+ ...normalizeProviderExports(
79
+ namespace[declaration.exportName],
80
+ packageEntry.packageId,
81
+ declaration.exportName
82
+ )
83
+ );
84
+ }
85
+ return Object.freeze(providers);
86
+ }
87
+
88
+ function appendCapabilityProviders({ providers, sourceId, seenProviderIds, orderedProviders }) {
89
+ for (const provider of providers) {
90
+ if (seenProviderIds.has(provider.id)) {
91
+ throw new Error(
92
+ `Provider id "${provider.id}" is duplicated between ${seenProviderIds.get(provider.id)} and ${sourceId}.`
93
+ );
94
+ }
95
+ seenProviderIds.set(provider.id, sourceId);
96
+ orderedProviders.push(provider);
97
+ }
98
+ }
99
+
100
+ export { appendCapabilityProviders, loadCapabilityProviders };
@@ -0,0 +1,80 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import {
7
+ appendCapabilityProviders,
8
+ loadCapabilityProviders
9
+ } from "./capabilityProviderLoader.js";
10
+
11
+ async function fixturePackage() {
12
+ const packageRoot = await mkdtemp(path.join(tmpdir(), "jskit-capability-provider-"));
13
+ await mkdir(path.join(packageRoot, "src"), { recursive: true });
14
+ return {
15
+ packageRoot,
16
+ packageId: "@fixture/example",
17
+ packageMetadata: {
18
+ runtime: {
19
+ server: {
20
+ providers: [{ entrypoint: "src/provider.js", export: "providers" }]
21
+ }
22
+ }
23
+ }
24
+ };
25
+ }
26
+
27
+ test("capability provider loader imports explicit provider definitions", async () => {
28
+ const packageEntry = await fixturePackage();
29
+ try {
30
+ await writeFile(
31
+ path.join(packageEntry.packageRoot, "src", "provider.js"),
32
+ [
33
+ "export const providers = [{",
34
+ " id: 'feature.example',",
35
+ " provides: { example: 'feature.example' },",
36
+ " setup() { return { example: { ready: true } }; }",
37
+ "}];"
38
+ ].join("\n"),
39
+ "utf8"
40
+ );
41
+
42
+ const providers = await loadCapabilityProviders({ packageEntry });
43
+ assert.deepEqual(providers.map((provider) => provider.id), ["feature.example"]);
44
+ assert.deepEqual(providers[0].provides, { example: "feature.example" });
45
+ } finally {
46
+ await rm(packageEntry.packageRoot, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ test("capability provider loader rejects discovery, classes, path escapes, and duplicates", async () => {
51
+ const packageEntry = await fixturePackage();
52
+ try {
53
+ packageEntry.packageMetadata.runtime.server.providers = [{ discover: { dir: "src" } }];
54
+ await assert.rejects(loadCapabilityProviders({ packageEntry }), /explicit entrypoint and export/u);
55
+
56
+ packageEntry.packageMetadata.runtime.server.providers = [{ entrypoint: "src/provider.js", export: "Provider" }];
57
+ await writeFile(
58
+ path.join(packageEntry.packageRoot, "src", "provider.js"),
59
+ "export class Provider { static id = 'old.provider'; register() {} }\n",
60
+ "utf8"
61
+ );
62
+ await assert.rejects(loadCapabilityProviders({ packageEntry }), /must contain provider definitions/u);
63
+
64
+ packageEntry.packageMetadata.runtime.server.providers = [{ entrypoint: "../outside.js", export: "provider" }];
65
+ await assert.rejects(loadCapabilityProviders({ packageEntry }), /escapes package root/u);
66
+
67
+ const providers = [{ id: "feature.example" }];
68
+ assert.throws(
69
+ () => appendCapabilityProviders({
70
+ providers,
71
+ sourceId: "second",
72
+ seenProviderIds: new Map([["feature.example", "first"]]),
73
+ orderedProviders: []
74
+ }),
75
+ /duplicated between first and second/u
76
+ );
77
+ } finally {
78
+ await rm(packageEntry.packageRoot, { recursive: true, force: true });
79
+ }
80
+ });
@@ -1,7 +1,10 @@
1
1
  import { sortStrings } from "../../../shared/support/sorting.js";
2
2
  import { discoverInstalledPackages } from "../../../internal/node/installedPackages.js";
3
3
 
4
- const EXCLUSIVE_CAPABILITIES = Object.freeze(["auth.provider"]);
4
+ const EXCLUSIVE_CAPABILITIES = Object.freeze([
5
+ "auth.service",
6
+ "runtime.database.driver"
7
+ ]);
5
8
 
6
9
  function normalizeUiRoutePath(pathValue) {
7
10
  const rawPath = String(pathValue || "").trim();
@@ -1,4 +1,3 @@
1
- import { createProviderRuntimeFromApp } from "./providerRuntime.js";
2
1
  import { matchesPathPrefix, normalizePathname } from "../../shared/surface/paths.js";
3
2
 
4
3
  function toRequestPathname(urlValue) {
@@ -129,22 +128,9 @@ function resolveRuntimeProfileFromSurface({
129
128
  return "";
130
129
  }
131
130
 
132
- async function tryCreateProviderRuntimeFromApp(options = {}) {
133
- try {
134
- return await createProviderRuntimeFromApp(options);
135
- } catch (error) {
136
- const message = String(error?.message || "");
137
- if (message.includes("Lock file not found:")) {
138
- return null;
139
- }
140
- throw error;
141
- }
142
- }
143
-
144
131
  export {
145
132
  toRequestPathname,
146
133
  shouldServePathForSurface,
147
134
  registerSurfaceRequestConstraint,
148
- resolveRuntimeProfileFromSurface,
149
- tryCreateProviderRuntimeFromApp
135
+ resolveRuntimeProfileFromSurface
150
136
  };
@@ -0,0 +1,69 @@
1
+ import { defineProvider } from "../../shared/capabilities/defineProvider.js";
2
+ import { isRecord, normalizeText } from "../../shared/support/normalize.js";
3
+ import { AUTH_POLICY_PUBLIC } from "../../shared/support/policies.js";
4
+
5
+ function createBootstrapRuntime() {
6
+ const contributors = [];
7
+ const contributorIds = new Set();
8
+ let sealed = false;
9
+
10
+ function register({ id, order = 0, contribute } = {}) {
11
+ if (sealed) throw new Error("Bootstrap registration is closed after the first payload is resolved.");
12
+ const contributorId = normalizeText(id);
13
+ if (!contributorId) throw new TypeError("Bootstrap contributor id is required.");
14
+ if (typeof contribute !== "function") throw new TypeError(`Bootstrap contributor "${contributorId}" requires contribute().`);
15
+ if (contributorIds.has(contributorId)) throw new Error(`Bootstrap contributor "${contributorId}" is duplicated.`);
16
+ contributorIds.add(contributorId);
17
+ contributors.push(Object.freeze({ id: contributorId, order: Number.isFinite(order) ? Number(order) : 0, contribute }));
18
+ return api;
19
+ }
20
+
21
+ async function resolve(context = {}) {
22
+ sealed = true;
23
+ let payload = {};
24
+ const ordered = contributors
25
+ .map((entry, index) => ({ entry, index }))
26
+ .sort((left, right) => left.entry.order - right.entry.order || left.index - right.index)
27
+ .map(({ entry }) => entry);
28
+ for (const contributor of ordered) {
29
+ const contribution = await contributor.contribute(Object.freeze({
30
+ ...(isRecord(context) ? context : {}),
31
+ payload: Object.freeze({ ...payload })
32
+ }));
33
+ payload = { ...payload, ...(isRecord(contribution) ? contribution : {}) };
34
+ }
35
+ return Object.freeze(payload);
36
+ }
37
+
38
+ function diagnostics() {
39
+ return Object.freeze({
40
+ sealed,
41
+ contributorIds: Object.freeze(contributors.map((entry) => entry.id).sort())
42
+ });
43
+ }
44
+
45
+ const api = Object.freeze({ register, resolve, diagnostics });
46
+ return api;
47
+ }
48
+
49
+ const BootstrapProvider = defineProvider({
50
+ id: "runtime.bootstrap",
51
+ requires: { http: "runtime.http" },
52
+ provides: { bootstrap: "runtime.bootstrap" },
53
+ setup({ http }) {
54
+ const bootstrap = createBootstrapRuntime();
55
+ http.router.register("GET", "/api/bootstrap", {
56
+ auth: AUTH_POLICY_PUBLIC,
57
+ meta: { tags: ["bootstrap"], summary: "Resolve app bootstrap payload" }
58
+ }, async (request, reply) => {
59
+ reply.code(200).send(await bootstrap.resolve({
60
+ request,
61
+ reply,
62
+ query: request.query || {}
63
+ }));
64
+ });
65
+ return { bootstrap };
66
+ }
67
+ });
68
+
69
+ export { BootstrapProvider, createBootstrapRuntime };
@@ -0,0 +1,74 @@
1
+ import { defineProvider } from "../../shared/capabilities/defineProvider.js";
2
+
3
+ function normalizeListener(value) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5
+ throw new TypeError("Event listener must be an object.");
6
+ }
7
+ const id = String(value.id || "").trim();
8
+ if (!id) throw new TypeError("Event listener id is required.");
9
+ if (typeof value.handle !== "function") {
10
+ throw new TypeError(`Event listener "${id}" requires handle().`);
11
+ }
12
+ if (value.matches != null && typeof value.matches !== "function") {
13
+ throw new TypeError(`Event listener "${id}" matches must be a function when provided.`);
14
+ }
15
+ return Object.freeze({
16
+ id,
17
+ handle: value.handle,
18
+ matches: typeof value.matches === "function" ? value.matches : null
19
+ });
20
+ }
21
+
22
+ function normalizeEvent(value) {
23
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
24
+ throw new TypeError("Published event must be an object.");
25
+ }
26
+ const type = String(value.type || "").trim();
27
+ if (!type) throw new TypeError("Published event type is required.");
28
+ return Object.freeze({ ...value, type });
29
+ }
30
+
31
+ function createEventRuntime() {
32
+ const listeners = [];
33
+ const listenerIds = new Set();
34
+
35
+ function register(value) {
36
+ const listener = normalizeListener(value);
37
+ if (listenerIds.has(listener.id)) {
38
+ throw new Error(`Event listener "${listener.id}" is duplicated.`);
39
+ }
40
+ listenerIds.add(listener.id);
41
+ listeners.push(listener);
42
+ return api;
43
+ }
44
+
45
+ async function publish(value) {
46
+ const event = normalizeEvent(value);
47
+ for (const listener of listeners) {
48
+ if (listener.matches && listener.matches(event) !== true) continue;
49
+ await listener.handle(event);
50
+ }
51
+ return event;
52
+ }
53
+
54
+ function diagnostics() {
55
+ return Object.freeze({
56
+ listenerIds: Object.freeze(listeners.map((listener) => listener.id))
57
+ });
58
+ }
59
+
60
+ const api = Object.freeze({ register, publish, diagnostics });
61
+ return api;
62
+ }
63
+
64
+ const EventProvider = defineProvider({
65
+ id: "runtime.events",
66
+ provides: {
67
+ events: "runtime.events"
68
+ },
69
+ setup() {
70
+ return { events: createEventRuntime() };
71
+ }
72
+ });
73
+
74
+ export { EventProvider, createEventRuntime };
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createEventRuntime } from "./EventProvider.js";
4
+
5
+ test("event runtime publishes ephemeral events to explicit listeners in registration order", async () => {
6
+ const events = createEventRuntime();
7
+ const calls = [];
8
+ events.register({
9
+ id: "first",
10
+ handle(event) {
11
+ calls.push(`first:${event.type}`);
12
+ }
13
+ });
14
+ events.register({
15
+ id: "matching",
16
+ matches(event) {
17
+ return event.type === "book.saved";
18
+ },
19
+ handle(event) {
20
+ calls.push(`matching:${event.bookId}`);
21
+ }
22
+ });
23
+
24
+ await events.publish({ type: "book.saved", bookId: "12" });
25
+
26
+ assert.deepEqual(calls, ["first:book.saved", "matching:12"]);
27
+ assert.deepEqual(events.diagnostics().listenerIds, ["first", "matching"]);
28
+ assert.throws(
29
+ () => events.register({ id: "first", handle() {} }),
30
+ /duplicated/u
31
+ );
32
+ });
@@ -1,4 +1,3 @@
1
- import { isContainerToken } from "../../shared/support/containerToken.js";
2
1
  import { normalizeObject, normalizeOpaqueId, normalizeText } from "../../shared/support/normalize.js";
3
2
  import { resolveServiceContext } from "./serviceAuthorization.js";
4
3
 
@@ -203,7 +202,7 @@ function normalizeRealtimePayload(value, { context = "realtime entity change.pay
203
202
  }
204
203
 
205
204
  function createRealtimeEntityChangeMeta({
206
- serviceToken,
205
+ serviceId,
207
206
  methodName,
208
207
  event,
209
208
  change = {}
@@ -213,7 +212,7 @@ function createRealtimeEntityChangeMeta({
213
212
  const meta = {
214
213
  ...baseMeta,
215
214
  service: Object.freeze({
216
- token: serviceToken,
215
+ id: serviceId,
217
216
  method: methodName
218
217
  })
219
218
  };
@@ -256,7 +255,7 @@ function createRealtimeEntityChangePublisher({
256
255
  source,
257
256
  entity,
258
257
  event,
259
- serviceToken,
258
+ serviceId,
260
259
  methodName,
261
260
  scopeResolver = resolveDefaultScope
262
261
  } = {}) {
@@ -265,8 +264,9 @@ function createRealtimeEntityChangePublisher({
265
264
  throw new TypeError("createRealtimeEntityChangePublisher requires event.");
266
265
  }
267
266
 
268
- if (!isContainerToken(serviceToken)) {
269
- throw new TypeError("createRealtimeEntityChangePublisher requires a valid serviceToken.");
267
+ const normalizedServiceId = normalizeText(serviceId);
268
+ if (!normalizedServiceId) {
269
+ throw new TypeError("createRealtimeEntityChangePublisher requires serviceId.");
270
270
  }
271
271
 
272
272
  const normalizedMethodName = normalizeText(methodName);
@@ -284,7 +284,7 @@ function createRealtimeEntityChangePublisher({
284
284
  return async function publishRealtimeEntityChange(operation, entityId, change = {}, options = {}) {
285
285
  const runtimeOptions = resolveRealtimeEntityChangeOptions(change, options);
286
286
  const meta = createRealtimeEntityChangeMeta({
287
- serviceToken,
287
+ serviceId: normalizedServiceId,
288
288
  methodName: normalizedMethodName,
289
289
  event: normalizedEvent,
290
290
  change
@@ -224,7 +224,7 @@ test("realtime entity change publisher adds service, semantic, and realtime meta
224
224
  source: "vibe64",
225
225
  entity: "project",
226
226
  event: "vibe64.project.changed",
227
- serviceToken: "vibe64.terminals.service",
227
+ serviceId: "vibe64.terminals",
228
228
  methodName: "projectRuntime"
229
229
  });
230
230
 
@@ -259,7 +259,7 @@ test("realtime entity change publisher adds service, semantic, and realtime meta
259
259
  id: "workspace_23"
260
260
  });
261
261
  assert.equal(payload?.actorId, "user_17");
262
- assert.equal(payload?.meta?.service?.token, "vibe64.terminals.service");
262
+ assert.equal(payload?.meta?.service?.id, "vibe64.terminals");
263
263
  assert.equal(payload?.meta?.service?.method, "projectRuntime");
264
264
  assert.equal(payload?.meta?.action, "runtime-closed");
265
265
  assert.equal(payload?.meta?.reason, "user-request");
@@ -1,6 +1,5 @@
1
1
  import { ActionRuntimeError } from "../../shared/actions/actionDefinitions.js";
2
2
  import { normalizeOpaqueId } from "../../shared/support/normalize.js";
3
- import { isAppError } from "./errors.js";
4
3
  import { resolveDefaultSurfaceId } from "../support/appConfig.js";
5
4
 
6
5
  const JSON_API_CONTENT_TYPE = "application/vnd.api+json";
@@ -403,44 +402,6 @@ function registerApiErrorHandler(
403
402
  });
404
403
  }
405
404
 
406
- function ensureApiErrorHandling(
407
- app,
408
- {
409
- fastifyToken = "jskit.fastify",
410
- markerToken = "kernel.runtime.apiErrorHandlerRegistered",
411
- isAppError: isAppErrorOverride,
412
- autoRegister = true,
413
- ...handlerOptions
414
- } = {}
415
- ) {
416
- if (!app || typeof app.make !== "function" || typeof app.has !== "function" || typeof app.instance !== "function") {
417
- throw new TypeError("ensureApiErrorHandling requires an application instance.");
418
- }
419
-
420
- if (autoRegister === false) {
421
- return false;
422
- }
423
-
424
- const normalizedMarkerToken = String(markerToken || "").trim() || "kernel.runtime.apiErrorHandlerRegistered";
425
- if (app.has(normalizedMarkerToken)) {
426
- return false;
427
- }
428
-
429
- const fastify = app.make(fastifyToken);
430
- if (!fastify || typeof fastify.setErrorHandler !== "function") {
431
- throw new TypeError("ensureApiErrorHandling requires a Fastify instance.");
432
- }
433
-
434
- const appErrorPredicate = typeof isAppErrorOverride === "function" ? isAppErrorOverride : isAppError;
435
- registerApiErrorHandler(fastify, {
436
- ...handlerOptions,
437
- isAppError: appErrorPredicate
438
- });
439
- app.instance(normalizedMarkerToken, true);
440
-
441
- return true;
442
- }
443
-
444
405
  function resolveDatabaseErrorCode(error) {
445
406
  const errorCode = String(error?.code || "")
446
407
  .trim()
@@ -546,7 +507,6 @@ export {
546
507
  registerBodylessContentTypeNormalizer,
547
508
  registerRequestLoggingHooks,
548
509
  registerApiErrorHandler,
549
- ensureApiErrorHandling,
550
510
  resolveDatabaseErrorCode,
551
511
  recordDbErrorBestEffort,
552
512
  runGracefulShutdown
@@ -1,6 +1,5 @@
1
1
  export { AppError, createValidationError } from "./errors.js";
2
2
  export { parsePositiveInteger } from "./integers.js";
3
3
  export { requireAuth } from "./serviceAuthorization.js";
4
- export { installServiceRegistrationApi, resolveServiceRegistrations } from "../registries/serviceRegistrationRegistry.js";
5
- export { registerDomainEventListener } from "../registries/domainEventListenerRegistry.js";
6
- export { registerBootstrapPayloadContributor } from "../registries/bootstrapPayloadContributorRegistry.js";
4
+ export { BootstrapProvider, createBootstrapRuntime } from "./BootstrapProvider.js";
5
+ export { EventProvider, createEventRuntime } from "./EventProvider.js";
@@ -16,4 +16,3 @@ export {
16
16
  buildVersionedApiPath,
17
17
  isVersionedApiPrefixMatch
18
18
  } from "../../shared/surface/apiPaths.js";
19
- export { SurfaceRoutingServiceProvider } from "./SurfaceRoutingServiceProvider.js";
@@ -242,6 +242,25 @@ function normalizeActionExtensions(value) {
242
242
  });
243
243
  }
244
244
 
245
+ function normalizeActionEvents(value, actionId) {
246
+ if (value == null) return Object.freeze([]);
247
+ if (!Array.isArray(value)) {
248
+ throw createActionRuntimeError(500, `Action definition "${actionId}" events must be an array.`, {
249
+ code: "ACTION_DEFINITION_INVALID"
250
+ });
251
+ }
252
+ return Object.freeze(value.map((builder, index) => {
253
+ if (typeof builder !== "function") {
254
+ throw createActionRuntimeError(
255
+ 500,
256
+ `Action definition "${actionId}" events[${index}] must be a function.`,
257
+ { code: "ACTION_DEFINITION_INVALID" }
258
+ );
259
+ }
260
+ return builder;
261
+ }));
262
+ }
263
+
245
264
  function normalizeActionDefinition(definition, { contributorId = "", contributorDomain = "" } = {}) {
246
265
  const source = definition && typeof definition === "object" ? definition : {};
247
266
 
@@ -312,6 +331,7 @@ function normalizeActionDefinition(definition, { contributorId = "", contributor
312
331
  }),
313
332
  observability: normalizeObservabilityConfig(source.observability),
314
333
  extensions: normalizeActionExtensions(source.extensions),
334
+ events: normalizeActionEvents(source.events, id),
315
335
  execute: source.execute,
316
336
  contributorId: normalizeText(contributorId)
317
337
  });
@@ -14,6 +14,20 @@ import { createNoopAuditAdapter } from "./audit.js";
14
14
  import { createNoopObservabilityAdapter } from "./observability.js";
15
15
  import { normalizeExecutionContext } from "./executionContext.js";
16
16
 
17
+ async function publishActionEvents(events, builders, execution) {
18
+ if (!Array.isArray(builders) || builders.length === 0) return;
19
+ if (!events || typeof events.publish !== "function") {
20
+ throw new TypeError("Action event publishing requires runtime.events.publish().");
21
+ }
22
+ for (const builder of builders) {
23
+ const built = await builder(execution);
24
+ const values = Array.isArray(built) ? built : [built];
25
+ for (const event of values) {
26
+ if (event != null) await events.publish(event);
27
+ }
28
+ }
29
+ }
30
+
17
31
  function normalizeOutcomeErrorCode(error) {
18
32
  return String(error?.code || "ACTION_EXECUTION_FAILED").trim();
19
33
  }
@@ -76,6 +90,7 @@ async function executeActionPipeline({
76
90
  idempotencyAdapter,
77
91
  auditAdapter,
78
92
  observabilityAdapter,
93
+ events,
79
94
  logger = console
80
95
  } = {}) {
81
96
  const normalizedContext = normalizeExecutionContext(context);
@@ -167,6 +182,13 @@ async function executeActionPipeline({
167
182
  const executionResult = await definition.execute(normalizedInput, normalizedContext, deps);
168
183
  const normalizedResult = await normalizeActionOutput(definition, executionResult, normalizedContext);
169
184
 
185
+ await publishActionEvents(events, definition.events, Object.freeze({
186
+ definition,
187
+ input: normalizedInput,
188
+ result: normalizedResult,
189
+ context: normalizedContext
190
+ }));
191
+
170
192
  if (idempotencyClaim && typeof normalizedIdempotencyAdapter.markSucceeded === "function") {
171
193
  await normalizedIdempotencyAdapter.markSucceeded({
172
194
  definition,
@@ -39,7 +39,7 @@ function ensureActionSurfaceAllowed(definition, context) {
39
39
  const surface = normalizeLowerText(context?.surface);
40
40
  const allowedSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
41
41
 
42
- if (!surface || !allowedSurfaces.includes(surface)) {
42
+ if (!allowedSurfaces.includes("*") && (!surface || !allowedSurfaces.includes(surface))) {
43
43
  throw createActionRuntimeError(403, "Forbidden.", {
44
44
  code: "ACTION_SURFACE_FORBIDDEN",
45
45
  details: {
@@ -114,6 +114,7 @@ function createActionRegistry({
114
114
  idempotencyAdapter,
115
115
  auditAdapter,
116
116
  observabilityAdapter,
117
+ events,
117
118
  logger = console
118
119
  } = {}) {
119
120
  const normalizedContributors = normalizeContributors(contributors);
@@ -129,6 +130,7 @@ function createActionRegistry({
129
130
  idempotencyAdapter,
130
131
  auditAdapter,
131
132
  observabilityAdapter,
133
+ events,
132
134
  logger
133
135
  });
134
136
 
@@ -156,6 +158,7 @@ function createActionRegistry({
156
158
  idempotencyAdapter,
157
159
  auditAdapter,
158
160
  observabilityAdapter,
161
+ events,
159
162
  logger
160
163
  });
161
164