@jskit-ai/kernel 0.1.4

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 (185) hide show
  1. package/README.md +24 -0
  2. package/_testable/index.js +4 -0
  3. package/client/appConfig.js +33 -0
  4. package/client/componentInteraction.js +51 -0
  5. package/client/componentInteraction.test.js +111 -0
  6. package/client/descriptorSections.js +75 -0
  7. package/client/index.d.ts +70 -0
  8. package/client/index.js +3 -0
  9. package/client/logging.js +38 -0
  10. package/client/moduleBootstrap.js +670 -0
  11. package/client/moduleBootstrap.test.js +403 -0
  12. package/client/shellBootstrap.js +233 -0
  13. package/client/shellBootstrap.test.js +185 -0
  14. package/client/shellRouting.js +321 -0
  15. package/client/shellRouting.test.js +113 -0
  16. package/client/vite/clientBootstrapPlugin.js +259 -0
  17. package/client/vite/clientBootstrapPlugin.test.js +563 -0
  18. package/client/vite/index.js +3 -0
  19. package/internal/node/fileSystem.js +21 -0
  20. package/internal/node/installedPackageDescriptor.js +104 -0
  21. package/package.json +43 -0
  22. package/server/actions/ActionRuntimeServiceProvider.js +309 -0
  23. package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
  24. package/server/actions/index.js +8 -0
  25. package/server/container/ContainerCoreServiceProvider.js +27 -0
  26. package/server/container/index.js +10 -0
  27. package/server/exportPolicy.test.js +68 -0
  28. package/server/http/HttpFastifyServiceProvider.js +25 -0
  29. package/server/http/_testable/index.js +2 -0
  30. package/server/http/index.js +1 -0
  31. package/server/http/lib/controller.js +183 -0
  32. package/server/http/lib/controller.test.js +143 -0
  33. package/server/http/lib/errors.js +12 -0
  34. package/server/http/lib/httpRuntime.js +82 -0
  35. package/server/http/lib/index.js +18 -0
  36. package/server/http/lib/kernel.js +15 -0
  37. package/server/http/lib/kernel.test.js +880 -0
  38. package/server/http/lib/middlewareRuntime.js +149 -0
  39. package/server/http/lib/requestActionExecutor.js +258 -0
  40. package/server/http/lib/requestScope.js +59 -0
  41. package/server/http/lib/routeRegistration.js +165 -0
  42. package/server/http/lib/routeSupport.js +45 -0
  43. package/server/http/lib/routeValidator.js +469 -0
  44. package/server/http/lib/routeValidator.test.js +474 -0
  45. package/server/http/lib/router.js +206 -0
  46. package/server/kernel/KernelCoreServiceProvider.js +27 -0
  47. package/server/kernel/index.js +10 -0
  48. package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
  49. package/server/platform/index.js +5 -0
  50. package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
  51. package/server/platform/providerRuntime/helpers.js +45 -0
  52. package/server/platform/providerRuntime/lockfile.js +27 -0
  53. package/server/platform/providerRuntime/providerLoader.js +283 -0
  54. package/server/platform/providerRuntime.js +142 -0
  55. package/server/platform/providerRuntime.test.js +217 -0
  56. package/server/platform/runtime.js +40 -0
  57. package/server/platform/surfaceRuntime.js +150 -0
  58. package/server/platform/surfaceRuntime.test.js +136 -0
  59. package/server/registries/actionSurfaceSourceRegistry.js +150 -0
  60. package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
  61. package/server/registries/domainEventListenerRegistry.js +61 -0
  62. package/server/registries/index.js +36 -0
  63. package/server/registries/primitives.js +63 -0
  64. package/server/registries/routeVisibilityResolverRegistry.js +87 -0
  65. package/server/registries/serviceRegistrationRegistry.js +431 -0
  66. package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
  67. package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
  68. package/server/runtime/apiRoutePolicyParity.test.js +109 -0
  69. package/server/runtime/apiRouteRegistration.js +65 -0
  70. package/server/runtime/bootBootstrapRoutes.js +46 -0
  71. package/server/runtime/bootBootstrapRoutes.test.js +79 -0
  72. package/server/runtime/bootstrapContributors.test.js +114 -0
  73. package/server/runtime/canonicalJson.js +74 -0
  74. package/server/runtime/composition.js +142 -0
  75. package/server/runtime/domainEvents.test.js +114 -0
  76. package/server/runtime/domainRules.js +50 -0
  77. package/server/runtime/domainRules.test.js +87 -0
  78. package/server/runtime/entityChangeEvents.js +182 -0
  79. package/server/runtime/entityChangeEvents.test.js +211 -0
  80. package/server/runtime/errors.js +68 -0
  81. package/server/runtime/errors.test.js +73 -0
  82. package/server/runtime/fastifyBootstrap.js +372 -0
  83. package/server/runtime/fastifyBootstrap.test.js +194 -0
  84. package/server/runtime/index.js +6 -0
  85. package/server/runtime/integers.js +13 -0
  86. package/server/runtime/moduleConfig.js +269 -0
  87. package/server/runtime/moduleConfig.test.js +141 -0
  88. package/server/runtime/pagination.js +13 -0
  89. package/server/runtime/realtimeNormalization.js +21 -0
  90. package/server/runtime/requestUrl.js +38 -0
  91. package/server/runtime/routeUtils.js +20 -0
  92. package/server/runtime/runtimeAssembly.js +113 -0
  93. package/server/runtime/runtimeKernel.js +55 -0
  94. package/server/runtime/securityAudit.js +269 -0
  95. package/server/runtime/securityAudit.test.js +41 -0
  96. package/server/runtime/serviceAuthorization.js +113 -0
  97. package/server/runtime/serviceAuthorization.test.js +100 -0
  98. package/server/runtime/serviceRegistration.test.js +197 -0
  99. package/server/support/SupportCoreServiceProvider.js +25 -0
  100. package/server/support/appConfig.js +37 -0
  101. package/server/support/appConfig.test.js +94 -0
  102. package/server/support/defaultMissingHandler.js +7 -0
  103. package/server/support/index.js +2 -0
  104. package/server/support/routePolicyConfig.js +51 -0
  105. package/server/support/symlinkSafeRequire.js +78 -0
  106. package/server/support/symlinkSafeRequire.test.js +27 -0
  107. package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
  108. package/server/surface/index.js +19 -0
  109. package/shared/actions/actionContributorHelpers.js +34 -0
  110. package/shared/actions/actionContributorHelpers.test.js +16 -0
  111. package/shared/actions/actionDefinitions.js +488 -0
  112. package/shared/actions/actionDefinitions.test.js +212 -0
  113. package/shared/actions/audit.js +7 -0
  114. package/shared/actions/executionContext.js +97 -0
  115. package/shared/actions/executionContext.test.js +66 -0
  116. package/shared/actions/idempotency.js +62 -0
  117. package/shared/actions/index.js +2 -0
  118. package/shared/actions/observability.js +10 -0
  119. package/shared/actions/pipeline.js +287 -0
  120. package/shared/actions/policies.js +342 -0
  121. package/shared/actions/policies.test.js +233 -0
  122. package/shared/actions/registry.js +187 -0
  123. package/shared/actions/registry.test.js +381 -0
  124. package/shared/actions/requestMeta.js +36 -0
  125. package/shared/actions/textNormalization.js +3 -0
  126. package/shared/actions/withActionDefaults.js +34 -0
  127. package/shared/index.js +2 -0
  128. package/shared/runtime/application.js +323 -0
  129. package/shared/runtime/container.js +261 -0
  130. package/shared/runtime/containerErrors.js +22 -0
  131. package/shared/runtime/index.js +18 -0
  132. package/shared/runtime/kernelErrors.js +20 -0
  133. package/shared/runtime/serviceProvider.js +13 -0
  134. package/shared/support/formatDateTime.js +10 -0
  135. package/shared/support/formatDateTime.test.js +15 -0
  136. package/shared/support/index.js +14 -0
  137. package/shared/support/linkPath.js +67 -0
  138. package/shared/support/linkPath.test.js +35 -0
  139. package/shared/support/normalize.js +116 -0
  140. package/shared/support/normalize.test.js +48 -0
  141. package/shared/support/packageDescriptor.test.js +121 -0
  142. package/shared/support/permissions.js +50 -0
  143. package/shared/support/pickOwnProperties.js +17 -0
  144. package/shared/support/pickOwnProperties.test.js +25 -0
  145. package/shared/support/policies.js +11 -0
  146. package/shared/support/queryPath.js +33 -0
  147. package/shared/support/queryPath.test.js +19 -0
  148. package/shared/support/queryResilience.js +34 -0
  149. package/shared/support/queryResilience.test.js +33 -0
  150. package/shared/support/returnToPath.js +153 -0
  151. package/shared/support/returnToPath.test.js +123 -0
  152. package/shared/support/sorting.js +15 -0
  153. package/shared/support/tokens.js +23 -0
  154. package/shared/support/tokens.test.js +17 -0
  155. package/shared/support/visibility.js +56 -0
  156. package/shared/support/visibility.test.js +45 -0
  157. package/shared/surface/apiPaths.js +84 -0
  158. package/shared/surface/escapeRegExp.js +5 -0
  159. package/shared/surface/index.js +6 -0
  160. package/shared/surface/paths.js +273 -0
  161. package/shared/surface/registry.js +135 -0
  162. package/shared/surface/registry.test.js +44 -0
  163. package/shared/surface/runtime.js +357 -0
  164. package/shared/surface/runtime.test.js +319 -0
  165. package/shared/validators/createCursorListValidator.js +42 -0
  166. package/shared/validators/createCursorListValidator.test.js +34 -0
  167. package/shared/validators/cursorPaginationQueryValidator.js +31 -0
  168. package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
  169. package/shared/validators/index.js +12 -0
  170. package/shared/validators/inputNormalization.js +13 -0
  171. package/shared/validators/mergeObjectSchemas.js +31 -0
  172. package/shared/validators/mergeObjectSchemas.test.js +67 -0
  173. package/shared/validators/mergeValidators.js +89 -0
  174. package/shared/validators/mergeValidators.test.js +116 -0
  175. package/shared/validators/nestValidator.js +53 -0
  176. package/shared/validators/nestValidator.test.js +60 -0
  177. package/shared/validators/recordIdParamsValidator.js +36 -0
  178. package/shared/validators/recordIdParamsValidator.test.js +20 -0
  179. package/shared/validators/resourceRequiredMetadata.js +41 -0
  180. package/shared/validators/resourceRequiredMetadata.test.js +49 -0
  181. package/test/barrelExposure.test.js +106 -0
  182. package/test/dynamicImportPolicy.test.js +89 -0
  183. package/test/exportsContract.test.js +168 -0
  184. package/test/routeInputContractGuard.test.js +78 -0
  185. package/test/surfaceIndependence.test.js +109 -0
