@jskit-ai/kernel 0.1.158 → 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 (103) 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.js +12 -3
  7. package/client/vite/clientBootstrapPlugin.test.js +21 -6
  8. package/internal/node/installedPackages.js +2 -7
  9. package/internal/node/installedPackages.test.js +26 -5
  10. package/package.json +3 -5
  11. package/server/actions/actionCatalogue.js +162 -0
  12. package/server/actions/actionEvents.js +66 -0
  13. package/server/actions/actionProvider.js +23 -0
  14. package/server/actions/index.js +3 -6
  15. package/server/features/defineFeature.js +131 -0
  16. package/server/features/defineFeature.test.js +238 -0
  17. package/server/features/index.js +1 -0
  18. package/server/http/HttpProvider.js +23 -0
  19. package/server/http/_testable/index.js +0 -1
  20. package/server/http/capabilityHttpRuntime.js +123 -0
  21. package/server/http/capabilityHttpRuntime.test.js +161 -0
  22. package/server/http/directRequestActionExecutor.js +78 -0
  23. package/server/http/index.js +2 -1
  24. package/server/http/lib/index.js +1 -13
  25. package/server/http/lib/routeRegistration.js +9 -25
  26. package/server/platform/index.js +2 -2
  27. package/server/platform/installedRuntime.js +122 -0
  28. package/server/platform/installedRuntime.test.js +103 -0
  29. package/server/platform/providerRuntime/capabilityProviderLoader.js +100 -0
  30. package/server/platform/providerRuntime/capabilityProviderLoader.test.js +80 -0
  31. package/server/platform/providerRuntime/packageCatalog.js +4 -1
  32. package/server/platform/surfaceRuntime.js +1 -15
  33. package/server/runtime/BootstrapProvider.js +69 -0
  34. package/server/runtime/EventProvider.js +74 -0
  35. package/server/runtime/EventProvider.test.js +32 -0
  36. package/server/runtime/entityChangeEvents.js +7 -7
  37. package/server/runtime/entityChangeEvents.test.js +2 -2
  38. package/server/runtime/fastifyBootstrap.js +0 -40
  39. package/server/runtime/index.js +2 -3
  40. package/server/surface/index.js +0 -1
  41. package/shared/actions/actionDefinitions.js +20 -0
  42. package/shared/actions/pipeline.js +22 -0
  43. package/shared/actions/policies.js +1 -1
  44. package/shared/actions/registry.js +3 -0
  45. package/shared/capabilities/defineProvider.js +99 -0
  46. package/shared/capabilities/index.js +2 -0
  47. package/shared/capabilities/runtime.js +243 -0
  48. package/shared/capabilities/runtime.test.js +170 -0
  49. package/test/barrelExposure.test.js +36 -13
  50. package/test/exportsContract.test.js +8 -3
  51. package/server/actions/ActionRuntimeServiceProvider.js +0 -304
  52. package/server/actions/ActionRuntimeServiceProvider.test.js +0 -558
  53. package/server/container/ContainerCoreServiceProvider.js +0 -27
  54. package/server/container/index.js +0 -10
  55. package/server/http/HttpFastifyServiceProvider.js +0 -25
  56. package/server/http/lib/httpRuntime.js +0 -83
  57. package/server/http/lib/kernel.js +0 -15
  58. package/server/http/lib/kernel.test.js +0 -1565
  59. package/server/http/lib/requestActionExecutor.js +0 -265
  60. package/server/http/lib/requestScope.js +0 -57
  61. package/server/kernel/KernelCoreServiceProvider.js +0 -27
  62. package/server/kernel/index.js +0 -10
  63. package/server/platform/PlatformServerRuntimeServiceProvider.js +0 -30
  64. package/server/platform/providerRuntime/providerLoader.js +0 -283
  65. package/server/platform/providerRuntime.js +0 -146
  66. package/server/platform/providerRuntime.test.js +0 -214
  67. package/server/platform/runtime.js +0 -40
  68. package/server/registries/actionSurfaceSourceRegistry.js +0 -148
  69. package/server/registries/bootstrapPayloadContributorRegistry.js +0 -51
  70. package/server/registries/domainEventListenerRegistry.js +0 -58
  71. package/server/registries/index.js +0 -32
  72. package/server/registries/primitives.js +0 -63
  73. package/server/registries/routeVisibilityResolverRegistry.js +0 -84
  74. package/server/registries/serviceRegistrationRegistry.js +0 -488
  75. package/server/runtime/ServerRuntimeCoreServiceProvider.js +0 -64
  76. package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +0 -53
  77. package/server/runtime/apiRoutePolicyParity.test.js +0 -109
  78. package/server/runtime/apiRouteRegistration.js +0 -65
  79. package/server/runtime/bootBootstrapRoutes.js +0 -29
  80. package/server/runtime/bootBootstrapRoutes.test.js +0 -69
  81. package/server/runtime/bootstrapContributors.test.js +0 -166
  82. package/server/runtime/composition.js +0 -142
  83. package/server/runtime/domainEvents.test.js +0 -114
  84. package/server/runtime/domainRules.js +0 -50
  85. package/server/runtime/domainRules.test.js +0 -87
  86. package/server/runtime/runtimeAssembly.js +0 -113
  87. package/server/runtime/runtimeKernel.js +0 -55
  88. package/server/runtime/serviceRegistration.test.js +0 -251
  89. package/server/support/SupportCoreServiceProvider.js +0 -27
  90. package/server/surface/SurfaceRoutingServiceProvider.js +0 -27
  91. package/shared/actions/index.js +0 -2
  92. package/shared/runtime/application.js +0 -324
  93. package/shared/runtime/container.js +0 -261
  94. package/shared/runtime/containerErrors.js +0 -22
  95. package/shared/runtime/index.js +0 -18
  96. package/shared/runtime/kernelErrors.js +0 -25
  97. package/shared/runtime/kernelErrors.test.js +0 -120
  98. package/shared/runtime/serviceProvider.js +0 -13
  99. package/shared/support/containerToken.js +0 -8
  100. package/shared/support/generatedUiContract.js +0 -542
  101. package/shared/support/generatedUiContract.test.js +0 -218
  102. package/shared/support/tokens.js +0 -1
  103. package/shared/support/tokens.test.js +0 -17
