@jskit-ai/kernel 0.1.159 → 0.1.161

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,123 @@
1
+ import {
2
+ normalizeRouteVisibility,
3
+ normalizeRouteVisibilityToken,
4
+ normalizeVisibilityContext
5
+ } from "../../shared/support/visibility.js";
6
+ import { normalizeText } from "../../shared/support/normalize.js";
7
+ import { createRouter } from "./lib/router.js";
8
+ import { registerRoutes } from "./lib/routeRegistration.js";
9
+ import {
10
+ registerApiErrorHandler,
11
+ registerBodylessContentTypeNormalizer
12
+ } from "../runtime/fastifyBootstrap.js";
13
+ import { isAppError } from "../runtime/errors.js";
14
+
15
+ function normalizeResolver({ id, resolve } = {}) {
16
+ const resolverId = normalizeText(id);
17
+ if (!resolverId) {
18
+ throw new TypeError("HTTP visibility resolver id is required.");
19
+ }
20
+ if (typeof resolve !== "function") {
21
+ throw new TypeError(`HTTP visibility resolver "${resolverId}" requires resolve().`);
22
+ }
23
+ return Object.freeze({ id: resolverId, resolve });
24
+ }
25
+
26
+ function createCapabilityHttpRuntime({ fastify, actions }) {
27
+ if (!fastify || typeof fastify.route !== "function") {
28
+ throw new TypeError("HTTP runtime requires a Fastify instance.");
29
+ }
30
+ if (!actions || typeof actions.registerContextContributor !== "function") {
31
+ throw new TypeError("HTTP runtime requires the runtime.actions capability.");
32
+ }
33
+
34
+ const router = createRouter();
35
+ const visibilityResolvers = [];
36
+ const visibilityResolverIds = new Set();
37
+ let started = false;
38
+ let registration = null;
39
+
40
+ function assertOpen() {
41
+ if (started) {
42
+ throw new Error("HTTP runtime registration is closed after startup.");
43
+ }
44
+ }
45
+
46
+ function registerVisibilityResolver(value) {
47
+ assertOpen();
48
+ const resolver = normalizeResolver(value);
49
+ if (visibilityResolverIds.has(resolver.id)) {
50
+ throw new Error(`HTTP visibility resolver "${resolver.id}" is duplicated.`);
51
+ }
52
+ visibilityResolverIds.add(resolver.id);
53
+ visibilityResolvers.push(resolver);
54
+ return api;
55
+ }
56
+
57
+ actions.registerContextContributor({
58
+ id: "runtime.http.visibility",
59
+ async contribute({ actionId, version, definition, input, context }) {
60
+ const visibility = normalizeRouteVisibilityToken(context.routeVisibility);
61
+ const patch = {};
62
+ for (const resolver of visibilityResolvers) {
63
+ const contribution = await resolver.resolve(Object.freeze({
64
+ request: context.requestMeta?.request || null,
65
+ visibility,
66
+ context,
67
+ input,
68
+ actionId,
69
+ version,
70
+ definition,
71
+ channel: normalizeText(context.channel).toLowerCase() || "api"
72
+ }));
73
+ if (contribution && typeof contribution === "object" && !Array.isArray(contribution)) {
74
+ Object.assign(patch, contribution);
75
+ }
76
+ }
77
+ const coreVisibility = normalizeRouteVisibility(visibility);
78
+ const visibilityContext = normalizeVisibilityContext({
79
+ ...patch,
80
+ visibility,
81
+ requiresActorScope: patch.requiresActorScope === true || coreVisibility === "user"
82
+ });
83
+ return {
84
+ visibilityContext,
85
+ requestMeta: {
86
+ visibilityContext,
87
+ routeVisibility: visibilityContext.visibility
88
+ }
89
+ };
90
+ }
91
+ });
92
+
93
+ function start(options = {}) {
94
+ assertOpen();
95
+ started = true;
96
+ registerBodylessContentTypeNormalizer(fastify);
97
+ registerApiErrorHandler(fastify, { isAppError });
98
+ registration = Object.freeze(registerRoutes(fastify, {
99
+ ...options,
100
+ actions,
101
+ routes: router.list()
102
+ }));
103
+ return registration;
104
+ }
105
+
106
+ function diagnostics() {
107
+ return Object.freeze({
108
+ started,
109
+ routeCount: registration?.routeCount || router.list().filter((route) => route.internal !== true).length,
110
+ visibilityResolverIds: Object.freeze([...visibilityResolverIds].sort())
111
+ });
112
+ }
113
+
114
+ const api = Object.freeze({
115
+ router,
116
+ registerVisibilityResolver,
117
+ start,
118
+ diagnostics
119
+ });
120
+ return api;
121
+ }
122
+
123
+ export { createCapabilityHttpRuntime };
@@ -0,0 +1,161 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createSchema } from "json-rest-schema";
4
+
5
+ import { createActionProvider } from "../actions/actionProvider.js";
6
+ import { createCapabilityRuntime } from "../../shared/capabilities/runtime.js";
7
+ import { defineFeature } from "../features/defineFeature.js";
8
+ import { HttpProvider } from "./HttpProvider.js";
9
+
10
+ function createFastifyStub() {
11
+ return {
12
+ routes: [],
13
+ hooks: {},
14
+ contentTypeParsers: new Map(),
15
+ log: { error() {} },
16
+ route(value) {
17
+ this.routes.push(value);
18
+ },
19
+ addHook(name, handler) {
20
+ this.hooks[name] = handler;
21
+ },
22
+ setErrorHandler(handler) {
23
+ this.errorHandler = handler;
24
+ },
25
+ hasContentTypeParser(type) {
26
+ return this.contentTypeParsers.has(type);
27
+ },
28
+ getDefaultJsonParser() {
29
+ return (_request, body, done) => done(null, body);
30
+ },
31
+ addContentTypeParser(type, options, parser) {
32
+ this.contentTypeParsers.set(type, { options, parser });
33
+ }
34
+ };
35
+ }
36
+
37
+ function createReply() {
38
+ return {
39
+ statusCode: 200,
40
+ headers: {},
41
+ code(value) {
42
+ this.statusCode = value;
43
+ return this;
44
+ },
45
+ header(name, value) {
46
+ this.headers[name.toLowerCase()] = value;
47
+ return this;
48
+ },
49
+ hasHeader(name) {
50
+ return Object.hasOwn(this.headers, name.toLowerCase());
51
+ },
52
+ send(value) {
53
+ this.payload = value;
54
+ return this;
55
+ }
56
+ };
57
+ }
58
+
59
+ test("HttpProvider registers routes that invoke actions directly", async () => {
60
+ const fastify = createFastifyStub();
61
+ const feature = defineFeature({
62
+ id: "feature.http-proof",
63
+ domain: "http-proof",
64
+ requires: { http: "runtime.http" },
65
+ setup({ http }) {
66
+ http.router.get("/proof", {
67
+ surface: "app",
68
+ visibility: "public"
69
+ }, async (request, reply) => {
70
+ const result = await request.executeAction({ actionId: "http-proof.read" });
71
+ reply.code(200).send(result);
72
+ });
73
+ return {};
74
+ },
75
+ actions: [{
76
+ id: "http-proof.read",
77
+ kind: "query",
78
+ channels: ["api"],
79
+ surfaces: ["app"],
80
+ input: { schema: createSchema({}) },
81
+ idempotency: "none",
82
+ async execute(_input, context) {
83
+ return {
84
+ channel: context.channel,
85
+ surface: context.surface,
86
+ visibility: context.visibilityContext.visibility,
87
+ hasRequest: Boolean(context.requestMeta.request)
88
+ };
89
+ }
90
+ }]
91
+ });
92
+ const runtime = createCapabilityRuntime({
93
+ inputs: { "runtime.fastify": fastify },
94
+ providers: [createActionProvider(), HttpProvider, feature]
95
+ });
96
+
97
+ await runtime.start();
98
+ assert.equal(fastify.routes.length, 1);
99
+ assert.equal(fastify.routes[0].url, "/proof");
100
+
101
+ const request = { id: "req-1", routeOptions: { config: fastify.routes[0].config } };
102
+ const reply = createReply();
103
+ await fastify.routes[0].handler(request, reply);
104
+
105
+ assert.equal(reply.statusCode, 200);
106
+ assert.deepEqual(reply.payload, {
107
+ channel: "api",
108
+ surface: "app",
109
+ visibility: "public",
110
+ hasRequest: true
111
+ });
112
+ assert.equal(Object.hasOwn(request, "scope"), false);
113
+ await runtime.shutdown();
114
+ });
115
+
116
+ test("direct HTTP action execution preserves request-scoped stream dependencies", async () => {
117
+ const fastify = createFastifyStub();
118
+ const streamWriter = Object.freeze({ id: "writer-1" });
119
+ const abortSignal = Object.freeze({ aborted: false });
120
+ const feature = defineFeature({
121
+ id: "feature.stream-proof",
122
+ domain: "stream-proof",
123
+ requires: { http: "runtime.http" },
124
+ setup({ http }) {
125
+ http.router.post("/stream-proof", { surface: "app", visibility: "public" }, async (request, reply) => {
126
+ const result = await request.executeAction({
127
+ actionId: "stream-proof.run",
128
+ deps: { streamWriter, abortSignal }
129
+ });
130
+ reply.code(200).send(result);
131
+ });
132
+ return {};
133
+ },
134
+ actions: [{
135
+ id: "stream-proof.run",
136
+ kind: "stream",
137
+ channels: ["api"],
138
+ surfaces: ["app"],
139
+ input: { schema: createSchema({}) },
140
+ idempotency: "none",
141
+ async execute(_input, _context, deps) {
142
+ return {
143
+ writerId: deps.streamWriter.id,
144
+ aborted: deps.abortSignal.aborted
145
+ };
146
+ }
147
+ }]
148
+ });
149
+ const runtime = createCapabilityRuntime({
150
+ inputs: { "runtime.fastify": fastify },
151
+ providers: [createActionProvider(), HttpProvider, feature]
152
+ });
153
+
154
+ await runtime.start();
155
+ const request = { routeOptions: { config: fastify.routes[0].config } };
156
+ const reply = createReply();
157
+ await fastify.routes[0].handler(request, reply);
158
+
159
+ assert.deepEqual(reply.payload, { writerId: "writer-1", aborted: false });
160
+ await runtime.shutdown();
161
+ });
@@ -0,0 +1,78 @@
1
+ import { normalizeObject, normalizeText } from "../../shared/support/normalize.js";
2
+ import { normalizeRouteVisibilityToken } from "../../shared/support/visibility.js";
3
+ import { resolveDefaultSurfaceId } from "../support/appConfig.js";
4
+ import { RouteRegistrationError } from "./lib/errors.js";
5
+
6
+ function resolveSurface(request, explicitSurface = "", defaultSurfaceId = "") {
7
+ return (
8
+ normalizeText(explicitSurface).toLowerCase() ||
9
+ normalizeText(request?.routeOptions?.config?.surface).toLowerCase() ||
10
+ normalizeText(request?.surface).toLowerCase() ||
11
+ resolveDefaultSurfaceId(null, { defaultSurfaceId })
12
+ );
13
+ }
14
+
15
+ function routeVisibility(request, payload = {}) {
16
+ const routeConfig = normalizeObject(request?.routeOptions?.config);
17
+ return normalizeRouteVisibilityToken(
18
+ Object.hasOwn(routeConfig, "visibility")
19
+ ? routeConfig.visibility
20
+ : payload.visibility
21
+ );
22
+ }
23
+
24
+ function attachDirectRequestActionExecutor({
25
+ actions,
26
+ request,
27
+ property = "executeAction",
28
+ defaultChannel = "api",
29
+ defaultSurfaceId = ""
30
+ } = {}) {
31
+ if (!actions || typeof actions.execute !== "function") {
32
+ throw new RouteRegistrationError("Direct request action execution requires the runtime.actions capability.");
33
+ }
34
+ if (!request || typeof request !== "object") {
35
+ return null;
36
+ }
37
+ const propertyName = normalizeText(property) || "executeAction";
38
+ if (typeof request[propertyName] === "function") {
39
+ return request[propertyName];
40
+ }
41
+
42
+ const executeAction = async (payload = {}) => {
43
+ const source = normalizeObject(payload);
44
+ const channel = normalizeText(source.channel || defaultChannel).toLowerCase() || "api";
45
+ const context = Object.freeze({
46
+ ...normalizeObject(source.context),
47
+ channel,
48
+ surface: resolveSurface(request, source.surface, defaultSurfaceId),
49
+ routeVisibility: routeVisibility(request, source),
50
+ requestMeta: Object.freeze({
51
+ ...normalizeObject(source.context?.requestMeta),
52
+ request
53
+ })
54
+ });
55
+
56
+ const actionId = source.actionId;
57
+ const version = source.version == null ? null : source.version;
58
+ const definition = actions.getDefinition(actionId, version);
59
+ const execute = definition.kind === "stream" ? actions.executeStream : actions.execute;
60
+ return execute({
61
+ actionId,
62
+ version,
63
+ input: normalizeObject(source.input),
64
+ context,
65
+ deps: normalizeObject(source.deps)
66
+ });
67
+ };
68
+
69
+ Object.defineProperty(request, propertyName, {
70
+ configurable: true,
71
+ enumerable: false,
72
+ writable: true,
73
+ value: executeAction
74
+ });
75
+ return executeAction;
76
+ }
77
+
78
+ export { attachDirectRequestActionExecutor };
@@ -1 +1,2 @@
1
- export { registerRouteVisibilityResolver } from "../registries/routeVisibilityResolverRegistry.js";
1
+ export { createCapabilityHttpRuntime } from "./capabilityHttpRuntime.js";
2
+ export { HttpProvider } from "./HttpProvider.js";
@@ -2,16 +2,4 @@ export { HttpKernelError, RouteDefinitionError, RouteRegistrationError } from ".
2
2
  export { HttpRouter, createRouter, joinPath } from "./router.js";
