@jskit-ai/kernel 0.1.245 → 0.1.247

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/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +13 -75
  4. package/client/index.js +1 -1
  5. package/client/logging.js +8 -2
  6. package/client/moduleBootstrap.js +41 -33
  7. package/client/moduleBootstrap.test.js +35 -0
  8. package/client/shellBootstrap.js +1 -8
  9. package/client/shellBootstrap.test.js +32 -5
  10. package/client/shellRouting.js +16 -61
  11. package/client/shellRouting.test.js +30 -1
  12. package/client/vite/clientBootstrapPlugin.js +0 -6
  13. package/client/vite/clientBootstrapPlugin.test.js +2 -3
  14. package/internal/node/installedPackages.js +19 -62
  15. package/internal/node/installedPackages.test.js +33 -0
  16. package/package.json +2 -4
  17. package/server/actions/actionCatalogue.js +13 -34
  18. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  19. package/{shared → server}/actions/actionDefinitions.js +24 -131
  20. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  21. package/server/actions/actionEvents.js +97 -2
  22. package/server/actions/actionEvents.test.js +79 -0
  23. package/{shared → server}/actions/executionContext.js +3 -13
  24. package/server/actions/idempotency.js +24 -0
  25. package/server/actions/pipeline.js +87 -0
  26. package/{shared → server}/actions/policies.js +20 -33
  27. package/{shared → server}/actions/policies.test.js +6 -6
  28. package/{shared → server}/actions/registry.js +3 -22
  29. package/{shared → server}/actions/registry.test.js +70 -32
  30. package/server/actions/requestMeta.js +17 -0
  31. package/server/http/capabilityHttpRuntime.js +1 -1
  32. package/server/http/capabilityHttpRuntime.test.js +56 -0
  33. package/server/http/errorHandling.js +291 -0
  34. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  35. package/server/http/lib/routeRegistration.js +36 -150
  36. package/server/http/lib/routeTransport.js +2 -0
  37. package/server/http/lib/routeValidator.js +5 -47
  38. package/server/http/lib/routeValidator.test.js +15 -17
  39. package/server/http/lib/router.js +10 -94
  40. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  41. package/server/platform/providerRuntime/helpers.js +1 -34
  42. package/server/runtime/integers.js +1 -5
  43. package/server/runtime/serviceAuthorization.js +4 -9
  44. package/server/support/appConfig.js +2 -1
  45. package/server/support/appConfigFiles.js +1 -38
  46. package/server/support/appConfigFiles.test.js +3 -39
  47. package/server/support/index.js +1 -20
  48. package/shared/capabilities/defineProvider.js +1 -1
  49. package/shared/capabilities/index.js +1 -1
  50. package/shared/index.js +1 -1
  51. package/shared/support/index.js +1 -4
  52. package/shared/support/mobileConfig.js +99 -0
  53. package/shared/support/mobileConfig.test.js +113 -0
  54. package/shared/support/normalize.js +0 -108
  55. package/shared/support/normalize.test.js +0 -134
  56. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  57. package/shared/support/returnToPath.js +1 -20
  58. package/shared/support/returnToPath.test.js +1 -36
  59. package/shared/support/shellLayoutTargets.js +2 -140
  60. package/shared/support/shellLayoutTargets.test.js +1 -110
  61. package/shared/support/sorting.js +1 -9
  62. package/shared/surface/apiPaths.js +1 -82
  63. package/shared/surface/paths.js +38 -171
  64. package/shared/surface/registry.js +2 -1
  65. package/shared/surface/runtime.js +18 -109
  66. package/shared/surface/runtime.test.js +57 -0
  67. package/shared/validators/index.js +1 -4
  68. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  69. package/shared/validators/recordIdParamsValidator.js +0 -6
  70. package/shared/validators/schemaDefinitions.js +4 -30
  71. package/shared/validators/schemaPayloadValidation.js +2 -6
  72. package/test/barrelExposure.test.js +3 -7
  73. package/test/exportsContract.test.js +1 -3
  74. package/client/mobileLaunchRouting.js +0 -231
  75. package/client/mobileLaunchRouting.test.js +0 -230
  76. package/server/http/lib/controller.js +0 -183
  77. package/server/http/lib/controller.test.js +0 -143
  78. package/server/http/lib/index.js +0 -5
  79. package/server/runtime/canonicalJson.js +0 -74
  80. package/server/runtime/entityChangeEvents.js +0 -307
  81. package/server/runtime/entityChangeEvents.test.js +0 -274
  82. package/server/runtime/fastifyBootstrap.js +0 -553
  83. package/server/runtime/moduleConfig.js +0 -223
  84. package/server/runtime/moduleConfig.test.js +0 -165
  85. package/server/runtime/pagination.js +0 -13
  86. package/server/runtime/realtimeNormalization.js +0 -21
  87. package/server/runtime/routeUtils.js +0 -20
  88. package/server/runtime/securityAudit.js +0 -269
  89. package/server/runtime/securityAudit.test.js +0 -41
  90. package/server/support/pageTargets.js +0 -772
  91. package/server/support/pageTargets.test.js +0 -659
  92. package/server/support/shellOutlets.js +0 -576
  93. package/server/support/shellOutlets.test.js +0 -336
  94. package/server/surface/index.js +0 -18
  95. package/shared/actions/audit.js +0 -7
  96. package/shared/actions/idempotency.js +0 -62
  97. package/shared/actions/observability.js +0 -10
  98. package/shared/actions/pipeline.js +0 -309
  99. package/shared/actions/requestMeta.js +0 -36
  100. package/shared/actions/textNormalization.js +0 -3
  101. package/shared/actions/withActionDefaults.js +0 -34
  102. /package/{shared → server}/actions/executionContext.test.js +0 -0