@@ -0,0 +1,103 @@
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 { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
7
+ import { createInstalledRuntime } from "@jskit-ai/kernel/server/platform";
8
+
9
+ test("installed runtime composes explicit package providers and application inputs", async (context) => {
10
+ const appRoot = await mkdtemp(path.join(tmpdir(), "jskit-installed-runtime-"));
11
+ context.after(() => rm(appRoot, { recursive: true, force: true }));
12
+ const packageRoot = path.join(appRoot, "packages", "example");
13
+ await mkdir(path.join(packageRoot, "src"), { recursive: true });
14
+ await writeFile(
15
+ path.join(appRoot, "package.json"),
16
+ JSON.stringify({
17
+ name: "fixture-app",
18
+ private: true,
19
+ dependencies: { "@fixture/example": "file:packages/example" }
20
+ })
21
+ );
22
+ await writeFile(
23
+ path.join(packageRoot, "package.json"),
24
+ JSON.stringify({
25
+ name: "@fixture/example",
26
+ version: "0.1.0",
27
+ type: "module",
28
+ jskit: {
29
+ kind: "runtime",
30
+ capabilities: {
31
+ requires: ["runtime.env"],
32
+ provides: ["feature.example"]
33
+ },
34
+ runtime: {
35
+ server: {
36
+ providers: [{ entrypoint: "src/provider.js", export: "ExampleProvider" }]
37
+ }
38
+ }
39
+ }
40
+ })
41
+ );
42
+ await writeFile(
43
+ path.join(packageRoot, "src", "provider.js"),
44
+ [
45
+ "export const ExampleProvider = {",
46
+ " id: 'feature.example',",
47
+ " requires: { env: 'runtime.env' },",
48
+ " provides: { example: 'feature.example' },",
49
+ " setup({ env }) { return { example: { name: env.APP_NAME } }; }",
50
+ "};"
51
+ ].join("\n")
52
+ );
53
+
54
+ let observed = null;
55
+ const observer = defineProvider({
56
+ id: "fixture.observer",
57
+ requires: {
58
+ actions: "runtime.actions",
59
+ example: "feature.example",
60
+ logger: "runtime.logger"
61
+ },
62
+ setup(dependencies) {
63
+ observed = dependencies;
64
+ }
65
+ });
66
+ const runtime = await createInstalledRuntime({
67
+ appRoot,
68
+ env: { APP_NAME: "Books" },
69
+ logger: { info() {} },
70
+ providers: [observer]
71
+ });
72
+
73
+ assert.equal(observed.example.name, "Books");
74
+ assert.equal(typeof observed.actions.execute, "function");
75
+ assert.equal(typeof observed.logger.info, "function");
76
+ assert.deepEqual(runtime.packageOrder, ["@fixture/example"]);
77
+ assert.deepEqual(runtime.providerPackageOrder, ["@fixture/example"]);
78
+ assert.deepEqual(runtime.diagnostics.providerOrder, [
79
+ "feature.example",
80
+ "runtime.events",
81
+ "runtime.actions",
82
+ "fixture.observer"
83
+ ]);
84
+ assert.equal(Object.hasOwn(runtime, "app"), false);
85
+ });
86
+
87
+ test("installed runtime attaches one idempotent Fastify shutdown hook", async (context) => {
88
+ const appRoot = await mkdtemp(path.join(tmpdir(), "jskit-installed-runtime-close-"));
89
+ context.after(() => rm(appRoot, { recursive: true, force: true }));
90
+ await writeFile(path.join(appRoot, "package.json"), JSON.stringify({ name: "fixture-app", private: true }));
91
+ const hooks = [];
92
+ const fastify = {
93
+ addHook(name, handler) {
94
+ hooks.push({ name, handler });
95
+ }
96
+ };
97
+ const installed = await createInstalledRuntime({ appRoot, fastify });
98
+ const closeHook = hooks.find((entry) => entry.name === "onClose");
99
+ assert.ok(closeHook);
100
+ await closeHook.handler();
101
+ await closeHook.handler();
102
+ assert.equal(installed.runtime.diagnostics().lifecycleState, "stopped");
103
+ });
@@ -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
  });