3
3
  export { defineRouteValidator, compileRouteValidator, resolveRouteValidatorOptions } from "./routeValidator.js";
4
4
  export { BaseController, DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE, resolveDomainErrorStatus } from "./controller.js";
5
- export {
6
- defaultMissingHandler,
7
- defaultApplyRoutePolicy,
8
- normalizeRoutePolicyConfig,
9
- registerRoutes,
10
- registerHttpRuntime,
11
- createHttpRuntime
12
- } from "./kernel.js";
13
- export {
14
- resolveRouteVisibilityResolvers,
15
- registerRouteVisibilityResolver,
16
- resolveRouteVisibilityContext
17
- } from "../../registries/routeVisibilityResolverRegistry.js";
5
+ export { defaultMissingHandler, registerRoutes } from "./routeRegistration.js";
@@ -6,8 +6,7 @@ import { defaultMissingHandler } from "../../support/defaultMissingHandler.js";
6
6
  import { registerJsonApiContentTypeParser } from "../../runtime/fastifyBootstrap.js";
7
7
  import { RouteRegistrationError } from "./errors.js";
8
8
  import { executeMiddlewareStack, normalizeRuntimeMiddlewareConfig, resolveRouteMiddlewareHandlers } from "./middlewareRuntime.js";
9
- import { attachRequestScope } from "./requestScope.js";
10
- import { attachRequestActionExecutor } from "./requestActionExecutor.js";
9
+ import { attachDirectRequestActionExecutor } from "../directRequestActionExecutor.js";
11
10
  import { normalizeRouteOutputTransform, normalizeRouteTransport } from "./routeTransport.js";