@@ -0,0 +1,187 @@
1
+ import {
2
+ createActionRuntimeError,
3
+ normalizeActionContributor,
4
+ createActionVersionKey
5
+ } from "./actionDefinitions.js";
6
+ import { executeActionPipeline } from "./pipeline.js";
7
+
8
+ function normalizeContributors(contributors) {
9
+ const source = Array.isArray(contributors) ? contributors : [];
10
+ return source.map((entry) => normalizeActionContributor(entry));
11
+ }
12
+
13
+ function buildDefinitionIndex(contributors) {
14
+ const definitions = [];
15
+ const byVersionKey = new Map();
16
+ const byActionId = new Map();
17
+
18
+ for (const contributor of contributors) {
19
+ for (const definition of contributor.actions) {
20
+ const versionKey = createActionVersionKey(definition.id, definition.version);
21
+ if (byVersionKey.has(versionKey)) {
22
+ const existing = byVersionKey.get(versionKey);
23
+ throw createActionRuntimeError(
24
+ 500,
25
+ `Action definition \"${versionKey}\" is duplicated between contributors \"${existing.contributorId}\" and \"${contributor.contributorId}\".`,
26
+ {
27
+ code: "ACTION_DEFINITION_DUPLICATE"
28
+ }
29
+ );
30
+ }
31
+
32
+ byVersionKey.set(versionKey, definition);
33
+ definitions.push(definition);
34
+
35
+ if (!byActionId.has(definition.id)) {
36
+ byActionId.set(definition.id, []);
37
+ }
38
+ byActionId.get(definition.id).push(definition);
39
+ }
40
+ }
41
+
42
+ for (const list of byActionId.values()) {
43
+ list.sort((left, right) => right.version - left.version);
44
+ }
45
+
46
+ return {
47
+ definitions: Object.freeze(definitions),
48
+ byVersionKey,
49
+ byActionId
50
+ };
51
+ }
52
+
53
+ function normalizeRequestedVersion(version) {
54
+ const parsed = Number(version);
55
+ if (!Number.isInteger(parsed) || parsed < 1) {
56
+ throw createActionRuntimeError(400, "Validation failed.", {
57
+ code: "ACTION_VERSION_INVALID",
58
+ details: {
59
+ fieldErrors: {
60
+ version: "version must be an integer >= 1."
61
+ }
62
+ }
63
+ });
64
+ }
65
+
66
+ return parsed;
67
+ }
68
+
69
+ function resolveActionDefinition(index, actionId, version) {
70
+ const normalizedActionId = String(actionId || "").trim();
71
+ if (!normalizedActionId) {
72
+ throw createActionRuntimeError(400, "Validation failed.", {
73
+ code: "ACTION_ID_REQUIRED",
74
+ details: {
75
+ fieldErrors: {
76
+ actionId: "actionId is required."
77
+ }
78
+ }
79
+ });
80
+ }
81
+
82
+ if (version == null) {
83
+ const versions = index.byActionId.get(normalizedActionId) || [];
84
+ if (versions.length < 1) {
85
+ throw createActionRuntimeError(404, "Not found.", {
86
+ code: "ACTION_NOT_FOUND",
87
+ details: {
88
+ actionId: normalizedActionId
89
+ }
90
+ });
91
+ }
92
+
93
+ return versions[0];
94
+ }
95
+
96
+ const normalizedVersion = normalizeRequestedVersion(version);
97
+ const versionKey = createActionVersionKey(normalizedActionId, normalizedVersion);
98
+ const definition = index.byVersionKey.get(versionKey);
99
+ if (!definition) {
100
+ throw createActionRuntimeError(404, "Not found.", {
101
+ code: "ACTION_NOT_FOUND",
102
+ details: {
103
+ actionId: normalizedActionId,
104
+ version: normalizedVersion
105
+ }
106
+ });
107
+ }
108
+
109
+ return definition;
110
+ }
111
+
112
+ function createActionRegistry({
113
+ contributors = [],
114
+ idempotencyAdapter,
115
+ auditAdapter,
116
+ observabilityAdapter,
117
+ logger = console
118
+ } = {}) {
119
+ const normalizedContributors = normalizeContributors(contributors);
120
+ const index = buildDefinitionIndex(normalizedContributors);
121
+
122
+ async function execute({ actionId, version = null, input = {}, context = {}, deps = {} } = {}) {
123
+ const definition = resolveActionDefinition(index, actionId, version);
124
+ const execution = await executeActionPipeline({
125
+ definition,
126
+ input,
127
+ context,
128
+ deps,
129
+ idempotencyAdapter,
130
+ auditAdapter,
131
+ observabilityAdapter,
132
+ logger
133
+ });
134
+
135
+ return execution.result;
136
+ }
137
+
138
+ async function executeStream({ actionId, version = null, input = {}, context = {}, deps = {} } = {}) {
139
+ const definition = resolveActionDefinition(index, actionId, version);
140
+ if (definition.kind !== "stream") {
141
+ throw createActionRuntimeError(400, "Validation failed.", {
142
+ code: "ACTION_STREAM_KIND_REQUIRED",
143
+ details: {
144
+ actionId: definition.id,
145
+ version: definition.version,
146
+ kind: definition.kind
147
+ }
148
+ });
149
+ }
150
+
151
+ const execution = await executeActionPipeline({
152
+ definition,
153
+ input,
154
+ context,
155
+ deps,
156
+ idempotencyAdapter,
157
+ auditAdapter,
158
+ observabilityAdapter,
159
+ logger
160
+ });
161
+
162
+ return execution.result;
163
+ }
164
+
165
+ function listDefinitions() {
166
+ return index.definitions.slice();
167
+ }
168
+
169
+ function getDefinition(actionId, version = null) {
170
+ return resolveActionDefinition(index, actionId, version);
171
+ }
172
+
173
+ return Object.freeze({
174
+ execute,
175
+ executeStream,
176
+ listDefinitions,
177
+ getDefinition
178
+ });
179
+ }
180
+
181
+ const __testables = {
182
+ normalizeContributors,
183
+ buildDefinitionIndex,
184
+ resolveActionDefinition
185
+ };
186
+
187
+ export { createActionRegistry, __testables };
@@ -0,0 +1,381 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { Type } from "typebox";
4
+
5
+ import { createActionRegistry } from "./registry.js";
6
+
7
+ function createPassThroughSchema() {
8
+ return {
9
+ parse(value) {
10
+ return value;
11
+ }
12
+ };
13
+ }
14
+
15
+ test("action registry executes latest version by default", async () => {
16
+ const calls = [];
17
+
18
+ const registry = createActionRegistry({
19
+ contributors: [
20
+ {
21
+ contributorId: "tests.settings",
22
+ domain: "settings",
23
+ actions: [
24
+ {
25
+ id: "settings.read",
26
+ version: 1,
27
+ domain: "settings",
28
+ kind: "query",
29
+ channels: ["api"],
30
+ surfaces: ["app", "admin", "console"],
31
+ inputValidator: { schema: createPassThroughSchema() },
32
+ idempotency: "none",
33
+ audit: {
34
+ actionName: "settings.read"
35
+ },
36
+ observability: {},
37
+ async execute() {
38
+ calls.push("version-one");
39
+ return {
40
+ version: 1
41
+ };
42
+ }
43
+ },
44
+ {
45
+ id: "settings.read",
46
+ version: 2,
47
+ domain: "settings",
48
+ kind: "query",
49
+ channels: ["api"],
50
+ surfaces: ["app", "admin", "console"],
51
+ inputValidator: { schema: createPassThroughSchema() },
52
+ idempotency: "none",
53
+ audit: {
54
+ actionName: "settings.read"
55
+ },
56
+ observability: {},
57
+ async execute() {
58
+ calls.push("v2");
59
+ return {
60
+ version: 2
61
+ };
62
+ }
63
+ }
64
+ ]
65
+ }
66
+ ]
67
+ });
68
+
69
+ const result = await registry.execute({
70
+ actionId: "settings.read",
71
+ input: {},
72
+ context: {
73
+ channel: "api",
74
+ surface: "app",
75
+ permissions: ["settings.read"]
76
+ }
77
+ });
78
+
79
+ assert.deepEqual(result, {
80
+ version: 2
81
+ });
82
+ assert.deepEqual(calls, ["v2"]);
83
+ });
84
+
85
+ test("action registry merges action input validators", async () => {
86
+ const registry = createActionRegistry({
87
+ contributors: [
88
+ {
89
+ contributorId: "tests.workspace",
90
+ domain: "workspace",
91
+ actions: [
92
+ {
93
+ id: "workspace.settings.update",
94
+ version: 1,
95
+ domain: "workspace",
96
+ kind: "command",
97
+ channels: ["api"],
98
+ surfaces: ["app"],
99
+ inputValidator: [
100
+ {
101
+ schema: Type.Object(
102
+ {
103
+ workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
104
+ },
105
+ { additionalProperties: false }
106
+ ),
107
+ normalize(input = {}) {
108
+ if (!Object.hasOwn(input, "workspaceSlug")) {
109
+ return {};
110
+ }
111
+
112
+ return {
113
+ workspaceSlug: String(input.workspaceSlug || "").trim().toLowerCase()
114
+ };
115
+ }
116
+ },
117
+ {
118
+ schema: Type.Object(
119
+ {
120
+ invitesEnabled: Type.Optional(Type.Boolean())
121
+ },
122
+ { additionalProperties: false }
123
+ ),
124
+ normalize(input = {}) {
125
+ if (!Object.hasOwn(input, "invitesEnabled")) {
126
+ return {};
127
+ }
128
+
129
+ return {
130
+ invitesEnabled: input.invitesEnabled === true
131
+ };
132
+ }
133
+ }
134
+ ],
135
+ idempotency: "optional",
136
+ audit: {
137
+ actionName: "workspace.settings.update"
138
+ },
139
+ observability: {},
140
+ async execute(input) {
141
+ return input;
142
+ }
143
+ }
144
+ ]
145
+ }
146
+ ]
147
+ });
148
+
149
+ const result = await registry.execute({
150
+ actionId: "workspace.settings.update",
151
+ input: {
152
+ workspaceSlug: " ACME ",
153
+ invitesEnabled: true
154
+ },
155
+ context: {
156
+ channel: "api",
157
+ surface: "app",
158
+ permissions: []
159
+ }
160
+ });
161
+
162
+ assert.deepEqual(result, {
163
+ workspaceSlug: "acme",
164
+ invitesEnabled: true
165
+ });
166
+ });
167
+
168
+ test("action registry fails startup on duplicate action id + version", () => {
169
+ assert.throws(
170
+ () =>
171
+ createActionRegistry({
172
+ contributors: [
173
+ {
174
+ contributorId: "tests.a",
175
+ domain: "settings",
176
+ actions: [
177
+ {
178
+ id: "settings.profile.update",
179
+ version: 1,
180
+ domain: "settings",
181
+ kind: "command",
182
+ channels: ["api"],
183
+ surfaces: ["app"],
184
+ inputValidator: { schema: createPassThroughSchema() },
185
+ idempotency: "optional",
186
+ audit: {
187
+ actionName: "settings.profile.update"
188
+ },
189
+ observability: {},
190
+ async execute() {
191
+ return {
192
+ ok: true
193
+ };
194
+ }
195
+ }
196
+ ]
197
+ },
198
+ {
199
+ contributorId: "tests.b",
200
+ domain: "settings",
201
+ actions: [
202
+ {
203
+ id: "settings.profile.update",
204
+ version: 1,
205
+ domain: "settings",
206
+ kind: "command",
207
+ channels: ["api"],
208
+ surfaces: ["app"],
209
+ inputValidator: { schema: createPassThroughSchema() },
210
+ idempotency: "optional",
211
+ audit: {
212
+ actionName: "settings.profile.update"
213
+ },
214
+ observability: {},
215
+ async execute() {
216
+ return {
217
+ ok: true
218
+ };
219
+ }
220
+ }
221
+ ]
222
+ }
223
+ ]
224
+ }),
225
+ /duplicated/
226
+ );
227
+ });
228
+
229
+ test("action registry rejects invalid version requests", async () => {
230
+ const registry = createActionRegistry({
231
+ contributors: [
232
+ {
233
+ contributorId: "tests.settings",
234
+ domain: "settings",
235
+ actions: [
236
+ {
237
+ id: "settings.read",
238
+ version: 1,
239
+ domain: "settings",
240
+ kind: "query",
241
+ channels: ["api"],
242
+ surfaces: ["app"],
243
+ inputValidator: { schema: createPassThroughSchema() },
244
+ idempotency: "none",
245
+ audit: {
246
+ actionName: "settings.read"
247
+ },
248
+ observability: {},
249
+ async execute() {
250
+ return {
251
+ ok: true
252
+ };
253
+ }
254
+ }
255
+ ]
256
+ }
257
+ ]
258
+ });
259
+
260
+ await assert.rejects(
261
+ () => registry.execute({ actionId: "settings.read", version: "invalid" }),
262
+ (error) => {
263
+ assert.equal(error.code, "ACTION_VERSION_INVALID");
264
+ assert.deepEqual(error.details?.fieldErrors, {
265
+ version: "version must be an integer >= 1."
266
+ });
267
+ return true;
268
+ }
269
+ );
270
+
271
+ await assert.rejects(
272
+ () => registry.execute({ actionId: "settings.read", version: 0 }),
273
+ (error) => {
274
+ assert.equal(error.code, "ACTION_VERSION_INVALID");
275
+ return true;
276
+ }
277
+ );
278
+ });
279
+
280
+ test("action registry ignores unknown legacy fields", async () => {
281
+ const registry = createActionRegistry({
282
+ contributors: [
283
+ {
284
+ contributorId: "tests.internal",
285
+ domain: "settings",
286
+ actions: [
287
+ {
288
+ id: "settings.internal.ping",
289
+ version: 1,
290
+ domain: "settings",
291
+ kind: "query",
292
+ channels: ["api", "internal"],
293
+ surfaces: ["app"],
294
+ consoleUsersOnly: true,
295
+ inputValidator: { schema: createPassThroughSchema() },
296
+ idempotency: "none",
297
+ audit: {
298
+ actionName: "settings.internal.ping"
299
+ },
300
+ observability: {},
301
+ async execute() {
302
+ return { ok: true };
303
+ }
304
+ }
305
+ ]
306
+ }
307
+ ]
308
+ });
309
+
310
+ const result = await registry.execute({
311
+ actionId: "settings.internal.ping",
312
+ context: {
313
+ channel: "api",
314
+ surface: "app"
315
+ }
316
+ });
317
+ assert.deepEqual(result, { ok: true });
318
+ });
319
+
320
+ test("action registry enforces action-level permissions", async () => {
321
+ const registry = createActionRegistry({
322
+ contributors: [
323
+ {
324
+ contributorId: "tests.permissions",
325
+ domain: "workspace",
326
+ actions: [
327
+ {
328
+ id: "workspace.settings.update",
329
+ version: 1,
330
+ domain: "workspace",
331
+ kind: "command",
332
+ channels: ["api", "internal"],
333
+ surfaces: ["app"],
334
+ permission: {
335
+ require: "all",
336
+ permissions: ["workspace.settings.update"]
337
+ },
338
+ inputValidator: { schema: createPassThroughSchema() },
339
+ idempotency: "optional",
340
+ audit: {
341
+ actionName: "workspace.settings.update"
342
+ },
343
+ observability: {},
344
+ async execute() {
345
+ return { ok: true };
346
+ }
347
+ }
348
+ ]
349
+ }
350
+ ]
351
+ });
352
+
353
+ await assert.rejects(
354
+ () =>
355
+ registry.execute({
356
+ actionId: "workspace.settings.update",
357
+ context: {
358
+ channel: "api",
359
+ surface: "app",
360
+ actor: { id: 7 },
361
+ permissions: ["workspace.settings.view"]
362
+ }
363
+ }),
364
+ (error) => {
365
+ assert.equal(error.code, "ACTION_PERMISSION_DENIED");
366
+ return true;
367
+ }
368
+ );
369
+
370
+ const allowed = await registry.execute({
371
+ actionId: "workspace.settings.update",
372
+ context: {
373
+ channel: "api",
374
+ surface: "app",
375
+ actor: { id: 7 },
376
+ permissions: ["workspace.settings.update"]
377
+ }
378
+ });
379
+
380
+ assert.deepEqual(allowed, { ok: true });
381
+ });
@@ -0,0 +1,36 @@
1
+ import { normalizeText } from "./textNormalization.js";
2
+
3
+ const DEFAULT_FIELDS = Object.freeze(["requestId", "commandId", "idempotencyKey", "ip", "userAgent", "request"]);
4
+
5
+ function normalizeRequestMeta(requestMeta = {}, { fields = DEFAULT_FIELDS, emptyAsNull = false } = {}) {
6
+ const source = requestMeta && typeof requestMeta === "object" ? requestMeta : {};
7
+ const fieldList = Array.isArray(fields) && fields.length > 0 ? fields : DEFAULT_FIELDS;
8
+
9
+ const normalizeValue = (value) => {
10
+ const normalized = normalizeText(value);
11
+ if (emptyAsNull && !normalized) {
12
+ return null;
13
+ }
14
+ return normalized;
15
+ };
16
+
17
+ const normalized = {
18
+ ...source
19
+ };
20
+ for (const field of fieldList) {
21
+ if (field === "request") {
22
+ normalized.request = source.request || null;
23
+ continue;
24
+ }
25
+ if (field === "logger") {
26
+ normalized.logger = source.logger && typeof source.logger.warn === "function" ? source.logger : null;
27
+ continue;
28
+ }
29
+
30
+ normalized[field] = normalizeValue(source[field]);
31
+ }
32
+
33
+ return normalized;
34
+ }
35
+
36
+ export { normalizeRequestMeta };
@@ -0,0 +1,3 @@
1
+ import { normalizeText, normalizeLowerText } from "../support/normalize.js";
2
+
3
+ export { normalizeText, normalizeLowerText };
@@ -0,0 +1,34 @@
1
+ import { normalizeObject } from "../support/normalize.js";
2
+
3
+ function withActionDefaults(actions = [], defaults = {}) {
4
+ const sourceActions = Array.isArray(actions) ? actions : [];
5
+ const sourceDefaults = normalizeObject(defaults);
6
+ const defaultDependencies = normalizeObject(sourceDefaults.dependencies);
7
+ const hasDefaultDependencies = Object.keys(defaultDependencies).length > 0;
8
+ const hasDefaultDomain = Object.hasOwn(sourceDefaults, "domain");
9
+
10
+ return Object.freeze(
11
+ sourceActions.map((entry) => {
12
+ const action = normalizeObject(entry);
13
+ const actionDependencies = normalizeObject(action.dependencies);
14
+ const next = {
15
+ ...action
16
+ };
17
+
18
+ if (hasDefaultDomain && !Object.hasOwn(next, "domain")) {
19
+ next.domain = sourceDefaults.domain;
20
+ }
21
+
22
+ if (hasDefaultDependencies || Object.keys(actionDependencies).length > 0) {
23
+ next.dependencies = Object.freeze({
24
+ ...defaultDependencies,
25
+ ...actionDependencies
26
+ });
27
+ }
28
+
29
+ return Object.freeze(next);
30
+ })
31
+ );
32
+ }
33
+
34
+ export { withActionDefaults };
@@ -0,0 +1,2 @@
1
+ export { resolveLinkPath } from "./support/linkPath.js";
2
+ export { normalizePathname } from "./surface/apiPaths.js";