@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.
- package/client/appConfig.js +2 -1
- package/client/asyncModuleRecovery.js +2 -4
- package/client/index.d.ts +13 -75
- package/client/index.js +1 -1
- package/client/logging.js +8 -2
- package/client/moduleBootstrap.js +41 -33
- package/client/moduleBootstrap.test.js +35 -0
- package/client/shellBootstrap.js +1 -8
- package/client/shellBootstrap.test.js +32 -5
- package/client/shellRouting.js +16 -61
- package/client/shellRouting.test.js +30 -1
- package/client/vite/clientBootstrapPlugin.js +0 -6
- package/client/vite/clientBootstrapPlugin.test.js +2 -3
- package/internal/node/installedPackages.js +19 -62
- package/internal/node/installedPackages.test.js +33 -0
- package/package.json +2 -4
- package/server/actions/actionCatalogue.js +13 -34
- package/{shared → server}/actions/actionContributorHelpers.js +0 -5
- package/{shared → server}/actions/actionDefinitions.js +24 -131
- package/{shared → server}/actions/actionDefinitions.test.js +75 -21
- package/server/actions/actionEvents.js +97 -2
- package/server/actions/actionEvents.test.js +79 -0
- package/{shared → server}/actions/executionContext.js +3 -13
- package/server/actions/idempotency.js +24 -0
- package/server/actions/pipeline.js +87 -0
- package/{shared → server}/actions/policies.js +20 -33
- package/{shared → server}/actions/policies.test.js +6 -6
- package/{shared → server}/actions/registry.js +3 -22
- package/{shared → server}/actions/registry.test.js +70 -32
- package/server/actions/requestMeta.js +17 -0
- package/server/http/capabilityHttpRuntime.js +1 -1
- package/server/http/capabilityHttpRuntime.test.js +56 -0
- package/server/http/errorHandling.js +291 -0
- package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
- package/server/http/lib/routeRegistration.js +36 -150
- package/server/http/lib/routeTransport.js +2 -0
- package/server/http/lib/routeValidator.js +5 -47
- package/server/http/lib/routeValidator.test.js +15 -17
- package/server/http/lib/router.js +10 -94
- package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
- package/server/platform/providerRuntime/helpers.js +1 -34
- package/server/runtime/integers.js +1 -5
- package/server/runtime/serviceAuthorization.js +4 -9
- package/server/support/appConfig.js +2 -1
- package/server/support/appConfigFiles.js +1 -38
- package/server/support/appConfigFiles.test.js +3 -39
- package/server/support/index.js +1 -20
- package/shared/capabilities/defineProvider.js +1 -1
- package/shared/capabilities/index.js +1 -1
- package/shared/index.js +1 -1
- package/shared/support/index.js +1 -4
- package/shared/support/mobileConfig.js +99 -0
- package/shared/support/mobileConfig.test.js +113 -0
- package/shared/support/normalize.js +0 -108
- package/shared/support/normalize.test.js +0 -134
- package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
- package/shared/support/returnToPath.js +1 -20
- package/shared/support/returnToPath.test.js +1 -36
- package/shared/support/shellLayoutTargets.js +2 -140
- package/shared/support/shellLayoutTargets.test.js +1 -110
- package/shared/support/sorting.js +1 -9
- package/shared/surface/apiPaths.js +1 -82
- package/shared/surface/paths.js +38 -171
- package/shared/surface/registry.js +2 -1
- package/shared/surface/runtime.js +18 -109
- package/shared/surface/runtime.test.js +57 -0
- package/shared/validators/index.js +1 -4
- package/shared/validators/jsonRestSchemaSupport.js +0 -9
- package/shared/validators/recordIdParamsValidator.js +0 -6
- package/shared/validators/schemaDefinitions.js +4 -30
- package/shared/validators/schemaPayloadValidation.js +2 -6
- package/test/barrelExposure.test.js +3 -7
- package/test/exportsContract.test.js +1 -3
- package/client/mobileLaunchRouting.js +0 -231
- package/client/mobileLaunchRouting.test.js +0 -230
- package/server/http/lib/controller.js +0 -183
- package/server/http/lib/controller.test.js +0 -143
- package/server/http/lib/index.js +0 -5
- package/server/runtime/canonicalJson.js +0 -74
- package/server/runtime/entityChangeEvents.js +0 -307
- package/server/runtime/entityChangeEvents.test.js +0 -274
- package/server/runtime/fastifyBootstrap.js +0 -553
- package/server/runtime/moduleConfig.js +0 -223
- package/server/runtime/moduleConfig.test.js +0 -165
- package/server/runtime/pagination.js +0 -13
- package/server/runtime/realtimeNormalization.js +0 -21
- package/server/runtime/routeUtils.js +0 -20
- package/server/runtime/securityAudit.js +0 -269
- package/server/runtime/securityAudit.test.js +0 -41
- package/server/support/pageTargets.js +0 -772
- package/server/support/pageTargets.test.js +0 -659
- package/server/support/shellOutlets.js +0 -576
- package/server/support/shellOutlets.test.js +0 -336
- package/server/surface/index.js +0 -18
- package/shared/actions/audit.js +0 -7
- package/shared/actions/idempotency.js +0 -62
- package/shared/actions/observability.js +0 -10
- package/shared/actions/pipeline.js +0 -309
- package/shared/actions/requestMeta.js +0 -36
- package/shared/actions/textNormalization.js +0 -3
- package/shared/actions/withActionDefaults.js +0 -34
- /package/{shared → server}/actions/executionContext.test.js +0 -0
|
@@ -56,34 +56,34 @@ function createPatchSchema() {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
test("
|
|
60
|
-
const definition = __testables.
|
|
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("
|
|
69
|
+
test("normalizeActionSchema rejects section-map syntax", () => {
|
|
70
70
|
assert.throws(
|
|
71
71
|
() =>
|
|
72
|
-
__testables.
|
|
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("
|
|
83
|
+
test("normalizeActionSchema rejects bare schema instances", () => {
|
|
84
84
|
assert.throws(
|
|
85
85
|
() =>
|
|
86
|
-
__testables.
|
|
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("
|
|
100
|
+
test("normalizeActionSchema rejects validator arrays", () => {
|
|
101
101
|
assert.throws(
|
|
102
102
|
() =>
|
|
103
|
-
__testables.
|
|
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("
|
|
152
|
-
const output = __testables.
|
|
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
|
-
{
|
|
162
|
+
{ defaultMode: "replace" }
|
|
163
163
|
);
|
|
164
164
|
|
|
165
165
|
assertJsonRestSchemaDefinition(output);
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
-
test("
|
|
168
|
+
test("normalizeActionSchema rejects section-map syntax", () => {
|
|
169
169
|
assert.throws(
|
|
170
170
|
() =>
|
|
171
|
-
__testables.
|
|
171
|
+
__testables.normalizeActionSchema(
|
|
172
172
|
{
|
|
173
173
|
payload: createPatchSchema()
|
|
174
174
|
},
|
|
175
175
|
"output",
|
|
176
|
-
{
|
|
176
|
+
{ defaultMode: "replace" }
|
|
177
177
|
),
|
|
178
178
|
/Action definition output must be a schema definition object/
|
|
179
179
|
);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
-
test("
|
|
183
|
-
const definition = __testables.
|
|
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:
|
|
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 "
|
|
3
|
-
import { normalizePermissionList } from "
|
|
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
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 || !
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
55
|
-
const mode =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
100
|
+
actionId: definition.id,
|
|
105
101
|
requiredPermissions
|
|
106
102
|
}
|
|
107
103
|
});
|
|
108
104
|
}
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
async function normalizeActionInput(definition, input
|
|
107
|
+
async function normalizeActionInput(definition, input) {
|
|
112
108
|
try {
|
|
113
|
-
return validateSchemaPayload(definition
|
|
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
|
|
141
|
-
if (!definition
|
|
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
|
};
|