@jskit-ai/kernel 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/_testable/index.js +4 -0
- package/client/appConfig.js +33 -0
- package/client/componentInteraction.js +51 -0
- package/client/componentInteraction.test.js +111 -0
- package/client/descriptorSections.js +75 -0
- package/client/index.d.ts +70 -0
- package/client/index.js +3 -0
- package/client/logging.js +38 -0
- package/client/moduleBootstrap.js +670 -0
- package/client/moduleBootstrap.test.js +403 -0
- package/client/shellBootstrap.js +233 -0
- package/client/shellBootstrap.test.js +185 -0
- package/client/shellRouting.js +321 -0
- package/client/shellRouting.test.js +113 -0
- package/client/vite/clientBootstrapPlugin.js +259 -0
- package/client/vite/clientBootstrapPlugin.test.js +563 -0
- package/client/vite/index.js +3 -0
- package/internal/node/fileSystem.js +21 -0
- package/internal/node/installedPackageDescriptor.js +104 -0
- package/package.json +43 -0
- package/server/actions/ActionRuntimeServiceProvider.js +309 -0
- package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
- package/server/actions/index.js +8 -0
- package/server/container/ContainerCoreServiceProvider.js +27 -0
- package/server/container/index.js +10 -0
- package/server/exportPolicy.test.js +68 -0
- package/server/http/HttpFastifyServiceProvider.js +25 -0
- package/server/http/_testable/index.js +2 -0
- package/server/http/index.js +1 -0
- package/server/http/lib/controller.js +183 -0
- package/server/http/lib/controller.test.js +143 -0
- package/server/http/lib/errors.js +12 -0
- package/server/http/lib/httpRuntime.js +82 -0
- package/server/http/lib/index.js +18 -0
- package/server/http/lib/kernel.js +15 -0
- package/server/http/lib/kernel.test.js +880 -0
- package/server/http/lib/middlewareRuntime.js +149 -0
- package/server/http/lib/requestActionExecutor.js +258 -0
- package/server/http/lib/requestScope.js +59 -0
- package/server/http/lib/routeRegistration.js +165 -0
- package/server/http/lib/routeSupport.js +45 -0
- package/server/http/lib/routeValidator.js +469 -0
- package/server/http/lib/routeValidator.test.js +474 -0
- package/server/http/lib/router.js +206 -0
- package/server/kernel/KernelCoreServiceProvider.js +27 -0
- package/server/kernel/index.js +10 -0
- package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
- package/server/platform/index.js +5 -0
- package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
- package/server/platform/providerRuntime/helpers.js +45 -0
- package/server/platform/providerRuntime/lockfile.js +27 -0
- package/server/platform/providerRuntime/providerLoader.js +283 -0
- package/server/platform/providerRuntime.js +142 -0
- package/server/platform/providerRuntime.test.js +217 -0
- package/server/platform/runtime.js +40 -0
- package/server/platform/surfaceRuntime.js +150 -0
- package/server/platform/surfaceRuntime.test.js +136 -0
- package/server/registries/actionSurfaceSourceRegistry.js +150 -0
- package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
- package/server/registries/domainEventListenerRegistry.js +61 -0
- package/server/registries/index.js +36 -0
- package/server/registries/primitives.js +63 -0
- package/server/registries/routeVisibilityResolverRegistry.js +87 -0
- package/server/registries/serviceRegistrationRegistry.js +431 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
- package/server/runtime/apiRoutePolicyParity.test.js +109 -0
- package/server/runtime/apiRouteRegistration.js +65 -0
- package/server/runtime/bootBootstrapRoutes.js +46 -0
- package/server/runtime/bootBootstrapRoutes.test.js +79 -0
- package/server/runtime/bootstrapContributors.test.js +114 -0
- package/server/runtime/canonicalJson.js +74 -0
- package/server/runtime/composition.js +142 -0
- package/server/runtime/domainEvents.test.js +114 -0
- package/server/runtime/domainRules.js +50 -0
- package/server/runtime/domainRules.test.js +87 -0
- package/server/runtime/entityChangeEvents.js +182 -0
- package/server/runtime/entityChangeEvents.test.js +211 -0
- package/server/runtime/errors.js +68 -0
- package/server/runtime/errors.test.js +73 -0
- package/server/runtime/fastifyBootstrap.js +372 -0
- package/server/runtime/fastifyBootstrap.test.js +194 -0
- package/server/runtime/index.js +6 -0
- package/server/runtime/integers.js +13 -0
- package/server/runtime/moduleConfig.js +269 -0
- package/server/runtime/moduleConfig.test.js +141 -0
- package/server/runtime/pagination.js +13 -0
- package/server/runtime/realtimeNormalization.js +21 -0
- package/server/runtime/requestUrl.js +38 -0
- package/server/runtime/routeUtils.js +20 -0
- package/server/runtime/runtimeAssembly.js +113 -0
- package/server/runtime/runtimeKernel.js +55 -0
- package/server/runtime/securityAudit.js +269 -0
- package/server/runtime/securityAudit.test.js +41 -0
- package/server/runtime/serviceAuthorization.js +113 -0
- package/server/runtime/serviceAuthorization.test.js +100 -0
- package/server/runtime/serviceRegistration.test.js +197 -0
- package/server/support/SupportCoreServiceProvider.js +25 -0
- package/server/support/appConfig.js +37 -0
- package/server/support/appConfig.test.js +94 -0
- package/server/support/defaultMissingHandler.js +7 -0
- package/server/support/index.js +2 -0
- package/server/support/routePolicyConfig.js +51 -0
- package/server/support/symlinkSafeRequire.js +78 -0
- package/server/support/symlinkSafeRequire.test.js +27 -0
- package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
- package/server/surface/index.js +19 -0
- package/shared/actions/actionContributorHelpers.js +34 -0
- package/shared/actions/actionContributorHelpers.test.js +16 -0
- package/shared/actions/actionDefinitions.js +488 -0
- package/shared/actions/actionDefinitions.test.js +212 -0
- package/shared/actions/audit.js +7 -0
- package/shared/actions/executionContext.js +97 -0
- package/shared/actions/executionContext.test.js +66 -0
- package/shared/actions/idempotency.js +62 -0
- package/shared/actions/index.js +2 -0
- package/shared/actions/observability.js +10 -0
- package/shared/actions/pipeline.js +287 -0
- package/shared/actions/policies.js +342 -0
- package/shared/actions/policies.test.js +233 -0
- package/shared/actions/registry.js +187 -0
- package/shared/actions/registry.test.js +381 -0
- package/shared/actions/requestMeta.js +36 -0
- package/shared/actions/textNormalization.js +3 -0
- package/shared/actions/withActionDefaults.js +34 -0
- package/shared/index.js +2 -0
- package/shared/runtime/application.js +323 -0
- package/shared/runtime/container.js +261 -0
- package/shared/runtime/containerErrors.js +22 -0
- package/shared/runtime/index.js +18 -0
- package/shared/runtime/kernelErrors.js +20 -0
- package/shared/runtime/serviceProvider.js +13 -0
- package/shared/support/formatDateTime.js +10 -0
- package/shared/support/formatDateTime.test.js +15 -0
- package/shared/support/index.js +14 -0
- package/shared/support/linkPath.js +67 -0
- package/shared/support/linkPath.test.js +35 -0
- package/shared/support/normalize.js +116 -0
- package/shared/support/normalize.test.js +48 -0
- package/shared/support/packageDescriptor.test.js +121 -0
- package/shared/support/permissions.js +50 -0
- package/shared/support/pickOwnProperties.js +17 -0
- package/shared/support/pickOwnProperties.test.js +25 -0
- package/shared/support/policies.js +11 -0
- package/shared/support/queryPath.js +33 -0
- package/shared/support/queryPath.test.js +19 -0
- package/shared/support/queryResilience.js +34 -0
- package/shared/support/queryResilience.test.js +33 -0
- package/shared/support/returnToPath.js +153 -0
- package/shared/support/returnToPath.test.js +123 -0
- package/shared/support/sorting.js +15 -0
- package/shared/support/tokens.js +23 -0
- package/shared/support/tokens.test.js +17 -0
- package/shared/support/visibility.js +56 -0
- package/shared/support/visibility.test.js +45 -0
- package/shared/surface/apiPaths.js +84 -0
- package/shared/surface/escapeRegExp.js +5 -0
- package/shared/surface/index.js +6 -0
- package/shared/surface/paths.js +273 -0
- package/shared/surface/registry.js +135 -0
- package/shared/surface/registry.test.js +44 -0
- package/shared/surface/runtime.js +357 -0
- package/shared/surface/runtime.test.js +319 -0
- package/shared/validators/createCursorListValidator.js +42 -0
- package/shared/validators/createCursorListValidator.test.js +34 -0
- package/shared/validators/cursorPaginationQueryValidator.js +31 -0
- package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
- package/shared/validators/index.js +12 -0
- package/shared/validators/inputNormalization.js +13 -0
- package/shared/validators/mergeObjectSchemas.js +31 -0
- package/shared/validators/mergeObjectSchemas.test.js +67 -0
- package/shared/validators/mergeValidators.js +89 -0
- package/shared/validators/mergeValidators.test.js +116 -0
- package/shared/validators/nestValidator.js +53 -0
- package/shared/validators/nestValidator.test.js +60 -0
- package/shared/validators/recordIdParamsValidator.js +36 -0
- package/shared/validators/recordIdParamsValidator.test.js +20 -0
- package/shared/validators/resourceRequiredMetadata.js +41 -0
- package/shared/validators/resourceRequiredMetadata.test.js +49 -0
- package/test/barrelExposure.test.js +106 -0
- package/test/dynamicImportPolicy.test.js +89 -0
- package/test/exportsContract.test.js +168 -0
- package/test/routeInputContractGuard.test.js +78 -0
- package/test/surfaceIndependence.test.js +109 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { Check, Errors } from "typebox/value";
|
|
2
|
+
import { createActionRuntimeError } from "./actionDefinitions.js";
|
|
3
|
+
import { normalizeLowerText, normalizeText } from "./textNormalization.js";
|
|
4
|
+
import { hasPermission, normalizePermissionList } from "../support/permissions.js";
|
|
5
|
+
import { isRecord, normalizeOpaqueId } from "../support/normalize.js";
|
|
6
|
+
|
|
7
|
+
function createActionValidationError({
|
|
8
|
+
status = 400,
|
|
9
|
+
message = "Validation failed.",
|
|
10
|
+
code = "ACTION_VALIDATION_FAILED",
|
|
11
|
+
details,
|
|
12
|
+
cause
|
|
13
|
+
} = {}) {
|
|
14
|
+
return createActionRuntimeError(status, message, {
|
|
15
|
+
code,
|
|
16
|
+
details,
|
|
17
|
+
cause
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureActionChannelAllowed(definition, context) {
|
|
22
|
+
const channel = normalizeLowerText(context?.channel);
|
|
23
|
+
const allowedChannels = Array.isArray(definition?.channels) ? definition.channels : [];
|
|
24
|
+
|
|
25
|
+
if (!channel || !allowedChannels.includes(channel)) {
|
|
26
|
+
throw createActionRuntimeError(403, "Forbidden.", {
|
|
27
|
+
code: "ACTION_CHANNEL_FORBIDDEN",
|
|
28
|
+
details: {
|
|
29
|
+
actionId: definition?.id,
|
|
30
|
+
channel
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureActionSurfaceAllowed(definition, context) {
|
|
37
|
+
const surface = normalizeLowerText(context?.surface);
|
|
38
|
+
const allowedSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
|
|
39
|
+
|
|
40
|
+
if (!surface || !allowedSurfaces.includes(surface)) {
|
|
41
|
+
throw createActionRuntimeError(403, "Forbidden.", {
|
|
42
|
+
code: "ACTION_SURFACE_FORBIDDEN",
|
|
43
|
+
details: {
|
|
44
|
+
actionId: definition?.id,
|
|
45
|
+
surface
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureActionPermissionAllowed(definition, context) {
|
|
52
|
+
const permission = isRecord(definition?.permission) ? definition.permission : { require: "none" };
|
|
53
|
+
const mode = normalizeLowerText(permission.require || "none");
|
|
54
|
+
|
|
55
|
+
if (mode === "none") {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const actorId = normalizeOpaqueId(context?.actor?.id);
|
|
60
|
+
if (actorId == null) {
|
|
61
|
+
throw createActionRuntimeError(401, permission.message || "Authentication required.", {
|
|
62
|
+
code: permission.code || "ACTION_AUTHENTICATION_REQUIRED",
|
|
63
|
+
details: {
|
|
64
|
+
actionId: definition?.id
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (mode === "authenticated") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const requiredPermissions = normalizePermissionList(permission.permissions);
|
|
74
|
+
if (requiredPermissions.length < 1) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const actorPermissions = normalizePermissionList(context?.permissions);
|
|
79
|
+
if (mode === "all") {
|
|
80
|
+
for (const requiredPermission of requiredPermissions) {
|
|
81
|
+
if (hasPermission(actorPermissions, requiredPermission)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw createActionRuntimeError(403, permission.message || "Forbidden.", {
|
|
86
|
+
code: permission.code || "ACTION_PERMISSION_DENIED",
|
|
87
|
+
details: {
|
|
88
|
+
actionId: definition?.id,
|
|
89
|
+
permission: requiredPermission
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const hasAnyPermission = requiredPermissions.some((requiredPermission) => hasPermission(actorPermissions, requiredPermission));
|
|
98
|
+
if (!hasAnyPermission) {
|
|
99
|
+
throw createActionRuntimeError(403, permission.message || "Forbidden.", {
|
|
100
|
+
code: permission.code || "ACTION_PERMISSION_DENIED",
|
|
101
|
+
details: {
|
|
102
|
+
actionId: definition?.id,
|
|
103
|
+
requiredPermissions
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeSchemaValidationErrors(schema) {
|
|
110
|
+
const errors = Array.isArray(schema?.errors) ? schema.errors : [];
|
|
111
|
+
if (errors.length < 1) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fieldErrors = {};
|
|
116
|
+
for (const entry of errors) {
|
|
117
|
+
const rawFieldPath = normalizeText(entry?.path || entry?.instancePath || entry?.field || "");
|
|
118
|
+
const fieldPath = rawFieldPath
|
|
119
|
+
? rawFieldPath.replace(/^\//, "").replace(/\//g, ".")
|
|
120
|
+
: "input";
|
|
121
|
+
const message = normalizeText(entry?.message || "Invalid value.") || "Invalid value.";
|
|
122
|
+
fieldErrors[fieldPath] = message;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Object.keys(fieldErrors).length > 0 ? fieldErrors : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildSchemaValidatorError({ phase, definition } = {}) {
|
|
129
|
+
return createActionValidationError({
|
|
130
|
+
details: {
|
|
131
|
+
error: "Schema validator must return { ok, value, errors } or throw.",
|
|
132
|
+
phase,
|
|
133
|
+
actionId: definition?.id,
|
|
134
|
+
version: definition?.version
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeTypeBoxValidationErrors(schema, payload) {
|
|
140
|
+
const issues = Check(schema, payload) ? [] : [...Errors(schema, payload)];
|
|
141
|
+
if (issues.length < 1) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return normalizeSchemaValidationErrors({
|
|
146
|
+
errors: issues
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeFunctionSchemaResult(result, payload, { phase, definition } = {}) {
|
|
151
|
+
if (!isRecord(result) || typeof result.ok !== "boolean") {
|
|
152
|
+
throw buildSchemaValidatorError({ phase, definition });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (result.ok) {
|
|
156
|
+
if (Object.hasOwn(result, "value")) {
|
|
157
|
+
return result.value;
|
|
158
|
+
}
|
|
159
|
+
return payload;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const details = {};
|
|
163
|
+
if (Object.hasOwn(result, "errors")) {
|
|
164
|
+
if (Array.isArray(result.errors)) {
|
|
165
|
+
const fieldErrors = normalizeSchemaValidationErrors({ errors: result.errors });
|
|
166
|
+
if (fieldErrors) {
|
|
167
|
+
details.fieldErrors = fieldErrors;
|
|
168
|
+
} else {
|
|
169
|
+
details.errors = result.errors;
|
|
170
|
+
}
|
|
171
|
+
} else if (result.errors && typeof result.errors === "object") {
|
|
172
|
+
details.fieldErrors = result.errors;
|
|
173
|
+
} else if (result.errors != null) {
|
|
174
|
+
details.error = String(result.errors);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw createActionValidationError({
|
|
179
|
+
details: Object.keys(details).length > 0 ? details : undefined
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function normalizeValidatorPayload(validator, payload, { phase, definition, context }) {
|
|
184
|
+
if (!validator || typeof validator !== "object") {
|
|
185
|
+
return payload;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (typeof validator.normalize !== "function") {
|
|
189
|
+
return payload;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return await validator.normalize(payload, {
|
|
193
|
+
phase,
|
|
194
|
+
actionId: definition?.id,
|
|
195
|
+
version: definition?.version,
|
|
196
|
+
context
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function validateSchemaPayload(schema, payload, { phase, definition }) {
|
|
201
|
+
if (schema == null) {
|
|
202
|
+
return payload;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (typeof schema === "function") {
|
|
206
|
+
const result = await schema(payload, {
|
|
207
|
+
phase,
|
|
208
|
+
actionId: definition?.id,
|
|
209
|
+
version: definition?.version
|
|
210
|
+
});
|
|
211
|
+
return normalizeFunctionSchemaResult(result, payload, { phase, definition });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!isRecord(schema)) {
|
|
215
|
+
throw buildSchemaValidatorError({ phase, definition });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof schema.parse === "function") {
|
|
219
|
+
return schema.parse(payload);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (typeof schema.assert === "function") {
|
|
223
|
+
const assertionResult = await schema.assert(payload);
|
|
224
|
+
return assertionResult == null ? payload : assertionResult;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (typeof schema.check === "function") {
|
|
228
|
+
const valid = await schema.check(payload);
|
|
229
|
+
if (!valid) {
|
|
230
|
+
throw createActionValidationError();
|
|
231
|
+
}
|
|
232
|
+
return payload;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (typeof schema.validate === "function") {
|
|
236
|
+
const valid = await schema.validate(payload);
|
|
237
|
+
if (!valid) {
|
|
238
|
+
throw createActionValidationError({
|
|
239
|
+
details: {
|
|
240
|
+
fieldErrors: normalizeSchemaValidationErrors(schema)
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return payload;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const fieldErrors = normalizeTypeBoxValidationErrors(schema, payload);
|
|
248
|
+
if (!fieldErrors) {
|
|
249
|
+
return payload;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
throw createActionValidationError({
|
|
253
|
+
details: {
|
|
254
|
+
fieldErrors
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function normalizeActionInput(definition, input, context) {
|
|
260
|
+
try {
|
|
261
|
+
const normalizedInput = await normalizeValidatorPayload(definition?.inputValidator, input, {
|
|
262
|
+
phase: "input",
|
|
263
|
+
definition,
|
|
264
|
+
context
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return await validateSchemaPayload(definition?.inputValidator?.schema, normalizedInput, {
|
|
268
|
+
phase: "input",
|
|
269
|
+
definition,
|
|
270
|
+
context
|
|
271
|
+
});
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error?.code === "ACTION_VALIDATION_FAILED") {
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
throw createActionValidationError({
|
|
278
|
+
details: {
|
|
279
|
+
error: String(error?.message || "Invalid input.")
|
|
280
|
+
},
|
|
281
|
+
cause: error
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function normalizeActionOutput(definition, output, context) {
|
|
287
|
+
if (!definition?.outputValidator) {
|
|
288
|
+
return output;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const normalizedOutput = await normalizeValidatorPayload(definition.outputValidator, output, {
|
|
293
|
+
phase: "output",
|
|
294
|
+
definition,
|
|
295
|
+
context
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return await validateSchemaPayload(definition.outputValidator.schema, normalizedOutput, {
|
|
299
|
+
phase: "output",
|
|
300
|
+
definition,
|
|
301
|
+
context
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error?.code === "ACTION_VALIDATION_FAILED") {
|
|
305
|
+
throw createActionValidationError({
|
|
306
|
+
status: 500,
|
|
307
|
+
message: "Action output validation failed.",
|
|
308
|
+
code: "ACTION_OUTPUT_VALIDATION_FAILED",
|
|
309
|
+
details: error.details,
|
|
310
|
+
cause: error
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
throw createActionValidationError({
|
|
315
|
+
status: 500,
|
|
316
|
+
message: "Action output validation failed.",
|
|
317
|
+
code: "ACTION_OUTPUT_VALIDATION_FAILED",
|
|
318
|
+
details: {
|
|
319
|
+
error: String(error?.message || "Invalid output.")
|
|
320
|
+
},
|
|
321
|
+
cause: error
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const __testables = {
|
|
327
|
+
normalizeText,
|
|
328
|
+
normalizeLowerText,
|
|
329
|
+
normalizeSchemaValidationErrors,
|
|
330
|
+
normalizeTypeBoxValidationErrors,
|
|
331
|
+
normalizeValidatorPayload,
|
|
332
|
+
validateSchemaPayload
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
export {
|
|
336
|
+
ensureActionChannelAllowed,
|
|
337
|
+
ensureActionSurfaceAllowed,
|
|
338
|
+
ensureActionPermissionAllowed,
|
|
339
|
+
normalizeActionInput,
|
|
340
|
+
normalizeActionOutput,
|
|
341
|
+
__testables
|
|
342
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
import { ensureActionPermissionAllowed, normalizeActionInput, normalizeActionOutput } from "./policies.js";
|
|
6
|
+
|
|
7
|
+
test("function schema returns normalized value when ok", async () => {
|
|
8
|
+
const definition = {
|
|
9
|
+
id: "tests.ok",
|
|
10
|
+
version: 1,
|
|
11
|
+
inputValidator: {
|
|
12
|
+
schema: () => ({
|
|
13
|
+
ok: true,
|
|
14
|
+
value: {
|
|
15
|
+
normalized: true
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const result = await normalizeActionInput(definition, { raw: true }, {});
|
|
22
|
+
assert.deepEqual(result, { normalized: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("function schema rejects non-validator results", async () => {
|
|
26
|
+
const definition = {
|
|
27
|
+
id: "tests.invalid",
|
|
28
|
+
version: 1,
|
|
29
|
+
inputValidator: {
|
|
30
|
+
schema: () => false
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
await assert.rejects(
|
|
35
|
+
() => normalizeActionInput(definition, { raw: true }, {}),
|
|
36
|
+
(error) => {
|
|
37
|
+
assert.equal(error.code, "ACTION_VALIDATION_FAILED");
|
|
38
|
+
assert.match(error.details?.error || "", /Schema validator must return/);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("function schema propagates validation errors", async () => {
|
|
45
|
+
const definition = {
|
|
46
|
+
id: "tests.errors",
|
|
47
|
+
version: 2,
|
|
48
|
+
inputValidator: {
|
|
49
|
+
schema: () => ({
|
|
50
|
+
ok: false,
|
|
51
|
+
errors: {
|
|
52
|
+
input: "input is required"
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
await assert.rejects(
|
|
59
|
+
() => normalizeActionInput(definition, null, {}),
|
|
60
|
+
(error) => {
|
|
61
|
+
assert.equal(error.code, "ACTION_VALIDATION_FAILED");
|
|
62
|
+
assert.deepEqual(error.details?.fieldErrors, {
|
|
63
|
+
input: "input is required"
|
|
64
|
+
});
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("raw TypeBox action schemas validate normalized action input", async () => {
|
|
71
|
+
const definition = {
|
|
72
|
+
id: "tests.typebox",
|
|
73
|
+
version: 1,
|
|
74
|
+
inputValidator: {
|
|
75
|
+
schema: Type.Object(
|
|
76
|
+
{
|
|
77
|
+
workspaceSlug: Type.String({ minLength: 1 })
|
|
78
|
+
},
|
|
79
|
+
{ additionalProperties: false }
|
|
80
|
+
),
|
|
81
|
+
normalize(value = {}) {
|
|
82
|
+
return {
|
|
83
|
+
workspaceSlug: String(value.workspaceSlug || "").trim().toLowerCase()
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const result = await normalizeActionInput(definition, { workspaceSlug: " ACME " }, {});
|
|
90
|
+
assert.deepEqual(result, { workspaceSlug: "acme" });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("typebox input validation normalizes pointer field errors to plain keys", async () => {
|
|
94
|
+
const definition = {
|
|
95
|
+
id: "tests.typebox.errors",
|
|
96
|
+
version: 1,
|
|
97
|
+
inputValidator: {
|
|
98
|
+
schema: Type.Object(
|
|
99
|
+
{
|
|
100
|
+
name: Type.String({ maxLength: 1 })
|
|
101
|
+
},
|
|
102
|
+
{ additionalProperties: false }
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
await assert.rejects(
|
|
108
|
+
() => normalizeActionInput(definition, { name: "too long" }, {}),
|
|
109
|
+
(error) => {
|
|
110
|
+
const fieldErrors = error.details?.fieldErrors || {};
|
|
111
|
+
assert.equal(typeof fieldErrors.name, "string");
|
|
112
|
+
assert.equal(Object.hasOwn(fieldErrors, "/name"), false);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("action output normalization runs before output validation", async () => {
|
|
119
|
+
const definition = {
|
|
120
|
+
id: "tests.output",
|
|
121
|
+
version: 1,
|
|
122
|
+
outputValidator: {
|
|
123
|
+
schema: Type.Object(
|
|
124
|
+
{
|
|
125
|
+
ok: Type.Boolean()
|
|
126
|
+
},
|
|
127
|
+
{ additionalProperties: false }
|
|
128
|
+
),
|
|
129
|
+
normalize(payload = {}) {
|
|
130
|
+
return {
|
|
131
|
+
ok: Boolean(payload.ok)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = await normalizeActionOutput(definition, { ok: 1 }, {});
|
|
138
|
+
assert.deepEqual(result, { ok: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("action permission denies unauthenticated access when required", () => {
|
|
142
|
+
assert.throws(
|
|
143
|
+
() =>
|
|
144
|
+
ensureActionPermissionAllowed(
|
|
145
|
+
{
|
|
146
|
+
id: "tests.secure",
|
|
147
|
+
permission: {
|
|
148
|
+
require: "authenticated"
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
permissions: []
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
(error) => error?.statusCode === 401 && error?.code === "ACTION_AUTHENTICATION_REQUIRED"
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("action permission enforces required permissions", () => {
|
|
160
|
+
assert.throws(
|
|
161
|
+
() =>
|
|
162
|
+
ensureActionPermissionAllowed(
|
|
163
|
+
{
|
|
164
|
+
id: "tests.secure.perm",
|
|
165
|
+
permission: {
|
|
166
|
+
require: "all",
|
|
167
|
+
permissions: ["workspace.settings.update"]
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
actor: {
|
|
172
|
+
id: 7
|
|
173
|
+
},
|
|
174
|
+
permissions: ["workspace.settings.view"]
|
|
175
|
+
}
|
|
176
|
+
),
|
|
177
|
+
(error) => error?.statusCode === 403 && error?.code === "ACTION_PERMISSION_DENIED"
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
assert.doesNotThrow(() =>
|
|
181
|
+
ensureActionPermissionAllowed(
|
|
182
|
+
{
|
|
183
|
+
id: "tests.secure.perm",
|
|
184
|
+
permission: {
|
|
185
|
+
require: "all",
|
|
186
|
+
permissions: ["workspace.settings.update"]
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
actor: {
|
|
191
|
+
id: 7
|
|
192
|
+
},
|
|
193
|
+
permissions: ["workspace.settings.update"]
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
assert.doesNotThrow(() =>
|
|
199
|
+
ensureActionPermissionAllowed(
|
|
200
|
+
{
|
|
201
|
+
id: "tests.secure.perm",
|
|
202
|
+
permission: {
|
|
203
|
+
require: "all",
|
|
204
|
+
permissions: ["workspace.settings.update"]
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
actor: {
|
|
209
|
+
id: 7
|
|
210
|
+
},
|
|
211
|
+
permissions: ["workspace.settings.*"]
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
assert.doesNotThrow(() =>
|
|
217
|
+
ensureActionPermissionAllowed(
|
|
218
|
+
{
|
|
219
|
+
id: "tests.secure.perm",
|
|
220
|
+
permission: {
|
|
221
|
+
require: "all",
|
|
222
|
+
permissions: ["workspace.settings.update"]
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
actor: {
|
|
227
|
+
id: "user-7"
|
|
228
|
+
},
|
|
229
|
+
permissions: ["workspace.settings.update"]
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
});
|