@jskit-ai/kernel 0.1.55 → 0.1.56

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 (55) hide show
  1. package/package.json +3 -2
  2. package/server/actions/ActionRuntimeServiceProvider.test.js +23 -15
  3. package/server/http/lib/kernel.test.js +447 -0
  4. package/server/http/lib/routeRegistration.js +236 -15
  5. package/server/http/lib/routeTransport.js +126 -0
  6. package/server/http/lib/routeValidator.js +133 -198
  7. package/server/http/lib/routeValidator.test.js +385 -278
  8. package/server/http/lib/router.js +17 -2
  9. package/server/platform/providerRuntime.test.js +7 -7
  10. package/server/runtime/bootBootstrapRoutes.js +2 -18
  11. package/server/runtime/bootBootstrapRoutes.test.js +5 -14
  12. package/server/runtime/fastifyBootstrap.js +119 -0
  13. package/server/runtime/fastifyBootstrap.test.js +119 -1
  14. package/server/runtime/moduleConfig.js +32 -62
  15. package/server/runtime/moduleConfig.test.js +48 -24
  16. package/server/support/pageTargets.js +15 -9
  17. package/server/support/pageTargets.test.js +1 -1
  18. package/shared/actions/actionContributorHelpers.js +5 -11
  19. package/shared/actions/actionDefinitions.js +37 -150
  20. package/shared/actions/actionDefinitions.test.js +117 -136
  21. package/shared/actions/policies.js +25 -169
  22. package/shared/actions/policies.test.js +76 -87
  23. package/shared/actions/registry.test.js +24 -50
  24. package/shared/support/crudFieldContract.js +322 -0
  25. package/shared/support/crudFieldContract.test.js +67 -0
  26. package/shared/support/crudListFilters.js +582 -38
  27. package/shared/support/crudListFilters.test.js +178 -8
  28. package/shared/support/crudLookup.js +14 -7
  29. package/shared/support/crudLookup.test.js +91 -66
  30. package/shared/support/shellLayoutTargets.test.js +1 -1
  31. package/shared/validators/composeSchemaDefinitions.js +53 -0
  32. package/shared/validators/composeSchemaDefinitions.test.js +156 -0
  33. package/shared/validators/createCursorListValidator.js +22 -35
  34. package/shared/validators/createCursorListValidator.test.js +22 -23
  35. package/shared/validators/cursorPaginationQueryValidator.js +14 -24
  36. package/shared/validators/cursorPaginationQueryValidator.test.js +18 -8
  37. package/shared/validators/htmlTimeSchemas.js +6 -4
  38. package/shared/validators/index.js +15 -7
  39. package/shared/validators/jsonRestSchemaSupport.js +139 -0
  40. package/shared/validators/mergeObjectSchemas.js +44 -6
  41. package/shared/validators/mergeObjectSchemas.test.js +60 -35
  42. package/shared/validators/recordIdParamsValidator.js +19 -52
  43. package/shared/validators/recordIdParamsValidator.test.js +13 -8
  44. package/shared/validators/resourceRequiredMetadata.js +3 -3
  45. package/shared/validators/resourceRequiredMetadata.test.js +29 -16
  46. package/shared/validators/schemaDefinitions.js +126 -0
  47. package/shared/validators/schemaDefinitions.test.js +51 -0
  48. package/shared/validators/schemaPayloadValidation.js +65 -0
  49. package/test/barrelExposure.test.js +30 -0
  50. package/test/routeInputContractGuard.test.js +10 -6
  51. package/shared/validators/mergeValidators.js +0 -89
  52. package/shared/validators/mergeValidators.test.js +0 -116
  53. package/shared/validators/nestValidator.js +0 -53
  54. package/shared/validators/nestValidator.test.js +0 -60
  55. package/shared/validators/settingsFieldNormalization.js +0 -40
@@ -1,122 +1,114 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { Type } from "typebox";
3
+ import { createSchema } from "json-rest-schema";
4
4
 
5
5
  import { __testables, normalizeActionDefinition } from "./actionDefinitions.js";
6
6
 
