@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,342 @@
1
+ import { Check, Errors } from "typebox/value";
2
+ import { createActionRuntimeError } from "./actionDefinitions.js";
3
+ import { normalizeLowerText, normalizeText } from "./textNormalization.js";
4
+ import { hasPermission, normalizePermissionList } from "../support/permissions.js";
5
+ import { isRecord, normalizeOpaqueId } from "../support/normalize.js";
6
+
7
+ function createActionValidationError({
8
+ status = 400,
9
+ message = "Validation failed.",
10
+ code = "ACTION_VALIDATION_FAILED",
11
+ details,
12
+ cause
13
+ } = {}) {
14
+ return createActionRuntimeError(status, message, {
15
+ code,
16
+ details,
17
+ cause
18
+ });
19
+ }
20
+
21
+ function ensureActionChannelAllowed(definition, context) {
22
+ const channel = normalizeLowerText(context?.channel);
23
+ const allowedChannels = Array.isArray(definition?.channels) ? definition.channels : [];
24
+
25
+ if (!channel || !allowedChannels.includes(channel)) {
26
+ throw createActionRuntimeError(403, "Forbidden.", {
27
+ code: "ACTION_CHANNEL_FORBIDDEN",
28
+ details: {
29
+ actionId: definition?.id,
30
+ channel
31
+ }
32
+ });
33
+ }
34
+ }
35
+
36
+ function ensureActionSurfaceAllowed(definition, context) {
37
+ const surface = normalizeLowerText(context?.surface);
38
+ const allowedSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
39
+
40
+ if (!surface || !allowedSurfaces.includes(surface)) {
41
+ throw createActionRuntimeError(403, "Forbidden.", {
42
+ code: "ACTION_SURFACE_FORBIDDEN",
43
+ details: {
44
+ actionId: definition?.id,
45
+ surface
46
+ }
47
+ });
48
+ }
49
+ }
50
+
51
+ function ensureActionPermissionAllowed(definition, context) {
52
+ const permission = isRecord(definition?.permission) ? definition.permission : { require: "none" };
53
+ const mode = normalizeLowerText(permission.require || "none");
54
+
55
+ if (mode === "none") {
56
+ return;
57
+ }
58
+
59
+ const actorId = normalizeOpaqueId(context?.actor?.id);
60
+ if (actorId == null) {
61
+ throw createActionRuntimeError(401, permission.message || "Authentication required.", {
62
+ code: permission.code || "ACTION_AUTHENTICATION_REQUIRED",
63
+ details: {
64
+ actionId: definition?.id
65
+ }
66
+ });
67
+ }
68
+
69
+ if (mode === "authenticated") {
70
+ return;
71
+ }
72
+
73
+ const requiredPermissions = normalizePermissionList(permission.permissions);
74
+ if (requiredPermissions.length < 1) {
75
+ return;
76
+ }
77
+
78
+ const actorPermissions = normalizePermissionList(context?.permissions);
79
+ if (mode === "all") {
80
+ for (const requiredPermission of requiredPermissions) {
81
+ if (hasPermission(actorPermissions, requiredPermission)) {
82
+ continue;
83
+ }
84
+
85
+ throw createActionRuntimeError(403, permission.message || "Forbidden.", {
86
+ code: permission.code || "ACTION_PERMISSION_DENIED",
87
+ details: {
88
+ actionId: definition?.id,
89
+ permission: requiredPermission
90
+ }
91
+ });
92
+ }
93
+
94
+ return;
95
+ }
96
+
97
+ const hasAnyPermission = requiredPermissions.some((requiredPermission) => hasPermission(actorPermissions, requiredPermission));
98
+ if (!hasAnyPermission) {
99
+ throw createActionRuntimeError(403, permission.message || "Forbidden.", {
100
+ code: permission.code || "ACTION_PERMISSION_DENIED",
101
+ details: {
102
+ actionId: definition?.id,
103
+ requiredPermissions
104
+ }
105
+ });
106
+ }
107
+ }
108
+
109
+ function normalizeSchemaValidationErrors(schema) {
110
+ const errors = Array.isArray(schema?.errors) ? schema.errors : [];
111
+ if (errors.length < 1) {
112
+ return null;
113
+ }
114
+
115
+ const fieldErrors = {};
116
+ for (const entry of errors) {
117
+ const rawFieldPath = normalizeText(entry?.path || entry?.instancePath || entry?.field || "");
118
+ const fieldPath = rawFieldPath
119
+ ? rawFieldPath.replace(/^\//, "").replace(/\//g, ".")
120
+ : "input";
121
+ const message = normalizeText(entry?.message || "Invalid value.") || "Invalid value.";
122
+ fieldErrors[fieldPath] = message;
123
+ }
124
+
125
+ return Object.keys(fieldErrors).length > 0 ? fieldErrors : null;
126
+ }
127
+
128
+ function buildSchemaValidatorError({ phase, definition } = {}) {
129
+ return createActionValidationError({
130
+ details: {
131
+ error: "Schema validator must return { ok, value, errors } or throw.",
132
+ phase,
133
+ actionId: definition?.id,
134
+ version: definition?.version
135
+ }
136
+ });
137
+ }
138
+
139
+ function normalizeTypeBoxValidationErrors(schema, payload) {
140
+ const issues = Check(schema, payload) ? [] : [...Errors(schema, payload)];
141
+ if (issues.length < 1) {
142
+ return null;
143
+ }
144
+
145
+ return normalizeSchemaValidationErrors({
146
+ errors: issues
147
+ });
148
+ }
149
+
150
+ function normalizeFunctionSchemaResult(result, payload, { phase, definition } = {}) {
151
+ if (!isRecord(result) || typeof result.ok !== "boolean") {
152
+ throw buildSchemaValidatorError({ phase, definition });
153
+ }
154
+
155
+ if (result.ok) {
156
+ if (Object.hasOwn(result, "value")) {
157
+ return result.value;
158
+ }
159
+ return payload;
160
+ }
161
+
162
+ const details = {};
163
+ if (Object.hasOwn(result, "errors")) {
164
+ if (Array.isArray(result.errors)) {
165
+ const fieldErrors = normalizeSchemaValidationErrors({ errors: result.errors });
166
+ if (fieldErrors) {
167
+ details.fieldErrors = fieldErrors;
168
+ } else {
169
+ details.errors = result.errors;
170
+ }
171
+ } else if (result.errors && typeof result.errors === "object") {
172
+ details.fieldErrors = result.errors;
173
+ } else if (result.errors != null) {
174
+ details.error = String(result.errors);
175
+ }
176
+ }
177
+
178
+ throw createActionValidationError({
179
+ details: Object.keys(details).length > 0 ? details : undefined
180
+ });
181
+ }
182
+
183
+ async function normalizeValidatorPayload(validator, payload, { phase, definition, context }) {
184
+ if (!validator || typeof validator !== "object") {
185
+ return payload;
186
+ }
187
+
188
+ if (typeof validator.normalize !== "function") {
189
+ return payload;
190
+ }
191
+
192
+ return await validator.normalize(payload, {
193
+ phase,
194
+ actionId: definition?.id,
195
+ version: definition?.version,
196
+ context
197
+ });
198
+ }
199
+
200
+ async function validateSchemaPayload(schema, payload, { phase, definition }) {
201
+ if (schema == null) {
202
+ return payload;
203
+ }
204
+
205
+ if (typeof schema === "function") {
206
+ const result = await schema(payload, {
207
+ phase,
208
+ actionId: definition?.id,
209
+ version: definition?.version
210
+ });
211
+ return normalizeFunctionSchemaResult(result, payload, { phase, definition });
212
+ }
213
+
214
+ if (!isRecord(schema)) {
215
+ throw buildSchemaValidatorError({ phase, definition });
216
+ }
217
+
218
+ if (typeof schema.parse === "function") {
219
+ return schema.parse(payload);
220
+ }
221
+
222
+ if (typeof schema.assert === "function") {
223
+ const assertionResult = await schema.assert(payload);
224
+ return assertionResult == null ? payload : assertionResult;
225
+ }
226
+
227
+ if (typeof schema.check === "function") {
228
+ const valid = await schema.check(payload);
229
+ if (!valid) {
230
+ throw createActionValidationError();
231
+ }
232
+ return payload;
233
+ }
234
+
235
+ if (typeof schema.validate === "function") {
236
+ const valid = await schema.validate(payload);
237
+ if (!valid) {
238
+ throw createActionValidationError({
239
+ details: {
240
+ fieldErrors: normalizeSchemaValidationErrors(schema)
241
+ }
242
+ });
243
+ }
244
+ return payload;
245
+ }
246
+
247
+ const fieldErrors = normalizeTypeBoxValidationErrors(schema, payload);
248
+ if (!fieldErrors) {
249
+ return payload;
250
+ }
251
+
252
+ throw createActionValidationError({
253
+ details: {
254
+ fieldErrors
255
+ }
256
+ });
257
+ }
258
+
259
+ async function normalizeActionInput(definition, input, context) {
260
+ try {
261
+ const normalizedInput = await normalizeValidatorPayload(definition?.inputValidator, input, {
262
+ phase: "input",
263
+ definition,
264
+ context
265
+ });
266
+
267
+ return await validateSchemaPayload(definition?.inputValidator?.schema, normalizedInput, {
268
+ phase: "input",
269
+ definition,
270
+ context
271
+ });
272
+ } catch (error) {
273
+ if (error?.code === "ACTION_VALIDATION_FAILED") {
274
+ throw error;
275
+ }
276
+
277
+ throw createActionValidationError({
278
+ details: {
279
+ error: String(error?.message || "Invalid input.")
280
+ },
281
+ cause: error
282
+ });
283
+ }
284
+ }
285
+
286
+ async function normalizeActionOutput(definition, output, context) {
287
+ if (!definition?.outputValidator) {
288
+ return output;
289
+ }
290
+
291
+ try {
292
+ const normalizedOutput = await normalizeValidatorPayload(definition.outputValidator, output, {
293
+ phase: "output",
294
+ definition,
295
+ context
296
+ });
297
+
298
+ return await validateSchemaPayload(definition.outputValidator.schema, normalizedOutput, {
299
+ phase: "output",
300
+ definition,
301
+ context
302
+ });
303
+ } catch (error) {
304
+ if (error?.code === "ACTION_VALIDATION_FAILED") {
305
+ throw createActionValidationError({
306
+ status: 500,
307
+ message: "Action output validation failed.",
308
+ code: "ACTION_OUTPUT_VALIDATION_FAILED",
309
+ details: error.details,
310
+ cause: error
311
+ });
312
+ }
313
+
314
+ throw createActionValidationError({
315
+ status: 500,
316
+ message: "Action output validation failed.",
317
+ code: "ACTION_OUTPUT_VALIDATION_FAILED",
318
+ details: {
319
+ error: String(error?.message || "Invalid output.")
320
+ },
321
+ cause: error
322
+ });
323
+ }
324
+ }
325
+
326
+ const __testables = {
327
+ normalizeText,
328
+ normalizeLowerText,
329
+ normalizeSchemaValidationErrors,
330
+ normalizeTypeBoxValidationErrors,
331
+ normalizeValidatorPayload,
332
+ validateSchemaPayload
333
+ };
334
+
335
+ export {
336
+ ensureActionChannelAllowed,
337
+ ensureActionSurfaceAllowed,
338
+ ensureActionPermissionAllowed,
339
+ normalizeActionInput,
340
+ normalizeActionOutput,
341
+ __testables
342
+ };
@@ -0,0 +1,233 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { Type } from "typebox";
4
+
5
+ import { ensureActionPermissionAllowed, normalizeActionInput, normalizeActionOutput } from "./policies.js";
6
+
7
+ test("function schema returns normalized value when ok", async () => {
8
+ const definition = {
9
+ id: "tests.ok",
10
+ version: 1,
11
+ inputValidator: {
12
+ schema: () => ({
13
+ ok: true,
14
+ value: {
15
+ normalized: true
16
+ }
17
+ })
18
+ }
19
+ };
20
+
21
+ const result = await normalizeActionInput(definition, { raw: true }, {});
22
+ assert.deepEqual(result, { normalized: true });
23
+ });
24
+
25
+ test("function schema rejects non-validator results", async () => {
26
+ const definition = {
27
+ id: "tests.invalid",
28
+ version: 1,
29
+ inputValidator: {
30
+ schema: () => false
31
+ }
32
+ };
33
+
34
+ await assert.rejects(
35
+ () => normalizeActionInput(definition, { raw: true }, {}),
36
+ (error) => {
37
+ assert.equal(error.code, "ACTION_VALIDATION_FAILED");
38
+ assert.match(error.details?.error || "", /Schema validator must return/);
39
+ return true;
40
+ }
41
+ );
42
+ });
43
+
44
+ test("function schema propagates validation errors", async () => {
45
+ const definition = {
46
+ id: "tests.errors",
47
+ version: 2,
48
+ inputValidator: {
49
+ schema: () => ({
50
+ ok: false,
51
+ errors: {
52
+ input: "input is required"
53
+ }
54
+ })
55
+ }
56
+ };
57
+
58
+ await assert.rejects(
59
+ () => normalizeActionInput(definition, null, {}),
60
+ (error) => {
61
+ assert.equal(error.code, "ACTION_VALIDATION_FAILED");
62
+ assert.deepEqual(error.details?.fieldErrors, {
63
+ input: "input is required"
64
+ });
65
+ return true;
66
+ }
67
+ );
68
+ });
69
+
70
+ test("raw TypeBox action schemas validate normalized action input", async () => {
71
+ const definition = {
72
+ id: "tests.typebox",
73
+ version: 1,
74
+ inputValidator: {
75
+ schema: Type.Object(
76
+ {
77
+ workspaceSlug: Type.String({ minLength: 1 })
78
+ },
79
+ { additionalProperties: false }
80
+ ),
81
+ normalize(value = {}) {
82
+ return {
83
+ workspaceSlug: String(value.workspaceSlug || "").trim().toLowerCase()
84
+ };
85
+ }
86
+ }
87
+ };
88
+
89
+ const result = await normalizeActionInput(definition, { workspaceSlug: " ACME " }, {});
90
+ assert.deepEqual(result, { workspaceSlug: "acme" });
91
+ });
92
+
93
+ test("typebox input validation normalizes pointer field errors to plain keys", async () => {
94
+ const definition = {
95
+ id: "tests.typebox.errors",
96
+ version: 1,
97
+ inputValidator: {
98
+ schema: Type.Object(
99
+ {
100
+ name: Type.String({ maxLength: 1 })
101
+ },
102
+ { additionalProperties: false }
103
+ )
104
+ }
105
+ };
106
+
107
+ await assert.rejects(
108
+ () => normalizeActionInput(definition, { name: "too long" }, {}),
109
+ (error) => {
110
+ const fieldErrors = error.details?.fieldErrors || {};
111
+ assert.equal(typeof fieldErrors.name, "string");
112
+ assert.equal(Object.hasOwn(fieldErrors, "/name"), false);
113
+ return true;
114
+ }
115
+ );
116
+ });
117
+
118
+ test("action output normalization runs before output validation", async () => {
119
+ const definition = {
120
+ id: "tests.output",
121
+ version: 1,
122
+ outputValidator: {
123
+ schema: Type.Object(
124
+ {
125
+ ok: Type.Boolean()
126
+ },
127
+ { additionalProperties: false }
128
+ ),
129
+ normalize(payload = {}) {
130
+ return {
131
+ ok: Boolean(payload.ok)
132
+ };
133
+ }
134
+ }
135
+ };
136
+
137
+ const result = await normalizeActionOutput(definition, { ok: 1 }, {});
138
+ assert.deepEqual(result, { ok: true });
139
+ });
140
+
141
+ test("action permission denies unauthenticated access when required", () => {
142
+ assert.throws(
143
+ () =>
144
+ ensureActionPermissionAllowed(
145
+ {
146
+ id: "tests.secure",
147
+ permission: {
148
+ require: "authenticated"
149
+ }
150
+ },
151
+ {
152
+ permissions: []
153
+ }
154
+ ),
155
+ (error) => error?.statusCode === 401 && error?.code === "ACTION_AUTHENTICATION_REQUIRED"
156
+ );
157
+ });
158
+
159
+ test("action permission enforces required permissions", () => {
160
+ assert.throws(
161
+ () =>
162
+ ensureActionPermissionAllowed(
163
+ {
164
+ id: "tests.secure.perm",
165
+ permission: {
166
+ require: "all",
167
+ permissions: ["workspace.settings.update"]
168
+ }
169
+ },
170
+ {
171
+ actor: {
172
+ id: 7
173
+ },
174
+ permissions: ["workspace.settings.view"]
175
+ }
176
+ ),
177
+ (error) => error?.statusCode === 403 && error?.code === "ACTION_PERMISSION_DENIED"
178
+ );
179
+
180
+ assert.doesNotThrow(() =>
181
+ ensureActionPermissionAllowed(
182
+ {
183
+ id: "tests.secure.perm",
184
+ permission: {
185
+ require: "all",
186
+ permissions: ["workspace.settings.update"]
187
+ }
188
+ },
189
+ {
190
+ actor: {
191
+ id: 7
192
+ },
193
+ permissions: ["workspace.settings.update"]
194
+ }
195
+ )
196
+ );
197
+
198
+ assert.doesNotThrow(() =>
199
+ ensureActionPermissionAllowed(
200
+ {
201
+ id: "tests.secure.perm",
202
+ permission: {
203
+ require: "all",
204
+ permissions: ["workspace.settings.update"]
205
+ }
206
+ },
207
+ {
208
+ actor: {
209
+ id: 7
210
+ },
211
+ permissions: ["workspace.settings.*"]
212
+ }
213
+ )
214
+ );
215
+
216
+ assert.doesNotThrow(() =>
217
+ ensureActionPermissionAllowed(
218
+ {
219
+ id: "tests.secure.perm",
220
+ permission: {
221
+ require: "all",
222
+ permissions: ["workspace.settings.update"]
223
+ }
224
+ },
225
+ {
226
+ actor: {
227
+ id: "user-7"
228
+ },
229
+ permissions: ["workspace.settings.update"]
230
+ }
231
+ )
232
+ );
233
+ });