@jskit-ai/kernel 0.1.245 → 0.1.246
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 +14 -58
- 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
|
@@ -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
|
};
|
|
@@ -20,7 +20,7 @@ test("plain json-rest-schema action schemas validate input without reshaping it"
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const result = await normalizeActionInput(definition, { workspaceSlug: " ACME " }
|
|
23
|
+
const result = await normalizeActionInput(definition, { workspaceSlug: " ACME " });
|
|
24
24
|
assert.deepEqual(result, { workspaceSlug: "ACME" });
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -41,7 +41,7 @@ test("json-rest-schema input validation surfaces plain field keys", async () =>
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
await assert.rejects(
|
|
44
|
-
() => normalizeActionInput(definition, { name: "too long" }
|
|
44
|
+
() => normalizeActionInput(definition, { name: "too long" }),
|
|
45
45
|
(error) => {
|
|
46
46
|
const fieldErrors = error.details?.fieldErrors || {};
|
|
47
47
|
assert.equal(typeof fieldErrors.name, "string");
|
|
@@ -67,11 +67,11 @@ test("action output validation preserves valid json-rest-schema output", async (
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
await assert.rejects(
|
|
70
|
-
() => normalizeActionOutput(definition, { ok: 2 }
|
|
70
|
+
() => normalizeActionOutput(definition, { ok: 2 }),
|
|
71
71
|
(error) => error?.code === "ACTION_OUTPUT_VALIDATION_FAILED"
|
|
72
72
|
);
|
|
73
73
|
|
|
74
|
-
const result = await normalizeActionOutput(definition, { ok: true }
|
|
74
|
+
const result = await normalizeActionOutput(definition, { ok: true });
|
|
75
75
|
assert.deepEqual(result, { ok: true });
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -94,7 +94,7 @@ test("json-rest-schema action validators normalize action input", async () => {
|
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
const result = await normalizeActionInput(definition, { name: " Acme " }
|
|
97
|
+
const result = await normalizeActionInput(definition, { name: " Acme " });
|
|
98
98
|
assert.deepEqual(result, { name: "Acme" });
|
|
99
99
|
});
|
|
100
100
|
|
|
@@ -118,7 +118,7 @@ test("json-rest-schema action validators surface field errors", async () => {
|
|
|
118
118
|
};
|
|
119
119
|
|
|
120
120
|
await assert.rejects(
|
|
121
|
-
() => normalizeActionInput(definition, { name: " " }
|
|
121
|
+
() => normalizeActionInput(definition, { name: " " }),
|
|
122
122
|
(error) => {
|
|
123
123
|
assert.equal(error.code, "ACTION_VALIDATION_FAILED");
|
|
124
124
|
assert.equal(error.details?.fieldErrors?.name, "Name is required.");
|
|
@@ -111,9 +111,6 @@ function resolveActionDefinition(index, actionId, version) {
|
|
|
111
111
|
|
|
112
112
|
function createActionRegistry({
|
|
113
113
|
contributors = [],
|
|
114
|
-
idempotencyAdapter,
|
|
115
|
-
auditAdapter,
|
|
116
|
-
observabilityAdapter,
|
|
117
114
|
events,
|
|
118
115
|
logger = console
|
|
119
116
|
} = {}) {
|
|
@@ -122,19 +119,14 @@ function createActionRegistry({
|
|
|
122
119
|
|
|
123
120
|
async function execute({ actionId, version = null, input = {}, context = {}, deps = {} } = {}) {
|
|
124
121
|
const definition = resolveActionDefinition(index, actionId, version);
|
|
125
|
-
|
|
122
|
+
return executeActionPipeline({
|
|
126
123
|
definition,
|
|
127
124
|
input,
|
|
128
125
|
context,
|
|
129
126
|
deps,
|
|
130
|
-
idempotencyAdapter,
|
|
131
|
-
auditAdapter,
|
|
132
|
-
observabilityAdapter,
|
|
133
127
|
events,
|
|
134
128
|
logger
|
|
135
129
|
});
|
|
136
|
-
|
|
137
|
-
return execution.result;
|
|
138
130
|
}
|
|
139
131
|
|
|
140
132
|
async function executeStream({ actionId, version = null, input = {}, context = {}, deps = {} } = {}) {
|
|
@@ -150,19 +142,14 @@ function createActionRegistry({
|
|
|
150
142
|
});
|
|
151
143
|
}
|
|
152
144
|
|
|
153
|
-
|
|
145
|
+
return executeActionPipeline({
|
|
154
146
|
definition,
|
|
155
147
|
input,
|
|
156
148
|
context,
|
|
157
149
|
deps,
|
|
158
|
-
idempotencyAdapter,
|
|
159
|
-
auditAdapter,
|
|
160
|
-
observabilityAdapter,
|
|
161
150
|
events,
|
|
162
151
|
logger
|
|
163
152
|
});
|
|
164
|
-
|
|
165
|
-
return execution.result;
|
|
166
153
|
}
|
|
167
154
|
|
|
168
155
|
function listDefinitions() {
|
|
@@ -181,10 +168,4 @@ function createActionRegistry({
|
|
|
181
168
|
});
|
|
182
169
|
}
|
|
183
170
|
|
|
184
|
-
|
|
185
|
-
normalizeContributors,
|
|
186
|
-
buildDefinitionIndex,
|
|
187
|
-
resolveActionDefinition
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
export { createActionRegistry, __testables };
|
|
171
|
+
export { createActionRegistry };
|