7
- function createWorkspaceSlugValidator() {
8
- return {
9
- schema: Type.Object(
10
- {
11
- workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
12
- },
13
- { additionalProperties: false }
14
- ),
15
- normalize(input = {}) {
16
- const source = input && typeof input === "object" ? input : {};
17
- if (!Object.hasOwn(source, "workspaceSlug")) {
18
- return {};
19
- }
7
+ function assertJsonRestSchemaDefinition(definition) {
8
+ assert.equal(typeof definition?.schema?.patch, "function");
9
+ assert.equal(typeof definition?.schema?.replace, "function");
10
+ assert.equal(typeof definition?.schema?.toJsonSchema, "function");
11
+ }
20
12
 
13
+ function createMockJsonRestSchema() {
14
+ return {
15
+ create(payload = {}) {
16
+ return {
17
+ validatedObject: payload,
18
+ errors: {}
19
+ };
20
+ },
21
+ replace(payload = {}) {
22
+ return this.create(payload);
23
+ },
24
+ patch(payload = {}) {
25
+ return this.create(payload);
26
+ },
27
+ toJsonSchema() {
21
28
  return {
22
- workspaceSlug: String(source.workspaceSlug || "").trim().toLowerCase()
29
+ type: "object"
23
30
  };
24
31
  }
25
32
  };
26
33
  }
27
34
 
28
- function createPatchValidator() {
35
+ function createWorkspaceSlugSchema() {
29
36
  return {
30
- schema: Type.Object(
31
- {
32
- name: Type.Optional(Type.String({ minLength: 1 }))
33
- },
34
- { additionalProperties: false }
35
- ),
36
- normalize(input = {}) {
37
- const source = input && typeof input === "object" ? input : {};
38
- if (!Object.hasOwn(source, "name")) {
39
- return {};
37
+ schema: createSchema({
38
+ workspaceSlug: {
39
+ type: "string",
40
+ minLength: 1
40
41
  }
42
+ }),
43
+ mode: "patch"
44
+ };
45
+ }
41
46
 
42
- return {
43
- name: String(source.name || "").trim().toLowerCase()
44
- };
45
- }
47
+ function createPatchSchema() {
48
+ return {
49
+ schema: createSchema({
50
+ name: {
51
+ type: "string",
52
+ minLength: 1
53
+ }
54
+ }),
55
+ mode: "patch"
46
56
  };
47
57
  }
48
58
 
49
- test("normalizeActionValidators accepts section-map validator syntax", async () => {
50
- const validator = __testables.normalizeActionValidators(
51
- {
52
- payload: createPatchValidator()
53
- },
54
- "inputValidator",
59
+ test("normalizeActionInputDefinition accepts a single schema definition", () => {
60
+ const definition = __testables.normalizeActionInputDefinition(
61
+ createPatchSchema(),
62
+ "input",
55
63
  { required: true }
56
64
  );
57
65
 
58
- assert.equal(typeof validator?.normalize, "function");
59
- assert.equal(validator?.schema?.type, "object");
60
- assert.ok(Object.hasOwn(validator.schema?.properties || {}, "payload"));
61
-
62
- const normalized = await validator.normalize({
63
- payload: {
64
- name: " Acme "
65
- }
66
- });
67
-
68
- assert.deepEqual(normalized, {
69
- payload: {
70
- name: "acme"
71
- }
72
- });
66
+ assertJsonRestSchemaDefinition(definition);
73
67
  });
74
68
 
75
- test("normalizeActionValidators composes root validators with section-map validators", async () => {
76
- const validator = __testables.normalizeActionValidators(
77
- [
78
- createWorkspaceSlugValidator(),
79
- {
80
- patch: createPatchValidator()
81
- }
82
- ],
83
- "inputValidator",
84
- { required: true }
69
+ test("normalizeActionInputDefinition rejects section-map syntax", () => {
70
+ assert.throws(
71
+ () =>
72
+ __testables.normalizeActionInputDefinition(
73
+ {
74
+ payload: createPatchSchema()
75
+ },
76
+ "input",
77
+ { required: true }
78
+ ),
79
+ /Action definition input must be a schema definition object/
85
80
  );
86
-
87
- const properties = Object.keys(validator.schema?.properties || {}).sort();
88
- assert.deepEqual(properties, ["patch", "workspaceSlug"]);
89
-
90
- const normalized = await validator.normalize({
91
- workspaceSlug: " TEAM-ALPHA ",
92
- patch: {
93
- name: " Project X "
94
- }
95
- });
96
-
97
- assert.deepEqual(normalized, {
98
- workspaceSlug: "team-alpha",
99
- patch: {
100
- name: "project x"
101
- }
102
- });
103
81
  });
104
82
 
105
- test("normalizeActionValidators rejects invalid section-map entries", () => {
83
+ test("normalizeActionInputDefinition rejects bare schema instances", () => {
106
84
  assert.throws(
107
85
  () =>
108
- __testables.normalizeActionValidators(
109
- {
110
- payload: {
111
- normalize() {
112
- return {};
113
- }
86
+ __testables.normalizeActionInputDefinition(
87
+ createSchema({
88
+ name: {
89
+ type: "string",
90
+ minLength: 1
114
91
  }
115
- },
116
- "inputValidator",
92
+ }),
93
+ "input",
94
+ { required: true }
95
+ ),
96
+ /Action definition input must be a schema definition object/
97
+ );
98
+ });
99
+
100
+ test("normalizeActionInputDefinition rejects validator arrays", () => {
101
+ assert.throws(
102
+ () =>
103
+ __testables.normalizeActionInputDefinition(
104
+ [
105
+ createWorkspaceSlugSchema(),
106
+ createPatchSchema()
107
+ ],
108
+ "input",
117
109
  { required: true }
118
110
  ),
119
- /inputValidator\[0\]\.payload\.schema is required/
111
+ /input must be a single schema definition/
120
112
  );
121
113
  });
122
114
 
@@ -131,7 +123,7 @@ test("normalizeActionExtensions keeps plain objects", () => {
131
123
  assert.equal(extensions.assistant?.description, "Update workspace settings.");
132
124
  });
133
125
 
134
- test("normalizeActionDefinition stays channel-agnostic and ignores unknown legacy fields", () => {
126
+ test("normalizeActionDefinition stays channel-agnostic and ignores unknown extra fields", () => {
135
127
  const definition = normalizeActionDefinition({
136
128
  id: "demo.workspace.settings.update",
137
129
  domain: "demo",
@@ -139,11 +131,11 @@ test("normalizeActionDefinition stays channel-agnostic and ignores unknown legac
139
131
  kind: "command",
140
132
  channels: ["automation"],
141
133
  surfaces: ["admin"],
142
- inputValidator: {
143
- schema: Type.Object({}, { additionalProperties: false })
134
+ input: {
135
+ schema: createSchema({})
144
136
  },
145
- outputValidator: {
146
- schema: Type.Object({}, { additionalProperties: false })
137
+ output: {
138
+ schema: createSchema({})
147
139
  },
148
140
  idempotency: "none",
149
141
  assistantTool: {
@@ -156,57 +148,46 @@ test("normalizeActionDefinition stays channel-agnostic and ignores unknown legac
156
148
  assert.equal(Object.prototype.hasOwnProperty.call(definition, "assistantTool"), false);
157
149
  });
158
150
 
159
- test("normalizeActionOutputValidator accepts section-map syntax", async () => {
160
- const outputValidator = __testables.normalizeActionOutputValidator(
151
+ test("normalizeActionOutputDefinition accepts single schema definitions", () => {
152
+ const output = __testables.normalizeActionOutputDefinition(
161
153
  {
162
- payload: createPatchValidator()
154
+ schema: createSchema({
155
+ ok: {
156
+ type: "boolean",
157
+ required: true
158
+ }
159
+ })
163
160
  },
164
- "outputValidator",
161
+ "output",
165
162
  { required: false }
166
163
  );
167
164
 
168
- assert.equal(outputValidator?.schema?.type, "object");
169
- assert.ok(Object.hasOwn(outputValidator?.schema?.properties || {}, "payload"));
170
-
171
- const normalized = await outputValidator.normalize({
172
- payload: {
173
- name: " Acme "
174
- }
175
- });
176
-
177
- assert.deepEqual(normalized, {
178
- payload: {
179
- name: "acme"
180
- }
181
- });
165
+ assertJsonRestSchemaDefinition(output);
182
166
  });
183
167
 
184
- test("normalizeActionOutputValidator composes array validators", async () => {
185
- const outputValidator = __testables.normalizeActionOutputValidator(
186
- [
187
- createWorkspaceSlugValidator(),
188
- {
189
- payload: createPatchValidator()
190
- }
191
- ],
192
- "outputValidator",
193
- { required: false }
168
+ test("normalizeActionOutputDefinition rejects section-map syntax", () => {
169
+ assert.throws(
170
+ () =>
171
+ __testables.normalizeActionOutputDefinition(
172
+ {
173
+ payload: createPatchSchema()
174
+ },
175
+ "output",
176
+ { required: false }
177
+ ),
178
+ /Action definition output must be a schema definition object/
194
179
  );
180
+ });
195
181
 
196
- const properties = Object.keys(outputValidator.schema?.properties || {}).sort();
197
- assert.deepEqual(properties, ["payload", "workspaceSlug"]);
198
-
199
- const normalized = await outputValidator.normalize({
200
- workspaceSlug: " TEAM-ALPHA ",
201
- payload: {
202
- name: " Project X "
203
- }
204
- });
182
+ test("normalizeActionInputDefinition preserves mode for json-rest-schema definitions", () => {
183
+ const definition = __testables.normalizeActionInputDefinition(
184
+ {
185
+ schema: createMockJsonRestSchema(),
186
+ mode: "patch"
187
+ },
188
+ "input",
189
+ { required: true }
190
+ );
205
191
 
206
- assert.deepEqual(normalized, {
207
- workspaceSlug: "team-alpha",
208
- payload: {
209
- name: "project x"
210
- }
211
- });
192
+ assert.equal(definition.mode, "patch");
212
193
  });
@@ -1,8 +1,10 @@
1
- import { Check, Errors } from "typebox/value";
2
1
  import { createActionRuntimeError } from "./actionDefinitions.js";
3
2
  import { normalizeLowerText, normalizeText } from "./textNormalization.js";
4
3
  import { hasPermission, normalizePermissionList } from "../support/permissions.js";
5
4
  import { isRecord, normalizeOpaqueId } from "../support/normalize.js";
5
+ import {
6
+ validateSchemaPayload
7
+ } from "../validators/schemaPayloadValidation.js";
6
8
 
7
9
  function createActionValidationError({
8
10
  status = 400,
@@ -106,170 +108,22 @@ function ensureActionPermissionAllowed(definition, context) {
106
108
  }
107
109
  }
108
110
 
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
111
  async function normalizeActionInput(definition, input, context) {
260
112
  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, {
113
+ return validateSchemaPayload(definition?.input, input, {
268
114
  phase: "input",
269
115
  definition,
270
116
  context
271
117
  });
272
118
  } catch (error) {
119
+ if (isRecord(error?.fieldErrors) || isRecord(error?.details?.fieldErrors)) {
120
+ throw createActionValidationError({
121
+ details: {
122
+ fieldErrors: error.fieldErrors || error.details?.fieldErrors
123
+ },
124
+ cause: error
125
+ });
126
+ }
273
127
  if (error?.code === "ACTION_VALIDATION_FAILED") {
274
128
  throw error;
275
129
  }
@@ -284,23 +138,28 @@ async function normalizeActionInput(definition, input, context) {
284
138
  }
285
139
 
286
140
  async function normalizeActionOutput(definition, output, context) {
287
- if (!definition?.outputValidator) {
141
+ if (!definition?.output) {
288
142
  return output;
289
143
  }
290
144
 
291
145
  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, {
146
+ return validateSchemaPayload(definition.output, output, {
299
147
  phase: "output",
300
148
  definition,
301
149
  context
302
150
  });
303
151
  } catch (error) {
152
+ if (isRecord(error?.fieldErrors) || isRecord(error?.details?.fieldErrors)) {
153
+ throw createActionValidationError({
154
+ status: 500,
155
+ message: "Action output validation failed.",
156
+ code: "ACTION_OUTPUT_VALIDATION_FAILED",
157
+ details: {
158
+ fieldErrors: error.fieldErrors || error.details?.fieldErrors
159
+ },
160
+ cause: error
161
+ });
162
+ }
304
163
  if (error?.code === "ACTION_VALIDATION_FAILED") {
305
164
  throw createActionValidationError({
306
165
  status: 500,
@@ -326,9 +185,6 @@ async function normalizeActionOutput(definition, output, context) {
326
185
  const __testables = {
327
186
  normalizeText,
328
187
  normalizeLowerText,
329
- normalizeSchemaValidationErrors,
330
- normalizeTypeBoxValidationErrors,
331
- normalizeValidatorPayload,
332
188
  validateSchemaPayload
333
189
  };
334
190