12
11
 
13
12
  const { structuredClone: cloneRouteSchema } = globalThis;
@@ -291,23 +290,21 @@ function registerRoutes(
291
290
  fastify,
292
291
  {
293
292
  routes = [],
294
- app = null,
295
293
  applyRoutePolicy = defaultApplyRoutePolicy,
296
294
  missingHandler = defaultMissingHandler,
297
- enableRequestScope = true,
298
- requestScopeProperty = "scope",
299
295
  requestActionExecutorProperty = "executeAction",
300
- actionExecutorToken = "actionExecutor",
301
296
  requestActionDefaultChannel = "api",
302
297
  requestActionDefaultSurface = "",
303
- requestScopeIdPrefix = "http",
304
- requestIdResolver = null,
305
- middleware = {}
298
+ middleware = {},
299
+ actions = null
306
300
  } = {}
307
301
  ) {
308
302
  if (!fastify || typeof fastify.route !== "function") {
309
303
  throw new RouteRegistrationError("registerRoutes requires a Fastify instance.");
310
304
  }
305
+ if (!actions || typeof actions.execute !== "function") {
306
+ throw new RouteRegistrationError("registerRoutes requires the runtime.actions capability.");
307
+ }
311
308
 
312
309
  const normalizedRoutes = normalizeArray(routes);
313
310
  const publicRoutes = normalizedRoutes.filter((route) => route?.internal !== true);
@@ -353,23 +350,10 @@ function registerRoutes(
353
350
  request.routeOptions.config = normalizeObject(routeOptions?.config);
354
351
  }
355
352
 
356
- if (enableRequestScope) {
357
- attachRequestScope({
358
- app,
359
- request,
360
- reply,
361
- requestScopeProperty,
362
- requestScopeIdPrefix,
363
- requestIdResolver
364
- });
365
- }
366
-
367
- attachRequestActionExecutor({
368
- app,
353
+ attachDirectRequestActionExecutor({
354
+ actions,
369
355
  request,
370
- requestScopeProperty,
371
- requestActionExecutorProperty,
372
- actionExecutorToken,
356
+ property: requestActionExecutorProperty,
373
357
  defaultChannel: requestActionDefaultChannel,
374
358
  defaultSurfaceId: routeActionDefaultSurface
375
359
  });
@@ -1,5 +1,5 @@
1
1
  export {
2
2
  registerSurfaceRequestConstraint,
3
- resolveRuntimeProfileFromSurface,
4
- tryCreateProviderRuntimeFromApp
3
+ resolveRuntimeProfileFromSurface
5
4
  } from "./surfaceRuntime.js";
5
+ export { createInstalledRuntime } from "./installedRuntime.js";
@@ -0,0 +1,122 @@
1
+ import { createActionProvider } from "../actions/actionProvider.js";
2
+ import { createCapabilityRuntime } from "../../shared/capabilities/runtime.js";
3
+ import { HttpProvider } from "../http/HttpProvider.js";
4
+ import { BootstrapProvider } from "../runtime/BootstrapProvider.js";
5
+ import { EventProvider } from "../runtime/EventProvider.js";
6
+ import {
7
+ collectGlobalUiPaths,
8
+ resolveInstalledJskitPackages,
9
+ resolvePackageLoadOrder,
10
+ validatePackageCapabilities
11
+ } from "./providerRuntime/packageCatalog.js";
12
+ import {
13
+ appendCapabilityProviders,
14
+ loadCapabilityProviders
15
+ } from "./providerRuntime/capabilityProviderLoader.js";
16
+
17
+ function normalizeInputCapabilities({ inputs, config, env, logger, fastify, appRoot }) {
18
+ const source = inputs && typeof inputs === "object" && !Array.isArray(inputs) ? inputs : {};
19
+ const normalized = {
20
+ ...source,
21
+ "runtime.app-root": appRoot,
22
+ "runtime.config": Object.freeze({ ...(config && typeof config === "object" ? config : {}) }),
23
+ "runtime.env": Object.freeze({ ...(env && typeof env === "object" ? env : {}) }),
24
+ "runtime.logger": logger || console,
25
+ ...(fastify && typeof fastify.route === "function" ? { "runtime.fastify": fastify } : {})
26
+ };
27
+ return Object.freeze(normalized);
28
+ }
29
+
30
+ function builtinCapabilityProviders(inputCapabilities) {
31
+ return Object.fromEntries([
32
+ ["runtime.actions", ["@jskit-ai/kernel"]],
33
+ ["runtime.events", ["@jskit-ai/kernel"]],
34
+ ...(Object.hasOwn(inputCapabilities, "runtime.fastify")
35
+ ? [
36
+ ["runtime.bootstrap", ["@jskit-ai/kernel"]],
37
+ ["runtime.http", ["@jskit-ai/kernel"]]
38
+ ]
39
+ : []),
40
+ ...Object.keys(inputCapabilities).map((capabilityId) => [capabilityId, ["application"]])
41
+ ]);
42
+ }
43
+
44
+ async function createInstalledRuntime({
45
+ appRoot,
46
+ profile = "",
47
+ providers = [],
48
+ inputs = {},
49
+ config = {},
50
+ env = {},
51
+ logger = console,
52
+ fastify = null
53
+ } = {}) {
54
+ if (!appRoot || typeof appRoot !== "string") {
55
+ throw new TypeError("createInstalledRuntime requires appRoot.");
56
+ }
57
+
58
+ const inputCapabilities = normalizeInputCapabilities({ inputs, config, env, logger, fastify, appRoot });
59
+ const installedPackages = await resolveInstalledJskitPackages({ appRoot });
60
+ validatePackageCapabilities(installedPackages, {
61
+ builtinProvidersByCapability: builtinCapabilityProviders(inputCapabilities)
62
+ });
63
+ const orderedPackages = resolvePackageLoadOrder(installedPackages);
64
+ const orderedProviders = [];
65
+ const seenProviderIds = new Map();
66
+ const providerPackageOrder = [];
67
+
68
+ appendCapabilityProviders({
69
+ providers: [
70
+ EventProvider,
71
+ createActionProvider({ logger }),
72
+ ...(fastify && typeof fastify.route === "function" ? [HttpProvider, BootstrapProvider] : [])
73
+ ],
74
+ sourceId: "@jskit-ai/kernel",
75
+ seenProviderIds,
76
+ orderedProviders
77
+ });
78
+
79
+ for (const packageEntry of orderedPackages) {
80
+ const packageProviders = await loadCapabilityProviders({ packageEntry });
81
+ if (packageProviders.length === 0) {
82
+ continue;
83
+ }
84
+ providerPackageOrder.push(packageEntry.packageId);
85
+ appendCapabilityProviders({
86
+ providers: packageProviders,
87
+ sourceId: packageEntry.packageId,
88
+ seenProviderIds,
89
+ orderedProviders
90
+ });
91
+ }
92
+
93
+ appendCapabilityProviders({
94
+ providers: Array.isArray(providers) ? providers : [],
95
+ sourceId: "application",
96
+ seenProviderIds,
97
+ orderedProviders
98
+ });
99
+
100
+ const runtime = createCapabilityRuntime({
101
+ providers: orderedProviders,
102
+ inputs: inputCapabilities,
103
+ profile
104
+ });
105
+ await runtime.start();
106
+
107
+ if (fastify && typeof fastify.addHook === "function") {
108
+ fastify.addHook("onClose", async () => {
109
+ await runtime.shutdown();
110
+ });
111
+ }
112
+
113
+ return Object.freeze({
114
+ runtime,
115
+ packageOrder: Object.freeze(orderedPackages.map((entry) => entry.packageId)),
116
+ providerPackageOrder: Object.freeze(providerPackageOrder),
117
+ globalUiPaths: collectGlobalUiPaths(orderedPackages),
118
+ diagnostics: runtime.diagnostics()
119
+ });
120
+ }
121
+
122
+ export { createInstalledRuntime };
@@ -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
+ });