@@ -56,34 +56,34 @@ function createPatchSchema() {
56
56
  };
57
57
  }
58
58
 
59
- test("normalizeActionInputDefinition accepts a single schema definition", () => {
60
- const definition = __testables.normalizeActionInputDefinition(
59
+ test("normalizeActionSchema accepts a single schema definition", () => {
60
+ const definition = __testables.normalizeActionSchema(
61
61
  createPatchSchema(),
62
62
  "input",
63
- { required: true }
63
+ { required: true, defaultMode: "patch" }
64
64
  );
65
65
 
66
66
  assertJsonRestSchemaDefinition(definition);
67
67
  });
68
68
 
69
- test("normalizeActionInputDefinition rejects section-map syntax", () => {
69
+ test("normalizeActionSchema rejects section-map syntax", () => {
70
70
  assert.throws(
71
71
  () =>
72
- __testables.normalizeActionInputDefinition(
72
+ __testables.normalizeActionSchema(
73
73
  {
74
74
  payload: createPatchSchema()
75
75
  },
76
76
  "input",
77
- { required: true }
77
+ { required: true, defaultMode: "patch" }
78
78
  ),
79
79
  /Action definition input must be a schema definition object/
80
80
  );
81
81
  });
82
82
 
83
- test("normalizeActionInputDefinition rejects bare schema instances", () => {
83
+ test("normalizeActionSchema rejects bare schema instances", () => {
84
84
  assert.throws(
85
85
  () =>
86
- __testables.normalizeActionInputDefinition(
86
+ __testables.normalizeActionSchema(
87
87
  createSchema({
88
88
  name: {
89
89
  type: "string",
@@ -91,22 +91,22 @@ test("normalizeActionInputDefinition rejects bare schema instances", () => {
91
91
  }
92
92
  }),
93
93
  "input",
94
- { required: true }
94
+ { required: true, defaultMode: "patch" }
95
95
  ),
96
96
  /Action definition input must be a schema definition object/
97
97
  );
98
98
  });
99
99
 
100
- test("normalizeActionInputDefinition rejects validator arrays", () => {
100
+ test("normalizeActionSchema rejects validator arrays", () => {
101
101
  assert.throws(
102
102
  () =>
103
- __testables.normalizeActionInputDefinition(
103
+ __testables.normalizeActionSchema(
104
104
  [
105
105
  createWorkspaceSlugSchema(),
106
106
  createPatchSchema()
107
107
  ],
108
108
  "input",
109
- { required: true }
109
+ { required: true, defaultMode: "patch" }
110
110
  ),
111
111
  /input must be a single schema definition/
112
112
  );
@@ -148,8 +148,8 @@ test("normalizeActionDefinition stays channel-agnostic and ignores unknown extra
148
148
  assert.equal(Object.prototype.hasOwnProperty.call(definition, "customExtension"), false);
149
149
  });
150
150
 
151
- test("normalizeActionOutputDefinition accepts single schema definitions", () => {
152
- const output = __testables.normalizeActionOutputDefinition(
151
+ test("normalizeActionSchema accepts single schema definitions", () => {
152
+ const output = __testables.normalizeActionSchema(
153
153
  {
154
154
  schema: createSchema({
155
155
  ok: {
@@ -159,35 +159,89 @@ test("normalizeActionOutputDefinition accepts single schema definitions", () =>
159
159
  })
160
160
  },
161
161
  "output",
162
- { required: false }
162
+ { defaultMode: "replace" }
163
163
  );
164
164
 
165
165
  assertJsonRestSchemaDefinition(output);
166
166
  });
167
167
 
168
- test("normalizeActionOutputDefinition rejects section-map syntax", () => {
168
+ test("normalizeActionSchema rejects section-map syntax", () => {
169
169
  assert.throws(
170
170
  () =>
171
- __testables.normalizeActionOutputDefinition(
171
+ __testables.normalizeActionSchema(
172
172
  {
173
173
  payload: createPatchSchema()
174
174
  },
175
175
  "output",
176
- { required: false }
176
+ { defaultMode: "replace" }
177
177
  ),
178
178
  /Action definition output must be a schema definition object/
179
179
  );
180
180
  });
181
181
 
182
- test("normalizeActionInputDefinition preserves mode for json-rest-schema definitions", () => {
183
- const definition = __testables.normalizeActionInputDefinition(
182
+ test("normalizeActionSchema preserves mode for json-rest-schema definitions", () => {
183
+ const definition = __testables.normalizeActionSchema(
184
184
  {
185
185
  schema: createMockJsonRestSchema(),
186
186
  mode: "patch"
187
187
  },
188
188
  "input",
189
- { required: true }
189
+ { required: true, defaultMode: "patch" }
190
190
  );
191
191
 
192
192
  assert.equal(definition.mode, "patch");
193
193
  });
194
+
195
+ test("normalizeActionSchema rejects a missing required schema and allows a missing optional one", () => {
196
+ assert.equal(__testables.normalizeActionSchema(null, "output"), null);
197
+ assert.throws(
198
+ () => __testables.normalizeActionSchema(undefined, "input", { required: true }),
199
+ /Action definition input is required/
200
+ );
201
+ });
202
+
203
+ test("normalizeActionDefinition keeps only the contract the runtime enforces", () => {
204
+ const definition = normalizeActionDefinition({
205
+ id: "demo.status.read",
206
+ domain: "demo",
207
+ channels: ["api"],
208
+ surfaces: ["app"],
209
+ input: { schema: createSchema({}) },
210
+ audit: { actionName: "demo.status.read" },
211
+ observability: {},
212
+ execute: async () => ({})
213
+ });
214
+
215
+ assert.equal(Object.hasOwn(definition, "audit"), false);
216
+ assert.equal(Object.hasOwn(definition, "observability"), false);
217
+ assert.deepEqual(Object.keys(definition).sort(), [
218
+ "channels",
219
+ "contributorId",
220
+ "domain",
221
+ "events",
222
+ "execute",
223
+ "extensions",
224
+ "id",
225
+ "idempotency",
226
+ "input",
227
+ "kind",
228
+ "output",
229
+ "permission",
230
+ "surfaces",
231
+ "version"
232
+ ]);
233
+ });
234
+
235
+ test("normalizeActionDefinition validates domains with the architecture id pattern", () => {
236
+ const base = {
237
+ id: "demo.status.read",
238
+ channels: ["api"],
239
+ surfaces: ["app"],
240
+ input: { schema: createSchema({}) },
241
+ execute: async () => ({})
242
+ };
243
+ assert.equal(normalizeActionDefinition({ ...base, domain: " Demo.Status " }).domain, "demo.status");
244
+ assert.throws(() => normalizeActionDefinition({ ...base, domain: "1demo" }), {
245
+ code: "ACTION_DEFINITION_INVALID"
246
+ });
247
+ });
@@ -1,8 +1,103 @@
1
- import { resolveDefaultScope } from "../runtime/entityChangeEvents.js";
2
1
  import { normalizeObject, normalizeOpaqueId, normalizeText } from "../../shared/support/normalize.js";
3
2
 
4
3
  const ENTITY_CHANGE_OPERATIONS = new Set(["created", "updated", "deleted"]);
5
4
 
5
+ function resolveContextScope(context = {}) {
6
+ const sourceScope = normalizeObject(context?.scope || context?.requestMeta?.scope || context?.request?.scope);
7
+ const kind = normalizeText(sourceScope.kind).toLowerCase();
8
+ if (!kind) {
9
+ return null;
10
+ }
11
+ if (kind === "global") {
12
+ return {
13
+ kind: "global",
14
+ id: null
15
+ };
16
+ }
17
+
18
+ const scopeId = normalizeOpaqueId(sourceScope.id);
19
+ if (scopeId == null) {
20
+ return null;
21
+ }
22
+
23
+ const resolvedScope = {
24
+ kind,
25
+ id: scopeId
26
+ };
27
+
28
+ const scopeUserId = normalizeOpaqueId(sourceScope.userId);
29
+ if (scopeUserId != null) {
30
+ resolvedScope.userId = scopeUserId;
31
+ }
32
+
33
+ const scopedScopeId = normalizeOpaqueId(sourceScope.scopeId);
34
+ if (scopedScopeId != null) {
35
+ resolvedScope.scopeId = scopedScopeId;
36
+ }
37
+
38
+ return resolvedScope;
39
+ }
40
+
41
+ function resolveVisibilityScope(visibilityContext = {}) {
42
+ const visibility = normalizeText(visibilityContext.visibility).toLowerCase();
43
+ const scopeKind = normalizeText(visibilityContext.scopeKind || visibility).toLowerCase();
44
+ const scopeOwnerId = normalizeOpaqueId(visibilityContext.scopeOwnerId);
45
+ const userId = normalizeOpaqueId(visibilityContext.userId);
46
+ const requiresActorScope = visibilityContext.requiresActorScope === true;
47
+
48
+ if (requiresActorScope && userId == null) {
49
+ return null;
50
+ }
51
+
52
+ if (scopeKind && scopeOwnerId != null) {
53
+ const scope = {
54
+ kind: scopeKind,
55
+ id: scopeOwnerId
56
+ };
57
+ if (requiresActorScope) {
58
+ scope.scopeId = scopeOwnerId;
59
+ scope.userId = userId;
60
+ }
61
+ return scope;
62
+ }
63
+ if (scopeKind && scopeKind !== "user") {
64
+ return null;
65
+ }
66
+
67
+ if (!scopeKind && scopeOwnerId != null) {
68
+ return {
69
+ kind: "scope",
70
+ id: scopeOwnerId
71
+ };
72
+ }
73
+
74
+ if (scopeKind === "user" && userId != null) {
75
+ return {
76
+ kind: "user",
77
+ id: userId
78
+ };
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ function resolveEventScope(context = {}) {
85
+ const visibilityScope = resolveVisibilityScope(normalizeObject(context.visibilityContext));
86
+ if (visibilityScope) {
87
+ return visibilityScope;
88
+ }
89
+
90
+ const contextScope = resolveContextScope(context);
91
+ if (contextScope) {
92
+ return contextScope;
93
+ }
94
+
95
+ return {
96
+ kind: "global",
97
+ id: null
98
+ };
99
+ }
100
+
6
101
  function resolveValue(value, execution) {
7
102
  return typeof value === "function" ? value(execution) : value;
8
103
  }
@@ -53,7 +148,7 @@ function createEntityChangedActionEvent({
53
148
  entity: normalizedEntity,
54
149
  operation: normalizedOperation,
55
150
  entityId: normalizedEntityId,
56
- scope: resolveDefaultScope(normalizeObject(context.visibilityContext), { context }),
151
+ scope: resolveEventScope(context),
57
152
  actorId: normalizeOpaqueId(context?.actor?.id),
58
153
  commandId: normalizeText(requestMeta.commandId || requestMeta.idempotencyKey) || null,
59
154
  sourceClientId: normalizeText(requestMeta.sourceClientId) || null,
@@ -0,0 +1,79 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createEntityChangedActionEvent } from "./actionEvents.js";
4
+
5
+ function buildEvent(context, { entityId = 11, realtime = null } = {}) {
6
+ return createEntityChangedActionEvent({
7
+ source: "workspace",
8
+ entity: "settings",
9
+ operation: "updated",
10
+ entityId,
11
+ realtime
12
+ })({ context });
13
+ }
14
+
15
+ test("entity-changed action events carry actor, command and source client", () => {
16
+ const event = buildEvent({
17
+ actor: { id: 17 },
18
+ requestMeta: { idempotencyKey: "cmd-1", sourceClientId: "client-a" },
19
+ visibilityContext: { scopeOwnerId: 23 }
20
+ }, {
21
+ entityId: 5,
22
+ realtime: { event: "settings.changed", payload: { reason: "edit" } }
23
+ });
24
+
25
+ assert.equal(event.type, "entity.changed");
26
+ assert.equal(event.entityId, "5");
27
+ assert.deepEqual(event.scope, { kind: "scope", id: "23" });
28
+ assert.equal(event.actorId, "17");
29
+ assert.equal(event.commandId, "cmd-1");
30
+ assert.equal(event.sourceClientId, "client-a");
31
+ assert.deepEqual(event.realtime, { event: "settings.changed", audience: "event_scope", payload: { reason: "edit" } });
32
+ });
33
+
34
+ test("entity-changed action events skip missing entity identifiers", () => {
35
+ assert.equal(buildEvent({}, { entityId: null }), null);
36
+ });
37
+
38
+ test("entity-changed action events fall back to the context scope when visibility has no owner", () => {
39
+ const event = buildEvent({
40
+ scope: { kind: "workspace", id: 23 },
41
+ visibilityContext: { visibility: "workspace" }
42
+ });
43
+
44
+ assert.deepEqual(event.scope, { kind: "workspace", id: "23" });
45
+ });
46
+
47
+ test("entity-changed action events keep actor-scoped ownership only with an explicit user", () => {
48
+ assert.deepEqual(buildEvent({
49
+ actor: { id: "user_17" },
50
+ visibilityContext: {
51
+ scopeKind: "workspace_user",
52
+ scopeOwnerId: "workspace_23",
53
+ userId: "user_17",
54
+ requiresActorScope: true
55
+ }
56
+ }).scope, {
57
+ kind: "workspace_user",
58
+ id: "workspace_23",
59
+ scopeId: "workspace_23",
60
+ userId: "user_17"
61
+ });
62
+
63
+ assert.deepEqual(buildEvent({
64
+ actor: { id: "user_17" },
65
+ visibilityContext: {
66
+ scopeKind: "workspace_user",
67
+ scopeOwnerId: "workspace_23",
68
+ requiresActorScope: true
69
+ }
70
+ }).scope, { kind: "global", id: null });
71
+
72
+ assert.deepEqual(buildEvent({
73
+ visibilityContext: {
74
+ scopeKind: "workspace_user",
75
+ scopeOwnerId: "workspace_23",
76
+ requiresActorScope: false
77
+ }
78
+ }).scope, { kind: "workspace_user", id: "workspace_23" });
79
+ });
@@ -1,6 +1,6 @@
1
1
  import { normalizeRequestMeta } from "./requestMeta.js";
2
- import { normalizeLowerText, normalizeText } from "./textNormalization.js";
3
- import { normalizePermissionList } from "../support/permissions.js";
2
+ import { normalizeLowerText, normalizeText } from "../../shared/support/normalize.js";
3
+ import { normalizePermissionList } from "../../shared/support/permissions.js";
4
4
 
5
5
  function copyRecord(value) {
6
6
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -73,14 +73,4 @@ function normalizeExecutionContext(context = {}) {
73
73
  });
74
74
  }
75
75
 
76
- const __testables = {
77
- normalizeText,
78
- normalizeLowerText,
79
- normalizePermissions: normalizePermissionList,
80
- copyRecord,
81
- normalizeActor,
82
- normalizeRequestMeta,
83
- normalizeTimeMeta
84
- };
85
-
86
- export { normalizeExecutionContext, __testables };
76
+ export { normalizeExecutionContext };
@@ -0,0 +1,24 @@
1
+ import { createActionRuntimeError } from "./actionDefinitions.js";
2
+ import { normalizeText } from "../../shared/support/normalize.js";
3
+
4
+ function ensureIdempotencyKeyIfRequired(definition, context) {
5
+ if (definition.idempotency !== "required") {
6
+ return;
7
+ }
8
+
9
+ const key = normalizeText(context?.requestMeta?.idempotencyKey || context?.requestMeta?.commandId);
10
+ if (key) {
11
+ return;
12
+ }
13
+
14
+ throw createActionRuntimeError(400, "Validation failed.", {
15
+ code: "ACTION_IDEMPOTENCY_KEY_REQUIRED",
16
+ details: {
17
+ fieldErrors: {
18
+ idempotencyKey: "Idempotency key is required for this action."
19
+ }
20
+ }
21
+ });
22
+ }
23
+
24
+ export { ensureIdempotencyKeyIfRequired };
@@ -0,0 +1,87 @@
1
+ import {
2
+ ensureActionChannelAllowed,
3
+ ensureActionSurfaceAllowed,
4
+ ensureActionPermissionAllowed,
5
+ normalizeActionInput,
6
+ normalizeActionOutput
7
+ } from "./policies.js";
8
+ import { ensureIdempotencyKeyIfRequired } from "./idempotency.js";
9
+ import { normalizeExecutionContext } from "./executionContext.js";
10
+
11
+ async function publishActionEvents(events, builders, execution) {
12
+ if (!Array.isArray(builders) || builders.length === 0) return;
13
+ if (!events || typeof events.publish !== "function") {
14
+ throw new TypeError("Action event publishing requires runtime.events.publish().");
15
+ }
16
+ for (const builder of builders) {
17
+ const built = await builder(execution);
18
+ const values = Array.isArray(built) ? built : [built];
19
+ for (const event of values) {
20
+ if (event != null) await events.publish(event);
21
+ }
22
+ }
23
+ }
24
+
25
+ function logActionExecution(logger, { definition, context, outcome, startedAt, errorCode = null }) {
26
+ if (!logger || typeof logger.debug !== "function") {
27
+ return;
28
+ }
29
+ logger.debug(
30
+ {
31
+ action: definition?.id,
32
+ version: definition?.version,
33
+ channel: context?.channel,
34
+ surface: context?.surface,
35
+ requestId: context?.requestMeta?.requestId || null,
36
+ durationMs: Date.now() - startedAt,
37
+ outcome,
38
+ errorCode
39
+ },
40
+ "action.execution"
41
+ );
42
+ }
43
+
44
+ async function executeActionPipeline({
45
+ definition,
46
+ input,
47
+ context,
48
+ deps = {},
49
+ events,
50
+ logger = console
51
+ } = {}) {
52
+ const normalizedContext = normalizeExecutionContext(context);
53
+ const startedAt = Date.now();
54
+
55
+ try {
56
+ ensureActionChannelAllowed(definition, normalizedContext);
57
+ ensureActionSurfaceAllowed(definition, normalizedContext);
58
+ ensureActionPermissionAllowed(definition, normalizedContext);
59
+
60
+ const normalizedInput = await normalizeActionInput(definition, input);
61
+ ensureIdempotencyKeyIfRequired(definition, normalizedContext);
62
+
63
+ const executionResult = await definition.execute(normalizedInput, normalizedContext, deps);
64
+ const normalizedResult = await normalizeActionOutput(definition, executionResult);
65
+
66
+ await publishActionEvents(events, definition.events, Object.freeze({
67
+ definition,
68
+ input: normalizedInput,
69
+ result: normalizedResult,
70
+ context: normalizedContext
71
+ }));
72
+
73
+ logActionExecution(logger, { definition, context: normalizedContext, outcome: "success", startedAt });
74
+ return normalizedResult;
75
+ } catch (error) {
76
+ logActionExecution(logger, {
77
+ definition,
78
+ context: normalizedContext,
79
+ outcome: "failure",
80
+ startedAt,
81
+ errorCode: String(error?.code || "ACTION_EXECUTION_FAILED").trim()
82
+ });
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ export { executeActionPipeline };
@@ -1,10 +1,7 @@
1
1
  import { createActionRuntimeError } from "./actionDefinitions.js";
2
- import { normalizeLowerText, normalizeText } from "./textNormalization.js";
3
- import { hasPermission, normalizePermissionList } from "../support/permissions.js";
4
- import { isRecord, normalizeOpaqueId } from "../support/normalize.js";
5
- import {
6
- validateSchemaPayload
7
- } from "../validators/schemaPayloadValidation.js";
2
+ import { hasPermission, normalizePermissionList } from "../../shared/support/permissions.js";
3
+ import { isRecord, normalizeLowerText, normalizeOpaqueId } from "../../shared/support/normalize.js";
4
+ import { validateSchemaPayload } from "../../shared/validators/schemaPayloadValidation.js";
8
5
 
9
6
  function createActionValidationError({
10
7
  status = 400,
@@ -22,13 +19,12 @@ function createActionValidationError({
22
19
 
23
20
  function ensureActionChannelAllowed(definition, context) {
24
21
  const channel = normalizeLowerText(context?.channel);
25
- const allowedChannels = Array.isArray(definition?.channels) ? definition.channels : [];
26
22
 
27
- if (!channel || !allowedChannels.includes(channel)) {
23
+ if (!channel || !definition.channels.includes(channel)) {
28
24
  throw createActionRuntimeError(403, "Forbidden.", {
29
25
  code: "ACTION_CHANNEL_FORBIDDEN",
30
26
  details: {
31
- actionId: definition?.id,
27
+ actionId: definition.id,
32
28
  channel
33
29
  }
34
30
  });
@@ -37,13 +33,13 @@ function ensureActionChannelAllowed(definition, context) {
37
33
 
38
34
  function ensureActionSurfaceAllowed(definition, context) {
39
35
  const surface = normalizeLowerText(context?.surface);
40
- const allowedSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
36
+ const allowedSurfaces = definition.surfaces;
41
37
 
42
38
  if (!allowedSurfaces.includes("*") && (!surface || !allowedSurfaces.includes(surface))) {
43
39
  throw createActionRuntimeError(403, "Forbidden.", {
44
40
  code: "ACTION_SURFACE_FORBIDDEN",
45
41
  details: {
46
- actionId: definition?.id,
42
+ actionId: definition.id,
47
43
  surface
48
44
  }
49
45
  });
@@ -51,8 +47,8 @@ function ensureActionSurfaceAllowed(definition, context) {
51
47
  }
52
48
 
53
49
  function ensureActionPermissionAllowed(definition, context) {
54
- const permission = isRecord(definition?.permission) ? definition.permission : { require: "none" };
55
- const mode = normalizeLowerText(permission.require || "none");
50
+ const { permission } = definition;
51
+ const mode = permission.require;
56
52
 
57
53
  if (mode === "none") {
58
54
  return;
@@ -63,7 +59,7 @@ function ensureActionPermissionAllowed(definition, context) {
63
59
  throw createActionRuntimeError(401, permission.message || "Authentication required.", {
64
60
  code: permission.code || "ACTION_AUTHENTICATION_REQUIRED",
65
61
  details: {
66
- actionId: definition?.id
62
+ actionId: definition.id
67
63
  }
68
64
  });
69
65
  }
@@ -72,7 +68,7 @@ function ensureActionPermissionAllowed(definition, context) {
72
68
  return;
73
69
  }
74
70
 
75
- const requiredPermissions = normalizePermissionList(permission.permissions);
71
+ const requiredPermissions = permission.permissions;
76
72
  if (requiredPermissions.length < 1) {
77
73
  return;
78
74
  }
@@ -87,7 +83,7 @@ function ensureActionPermissionAllowed(definition, context) {
87
83
  throw createActionRuntimeError(403, permission.message || "Forbidden.", {
88
84
  code: permission.code || "ACTION_PERMISSION_DENIED",
89
85
  details: {
90
- actionId: definition?.id,
86
+ actionId: definition.id,
91
87
  permission: requiredPermission
92
88
  }
93
89
  });
@@ -101,19 +97,18 @@ function ensureActionPermissionAllowed(definition, context) {
101
97
  throw createActionRuntimeError(403, permission.message || "Forbidden.", {
102
98
  code: permission.code || "ACTION_PERMISSION_DENIED",
103
99
  details: {
104
- actionId: definition?.id,
100
+ actionId: definition.id,
105
101
  requiredPermissions
106
102
  }
107
103
  });
108
104
  }
109
105
  }
110
106
 
111
- async function normalizeActionInput(definition, input, context) {
107
+ async function normalizeActionInput(definition, input) {
112
108
  try {
113
- return validateSchemaPayload(definition?.input, input, {
109
+ return validateSchemaPayload(definition.input, input, {
114
110
  phase: "input",
115
- definition,
116
- context
111
+ context: `Action ${definition.id} input`
117
112
  });
118
113
  } catch (error) {
119
114
  if (isRecord(error?.fieldErrors) || isRecord(error?.details?.fieldErrors)) {
@@ -137,16 +132,15 @@ async function normalizeActionInput(definition, input, context) {
137
132
  }
138
133
  }
139
134
 
140
- async function normalizeActionOutput(definition, output, context) {
141
- if (!definition?.output) {
135
+ async function normalizeActionOutput(definition, output) {
136
+ if (!definition.output) {
142
137
  return output;
143
138
  }
144
139
 
145
140
  try {
146
141
  return validateSchemaPayload(definition.output, output, {
147
142
  phase: "output",
148
- definition,
149
- context
143
+ context: `Action ${definition.id} output`
150
144
  });
151
145
  } catch (error) {
152
146
  if (isRecord(error?.fieldErrors) || isRecord(error?.details?.fieldErrors)) {
@@ -182,17 +176,10 @@ async function normalizeActionOutput(definition, output, context) {
182
176
  }
183
177
  }
184
178
 
185
- const __testables = {
186
- normalizeText,
187
- normalizeLowerText,
188
- validateSchemaPayload
189
- };
190
-
191
179
  export {
192
180
  ensureActionChannelAllowed,
193
181
  ensureActionSurfaceAllowed,
194
182
  ensureActionPermissionAllowed,
195
183
  normalizeActionInput,
196
- normalizeActionOutput,
197
- __testables
184
+ normalizeActionOutput
198
185
  };