@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,488 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { mergeValidators } from "../validators/mergeValidators.js";
|
|
3
|
+
import { normalizeObjectInput } from "../validators/inputNormalization.js";
|
|
4
|
+
import { isRecord as isPlainObject, normalizePositiveInteger } from "../support/normalize.js";
|
|
5
|
+
import { normalizePermissionList } from "../support/permissions.js";
|
|
6
|
+
import { normalizeText } from "./textNormalization.js";
|
|
7
|
+
|
|
8
|
+
const ACTION_KINDS = Object.freeze(["query", "command", "stream"]);
|
|
9
|
+
const ACTION_IDEMPOTENCY_POLICIES = Object.freeze(["none", "optional", "required", "domain_native"]);
|
|
10
|
+
const ACTION_PERMISSION_REQUIRE_MODES = Object.freeze(["none", "authenticated", "all", "any"]);
|
|
11
|
+
|
|
12
|
+
const ACTION_KIND_SET = new Set(ACTION_KINDS);
|
|
13
|
+
const ACTION_IDEMPOTENCY_SET = new Set(ACTION_IDEMPOTENCY_POLICIES);
|
|
14
|
+
const ACTION_PERMISSION_REQUIRE_SET = new Set(ACTION_PERMISSION_REQUIRE_MODES);
|
|
15
|
+
const ACTION_DOMAIN_PATTERN = /^[a-z][a-z0-9_.-]*$/;
|
|
16
|
+
|
|
17
|
+
class ActionRuntimeError extends Error {
|
|
18
|
+
constructor(status, message, options = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ActionRuntimeError";
|
|
21
|
+
this.status = Number(status) || 500;
|
|
22
|
+
this.statusCode = this.status;
|
|
23
|
+
this.code = String(options.code || "ACTION_RUNTIME_ERROR");
|
|
24
|
+
this.details = options.details;
|
|
25
|
+
this.headers = options.headers || {};
|
|
26
|
+
this.cause = options.cause;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createActionRuntimeError(status, message, options = {}) {
|
|
31
|
+
return new ActionRuntimeError(status, message, options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeActionDomain(value, { context = "domain", errorCode = "ACTION_DEFINITION_INVALID" } = {}) {
|
|
35
|
+
const domain = normalizeText(value).toLowerCase();
|
|
36
|
+
if (!domain) {
|
|
37
|
+
throw createActionRuntimeError(500, `${context} is required.`, {
|
|
38
|
+
code: errorCode
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (!ACTION_DOMAIN_PATTERN.test(domain)) {
|
|
42
|
+
throw createActionRuntimeError(
|
|
43
|
+
500,
|
|
44
|
+
`${context} must match ${ACTION_DOMAIN_PATTERN.toString()} (lowercase letters, digits, _, -, .).`,
|
|
45
|
+
{
|
|
46
|
+
code: errorCode
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return domain;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeStringArray(value, { fieldName, allowedSet, allowEmpty = false } = {}) {
|
|
55
|
+
const source = Array.isArray(value) ? value : [];
|
|
56
|
+
const normalized = Array.from(
|
|
57
|
+
new Set(
|
|
58
|
+
source
|
|
59
|
+
.map((entry) => normalizeText(entry).toLowerCase())
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.filter((entry) => {
|
|
62
|
+
if (!(allowedSet instanceof Set)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return allowedSet.has(entry);
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!allowEmpty && normalized.length < 1) {
|
|
71
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName} must include at least one value.`, {
|
|
72
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeSingleActionValidator(value, fieldName, { required = false } = {}) {
|
|
80
|
+
if (value == null) {
|
|
81
|
+
if (!required) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName} is required.`, {
|
|
86
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!isPlainObject(value)) {
|
|
91
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName} must be an object.`, {
|
|
92
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Object.prototype.hasOwnProperty.call(value, "schema")) {
|
|
97
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName}.schema is required.`, {
|
|
98
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
value.schema == null ||
|
|
104
|
+
(typeof value.schema !== "function" && (typeof value.schema !== "object" || Array.isArray(value.schema)))
|
|
105
|
+
) {
|
|
106
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName}.schema must be a function or object.`, {
|
|
107
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (Object.prototype.hasOwnProperty.call(value, "normalize")) {
|
|
112
|
+
if (value.normalize != null && typeof value.normalize !== "function") {
|
|
113
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName}.normalize must be a function.`, {
|
|
114
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
schema: value.schema,
|
|
121
|
+
...(typeof value.normalize === "function" ? { normalize: value.normalize } : {})
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isActionValidatorShape(value) {
|
|
126
|
+
return (
|
|
127
|
+
isPlainObject(value) &&
|
|
128
|
+
(Object.prototype.hasOwnProperty.call(value, "schema") || Object.prototype.hasOwnProperty.call(value, "normalize"))
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeSectionActionValidatorMap(value, fieldName) {
|
|
133
|
+
if (!isPlainObject(value) || isActionValidatorShape(value)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const entries = Object.entries(value);
|
|
138
|
+
if (entries.length < 1) {
|
|
139
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName} must define at least one section validator.`, {
|
|
140
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const schemaProperties = {};
|
|
145
|
+
const sectionNormalizers = [];
|
|
146
|
+
|
|
147
|
+
for (const [rawKey, rawValidator] of entries) {
|
|
148
|
+
const sectionKey = normalizeText(rawKey);
|
|
149
|
+
if (!sectionKey) {
|
|
150
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName} section keys must be non-empty strings.`, {
|
|
151
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sectionValidator = normalizeSingleActionValidator(rawValidator, `${fieldName}.${sectionKey}`, {
|
|
156
|
+
required: true
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
schemaProperties[sectionKey] = sectionValidator.schema;
|
|
160
|
+
sectionNormalizers.push({
|
|
161
|
+
key: sectionKey,
|
|
162
|
+
normalize: typeof sectionValidator.normalize === "function" ? sectionValidator.normalize : null
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
schema: Type.Object(schemaProperties, {
|
|
168
|
+
additionalProperties: false
|
|
169
|
+
}),
|
|
170
|
+
async normalize(payload, meta) {
|
|
171
|
+
const source = normalizeObjectInput(payload);
|
|
172
|
+
const normalized = {};
|
|
173
|
+
|
|
174
|
+
for (const section of sectionNormalizers) {
|
|
175
|
+
if (!Object.hasOwn(source, section.key)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sectionPayload = source[section.key];
|
|
180
|
+
normalized[section.key] = section.normalize ? await section.normalize(sectionPayload, meta) : sectionPayload;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return normalized;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function mergeNormalizedActionValidators(validators, fieldName) {
|
|
189
|
+
return mergeValidators(validators, {
|
|
190
|
+
context: `Action definition ${fieldName}`,
|
|
191
|
+
requireSchema: true,
|
|
192
|
+
requiredSchemaMessage: `Action definition ${fieldName}.schema is required.`,
|
|
193
|
+
normalizeResultMessage: `Action definition ${fieldName}.normalize must return an object.`,
|
|
194
|
+
createError(message) {
|
|
195
|
+
return createActionRuntimeError(500, message, {
|
|
196
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeActionValidators(value, fieldName, { required = false } = {}) {
|
|
203
|
+
if (value == null) {
|
|
204
|
+
if (!required) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName} is required.`, {
|
|
209
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const validatorsSource = Array.isArray(value) ? value : [value];
|
|
214
|
+
|
|
215
|
+
if (validatorsSource.length < 1) {
|
|
216
|
+
throw createActionRuntimeError(500, `Action definition ${fieldName} is required.`, {
|
|
217
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const validators = validatorsSource.map((entry, index) => {
|
|
222
|
+
const contextFieldName = `${fieldName}[${index}]`;
|
|
223
|
+
const sectionMapValidator = normalizeSectionActionValidatorMap(entry, contextFieldName);
|
|
224
|
+
if (sectionMapValidator) {
|
|
225
|
+
return sectionMapValidator;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const validator = normalizeSingleActionValidator(entry, contextFieldName, {
|
|
229
|
+
required: true
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!validator) {
|
|
233
|
+
throw createActionRuntimeError(500, `Action definition ${contextFieldName} is required.`, {
|
|
234
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return validator;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return mergeNormalizedActionValidators(validators, fieldName);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeActionOutputValidator(value, fieldName, { required = false } = {}) {
|
|
245
|
+
return normalizeActionValidators(value, fieldName, {
|
|
246
|
+
required
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function normalizeActionPermission(permission, actionId) {
|
|
251
|
+
if (permission == null) {
|
|
252
|
+
return Object.freeze({
|
|
253
|
+
require: "none",
|
|
254
|
+
permissions: Object.freeze([]),
|
|
255
|
+
message: "",
|
|
256
|
+
code: ""
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!isPlainObject(permission)) {
|
|
261
|
+
throw createActionRuntimeError(
|
|
262
|
+
500,
|
|
263
|
+
`Action definition \"${actionId}\" permission must be an object when provided.`,
|
|
264
|
+
{
|
|
265
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const requireMode = normalizeText(permission.require || "authenticated").toLowerCase();
|
|
271
|
+
if (!ACTION_PERMISSION_REQUIRE_SET.has(requireMode)) {
|
|
272
|
+
throw createActionRuntimeError(
|
|
273
|
+
500,
|
|
274
|
+
`Action definition \"${actionId}\" permission.require must be one of: ${ACTION_PERMISSION_REQUIRE_MODES.join(", ")}.`,
|
|
275
|
+
{
|
|
276
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return Object.freeze({
|
|
282
|
+
require: requireMode,
|
|
283
|
+
permissions: Object.freeze(normalizePermissionList(permission.permissions)),
|
|
284
|
+
message: normalizeText(permission.message),
|
|
285
|
+
code: normalizeText(permission.code)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeAuditConfig(audit, { actionId }) {
|
|
290
|
+
const source = audit && typeof audit === "object" ? audit : {};
|
|
291
|
+
const actionName = normalizeText(source.actionName || actionId);
|
|
292
|
+
const metadataBuilder = source.metadataBuilder;
|
|
293
|
+
const piiTags = normalizeStringArray(source.piiTags, {
|
|
294
|
+
fieldName: "audit.piiTags",
|
|
295
|
+
allowEmpty: true
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (metadataBuilder != null && typeof metadataBuilder !== "function") {
|
|
299
|
+
throw createActionRuntimeError(500, "Action definition audit.metadataBuilder must be a function when provided.", {
|
|
300
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return Object.freeze({
|
|
305
|
+
actionName,
|
|
306
|
+
metadataBuilder: typeof metadataBuilder === "function" ? metadataBuilder : null,
|
|
307
|
+
piiTags
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function normalizeObservabilityConfig(observability) {
|
|
312
|
+
const source = observability && typeof observability === "object" ? observability : {};
|
|
313
|
+
const metricTags = normalizeStringArray(source.metricTags, {
|
|
314
|
+
fieldName: "observability.metricTags",
|
|
315
|
+
allowEmpty: true
|
|
316
|
+
});
|
|
317
|
+
const sampleRate = Number(source.sampleRate);
|
|
318
|
+
|
|
319
|
+
return Object.freeze({
|
|
320
|
+
metricTags,
|
|
321
|
+
sampleRate: Number.isFinite(sampleRate) && sampleRate >= 0 && sampleRate <= 1 ? sampleRate : null
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeActionExtensions(value) {
|
|
326
|
+
if (value == null) {
|
|
327
|
+
return Object.freeze({});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!isPlainObject(value)) {
|
|
331
|
+
throw createActionRuntimeError(500, "Action definition extensions must be an object when provided.", {
|
|
332
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return Object.freeze(normalizeObjectInput(value));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeActionDefinition(definition, { contributorId = "", contributorDomain = "" } = {}) {
|
|
340
|
+
const source = definition && typeof definition === "object" ? definition : {};
|
|
341
|
+
|
|
342
|
+
const id = normalizeText(source.id);
|
|
343
|
+
if (!id) {
|
|
344
|
+
throw createActionRuntimeError(500, "Action definition id is required.", {
|
|
345
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const version = normalizePositiveInteger(source.version, {
|
|
350
|
+
fallback: 1
|
|
351
|
+
});
|
|
352
|
+
const domain = normalizeActionDomain(source.domain || contributorDomain, {
|
|
353
|
+
context: `Action definition \"${id}\" domain`,
|
|
354
|
+
errorCode: "ACTION_DEFINITION_INVALID"
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const kind = normalizeText(source.kind || "command").toLowerCase();
|
|
358
|
+
if (!ACTION_KIND_SET.has(kind)) {
|
|
359
|
+
throw createActionRuntimeError(500, `Action definition \"${id}\" has unsupported kind \"${kind}\".`, {
|
|
360
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const channels = normalizeStringArray(source.channels, {
|
|
365
|
+
fieldName: "channels"
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const surfaces = normalizeStringArray(source.surfaces, {
|
|
369
|
+
fieldName: "surfaces"
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (Object.prototype.hasOwnProperty.call(source, "visibility")) {
|
|
373
|
+
throw createActionRuntimeError(500, `Action definition \"${id}\" visibility is not supported.`, {
|
|
374
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const idempotency = normalizeText(source.idempotency || "none").toLowerCase();
|
|
379
|
+
if (!ACTION_IDEMPOTENCY_SET.has(idempotency)) {
|
|
380
|
+
throw createActionRuntimeError(
|
|
381
|
+
500,
|
|
382
|
+
`Action definition \"${id}\" has unsupported idempotency policy \"${idempotency}\".`,
|
|
383
|
+
{
|
|
384
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (typeof source.execute !== "function") {
|
|
390
|
+
throw createActionRuntimeError(500, `Action definition \"${id}\" execute handler is required.`, {
|
|
391
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (Object.prototype.hasOwnProperty.call(source, "input")) {
|
|
396
|
+
throw createActionRuntimeError(500, `Action definition \"${id}\" must use inputValidator instead of input.`, {
|
|
397
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (Object.prototype.hasOwnProperty.call(source, "output")) {
|
|
402
|
+
throw createActionRuntimeError(500, `Action definition \"${id}\" must use outputValidator instead of output.`, {
|
|
403
|
+
code: "ACTION_DEFINITION_INVALID"
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return Object.freeze({
|
|
408
|
+
id,
|
|
409
|
+
version,
|
|
410
|
+
domain,
|
|
411
|
+
kind,
|
|
412
|
+
channels,
|
|
413
|
+
surfaces,
|
|
414
|
+
inputValidator: normalizeActionValidators(source.inputValidator, "inputValidator", {
|
|
415
|
+
required: true
|
|
416
|
+
}),
|
|
417
|
+
outputValidator: normalizeActionOutputValidator(source.outputValidator, "outputValidator", {
|
|
418
|
+
required: false
|
|
419
|
+
}),
|
|
420
|
+
idempotency,
|
|
421
|
+
permission: normalizeActionPermission(source.permission, id),
|
|
422
|
+
audit: normalizeAuditConfig(source.audit, {
|
|
423
|
+
actionId: id
|
|
424
|
+
}),
|
|
425
|
+
observability: normalizeObservabilityConfig(source.observability),
|
|
426
|
+
extensions: normalizeActionExtensions(source.extensions),
|
|
427
|
+
execute: source.execute,
|
|
428
|
+
contributorId: normalizeText(contributorId)
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function normalizeActionContributor(contributor) {
|
|
433
|
+
const source = contributor && typeof contributor === "object" ? contributor : {};
|
|
434
|
+
const contributorId = normalizeText(source.contributorId);
|
|
435
|
+
if (!contributorId) {
|
|
436
|
+
throw createActionRuntimeError(500, "Action contributor contributorId is required.", {
|
|
437
|
+
code: "ACTION_CONTRIBUTOR_INVALID"
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const domain = normalizeActionDomain(source.domain, {
|
|
442
|
+
context: `Action contributor \"${contributorId}\" domain`,
|
|
443
|
+
errorCode: "ACTION_CONTRIBUTOR_INVALID"
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const sourceActions = Array.isArray(source.actions) ? source.actions : [];
|
|
447
|
+
const actions = sourceActions.map((entry) =>
|
|
448
|
+
normalizeActionDefinition(entry, {
|
|
449
|
+
contributorId,
|
|
450
|
+
contributorDomain: domain
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
return Object.freeze({
|
|
455
|
+
contributorId,
|
|
456
|
+
domain,
|
|
457
|
+
actions: Object.freeze(actions)
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function createActionVersionKey(actionId, version) {
|
|
462
|
+
return `${normalizeText(actionId)}@v${normalizePositiveInteger(version, { fallback: 1 })}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const __testables = {
|
|
466
|
+
normalizeText,
|
|
467
|
+
isPlainObject,
|
|
468
|
+
normalizeStringArray,
|
|
469
|
+
normalizeSingleActionValidator,
|
|
470
|
+
normalizeSectionActionValidatorMap,
|
|
471
|
+
normalizeActionValidators,
|
|
472
|
+
normalizeActionOutputValidator,
|
|
473
|
+
normalizeActionPermission,
|
|
474
|
+
normalizeAuditConfig,
|
|
475
|
+
normalizeObservabilityConfig,
|
|
476
|
+
normalizeActionExtensions
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
export {
|
|
480
|
+
ActionRuntimeError,
|
|
481
|
+
createActionRuntimeError,
|
|
482
|
+
normalizeActionDefinition,
|
|
483
|
+
normalizeActionContributor,
|
|
484
|
+
normalizeActionDomain,
|
|
485
|
+
createActionVersionKey,
|
|
486
|
+
isPlainObject,
|
|
487
|
+
__testables
|
|
488
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
|
|
5
|
+
import { __testables, normalizeActionDefinition } from "./actionDefinitions.js";
|
|
6
|
+
|
|
7
|
+
function createWorkspaceSlugValidator() {
|
|
8
|
+
return {
|
|
9
|
+
schema: Type.Object(
|
|
10
|
+
{
|
|
11
|
+
workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
|
|
12
|
+
},
|
|
13
|
+
{ additionalProperties: false }
|
|
14
|
+
),
|
|
15
|
+
normalize(input = {}) {
|
|
16
|
+
const source = input && typeof input === "object" ? input : {};
|
|
17
|
+
if (!Object.hasOwn(source, "workspaceSlug")) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
workspaceSlug: String(source.workspaceSlug || "").trim().toLowerCase()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createPatchValidator() {
|
|
29
|
+
return {
|
|
30
|
+
schema: Type.Object(
|
|
31
|
+
{
|
|
32
|
+
name: Type.Optional(Type.String({ minLength: 1 }))
|
|
33
|
+
},
|
|
34
|
+
{ additionalProperties: false }
|
|
35
|
+
),
|
|
36
|
+
normalize(input = {}) {
|
|
37
|
+
const source = input && typeof input === "object" ? input : {};
|
|
38
|
+
if (!Object.hasOwn(source, "name")) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
name: String(source.name || "").trim().toLowerCase()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("normalizeActionValidators accepts section-map validator syntax", async () => {
|
|
50
|
+
const validator = __testables.normalizeActionValidators(
|
|
51
|
+
{
|
|
52
|
+
payload: createPatchValidator()
|
|
53
|
+
},
|
|
54
|
+
"inputValidator",
|
|
55
|
+
{ required: true }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
assert.equal(typeof validator?.normalize, "function");
|
|
59
|
+
assert.equal(validator?.schema?.type, "object");
|
|
60
|
+
assert.ok(Object.hasOwn(validator.schema?.properties || {}, "payload"));
|
|
61
|
+
|
|
62
|
+
const normalized = await validator.normalize({
|
|
63
|
+
payload: {
|
|
64
|
+
name: " Acme "
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.deepEqual(normalized, {
|
|
69
|
+
payload: {
|
|
70
|
+
name: "acme"
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("normalizeActionValidators composes root validators with section-map validators", async () => {
|
|
76
|
+
const validator = __testables.normalizeActionValidators(
|
|
77
|
+
[
|
|
78
|
+
createWorkspaceSlugValidator(),
|
|
79
|
+
{
|
|
80
|
+
patch: createPatchValidator()
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"inputValidator",
|
|
84
|
+
{ required: true }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const properties = Object.keys(validator.schema?.properties || {}).sort();
|
|
88
|
+
assert.deepEqual(properties, ["patch", "workspaceSlug"]);
|
|
89
|
+
|
|
90
|
+
const normalized = await validator.normalize({
|
|
91
|
+
workspaceSlug: " TEAM-ALPHA ",
|
|
92
|
+
patch: {
|
|
93
|
+
name: " Project X "
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(normalized, {
|
|
98
|
+
workspaceSlug: "team-alpha",
|
|
99
|
+
patch: {
|
|
100
|
+
name: "project x"
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("normalizeActionValidators rejects invalid section-map entries", () => {
|
|
106
|
+
assert.throws(
|
|
107
|
+
() =>
|
|
108
|
+
__testables.normalizeActionValidators(
|
|
109
|
+
{
|
|
110
|
+
payload: {
|
|
111
|
+
normalize() {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"inputValidator",
|
|
117
|
+
{ required: true }
|
|
118
|
+
),
|
|
119
|
+
/inputValidator\[0\]\.payload\.schema is required/
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("normalizeActionExtensions keeps plain objects", () => {
|
|
124
|
+
const extensions = __testables.normalizeActionExtensions({
|
|
125
|
+
assistant: {
|
|
126
|
+
description: "Update workspace settings."
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
assert.equal(typeof extensions, "object");
|
|
131
|
+
assert.equal(extensions.assistant?.description, "Update workspace settings.");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("normalizeActionDefinition stays channel-agnostic and ignores unknown legacy fields", () => {
|
|
135
|
+
const definition = normalizeActionDefinition({
|
|
136
|
+
id: "demo.workspace.settings.update",
|
|
137
|
+
domain: "demo",
|
|
138
|
+
version: 1,
|
|
139
|
+
kind: "command",
|
|
140
|
+
channels: ["automation"],
|
|
141
|
+
surfaces: ["admin"],
|
|
142
|
+
inputValidator: {
|
|
143
|
+
schema: Type.Object({}, { additionalProperties: false })
|
|
144
|
+
},
|
|
145
|
+
outputValidator: {
|
|
146
|
+
schema: Type.Object({}, { additionalProperties: false })
|
|
147
|
+
},
|
|
148
|
+
idempotency: "none",
|
|
149
|
+
assistantTool: {
|
|
150
|
+
description: "Legacy field"
|
|
151
|
+
},
|
|
152
|
+
execute: async () => ({})
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
assert.equal(typeof definition, "object");
|
|
156
|
+
assert.equal(Object.prototype.hasOwnProperty.call(definition, "assistantTool"), false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("normalizeActionOutputValidator accepts section-map syntax", async () => {
|
|
160
|
+
const outputValidator = __testables.normalizeActionOutputValidator(
|
|
161
|
+
{
|
|
162
|
+
payload: createPatchValidator()
|
|
163
|
+
},
|
|
164
|
+
"outputValidator",
|
|
165
|
+
{ required: false }
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
assert.equal(outputValidator?.schema?.type, "object");
|
|
169
|
+
assert.ok(Object.hasOwn(outputValidator?.schema?.properties || {}, "payload"));
|
|
170
|
+
|
|
171
|
+
const normalized = await outputValidator.normalize({
|
|
172
|
+
payload: {
|
|
173
|
+
name: " Acme "
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
assert.deepEqual(normalized, {
|
|
178
|
+
payload: {
|
|
179
|
+
name: "acme"
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("normalizeActionOutputValidator composes array validators", async () => {
|
|
185
|
+
const outputValidator = __testables.normalizeActionOutputValidator(
|
|
186
|
+
[
|
|
187
|
+
createWorkspaceSlugValidator(),
|
|
188
|
+
{
|
|
189
|
+
payload: createPatchValidator()
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
"outputValidator",
|
|
193
|
+
{ required: false }
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const properties = Object.keys(outputValidator.schema?.properties || {}).sort();
|
|
197
|
+
assert.deepEqual(properties, ["payload", "workspaceSlug"]);
|
|
198
|
+
|
|
199
|
+
const normalized = await outputValidator.normalize({
|
|
200
|
+
workspaceSlug: " TEAM-ALPHA ",
|
|
201
|
+
payload: {
|
|
202
|
+
name: " Project X "
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
assert.deepEqual(normalized, {
|
|
207
|
+
workspaceSlug: "team-alpha",
|
|
208
|
+
payload: {
|
|
209
|
+
name: "project x"
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|