@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,223 +0,0 @@
|
|
|
1
|
-
import { deepFreeze } from "../../shared/support/deepFreeze.js";
|
|
2
|
-
import { normalizeObject, normalizeText } from "../../shared/support/normalize.js";
|
|
3
|
-
import { validateSchemaPayload } from "../../shared/validators/schemaPayloadValidation.js";
|
|
4
|
-
|
|
5
|
-
function normalizeModuleId(value) {
|
|
6
|
-
const moduleId = normalizeText(value);
|
|
7
|
-
if (!moduleId) {
|
|
8
|
-
throw new TypeError("defineModuleConfig requires a non-empty moduleId.");
|
|
9
|
-
}
|
|
10
|
-
return moduleId;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function normalizeSchema(value) {
|
|
14
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
15
|
-
throw new TypeError("defineModuleConfig requires a schema object.");
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function formatIssues(issues = []) {
|
|
21
|
-
return issues
|
|
22
|
-
.slice(0, 8)
|
|
23
|
-
.map((issue) => `${issue.path}: ${issue.message}`)
|
|
24
|
-
.join("; ");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function buildInvalidConfigMessage(moduleId, issues = []) {
|
|
28
|
-
const summary = formatIssues(issues);
|
|
29
|
-
if (!summary) {
|
|
30
|
-
return `Invalid config for module "${moduleId}".`;
|
|
31
|
-
}
|
|
32
|
-
return `Invalid config for module "${moduleId}": ${summary}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function normalizeCustomValidationIssues(result) {
|
|
36
|
-
if (result == null || result === true) {
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (result === false) {
|
|
41
|
-
return [
|
|
42
|
-
{
|
|
43
|
-
path: "(custom)",
|
|
44
|
-
message: "Custom module config validation failed."
|
|
45
|
-
}
|
|
46
|
-
];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (typeof result === "string") {
|
|
50
|
-
return [
|
|
51
|
-
{
|
|
52
|
-
path: "(custom)",
|
|
53
|
-
message: normalizeText(result) || "Custom module config validation failed."
|
|
54
|
-
}
|
|
55
|
-
];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (Array.isArray(result)) {
|
|
59
|
-
return result
|
|
60
|
-
.map((entry) => {
|
|
61
|
-
if (typeof entry === "string") {
|
|
62
|
-
return {
|
|
63
|
-
path: "(custom)",
|
|
64
|
-
message: normalizeText(entry) || "Custom module config validation failed."
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!entry || typeof entry !== "object") {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
path: normalizeText(entry.path, { fallback: "(custom)" }),
|
|
74
|
-
message: normalizeText(entry.message, { fallback: "Custom module config validation failed." })
|
|
75
|
-
};
|
|
76
|
-
})
|
|
77
|
-
.filter(Boolean);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (result && typeof result === "object" && Array.isArray(result.issues)) {
|
|
81
|
-
return normalizeCustomValidationIssues(result.issues);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return [
|
|
85
|
-
{
|
|
86
|
-
path: "(custom)",
|
|
87
|
-
message: "Custom module config validation failed."
|
|
88
|
-
}
|
|
89
|
-
];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
class ModuleConfigError extends Error {
|
|
93
|
-
constructor(message, { moduleId = "", issues = [], cause } = {}) {
|
|
94
|
-
super(String(message || "Module config error."));
|
|
95
|
-
this.name = "ModuleConfigError";
|
|
96
|
-
this.moduleId = normalizeText(moduleId);
|
|
97
|
-
this.issues = Object.freeze(Array.isArray(issues) ? [...issues] : []);
|
|
98
|
-
if (cause !== undefined) {
|
|
99
|
-
this.cause = cause;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function validateSchemaOrThrow({ moduleId, schema, value, coerce = false }) {
|
|
105
|
-
const schemaDefinition = {
|
|
106
|
-
schema,
|
|
107
|
-
mode: "replace"
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
return validateSchemaPayload(schemaDefinition, value, {
|
|
112
|
-
phase: "input",
|
|
113
|
-
context: `module config "${moduleId}"`
|
|
114
|
-
});
|
|
115
|
-
} catch (cause) {
|
|
116
|
-
const fieldErrors = normalizeObject(cause?.fieldErrors);
|
|
117
|
-
const issues = Object.keys(fieldErrors).length > 0
|
|
118
|
-
? Object.keys(fieldErrors).map((path) => ({
|
|
119
|
-
path,
|
|
120
|
-
message: normalizeText(fieldErrors[path], { fallback: "Invalid value." }),
|
|
121
|
-
keyword: ""
|
|
122
|
-
}))
|
|
123
|
-
: [
|
|
124
|
-
{
|
|
125
|
-
path: "(root)",
|
|
126
|
-
message: normalizeText(cause?.message, { fallback: "Invalid value." }),
|
|
127
|
-
keyword: ""
|
|
128
|
-
}
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
throw new ModuleConfigError(buildInvalidConfigMessage(moduleId, issues), {
|
|
132
|
-
moduleId,
|
|
133
|
-
issues,
|
|
134
|
-
cause
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function defineModuleConfig({
|
|
140
|
-
moduleId,
|
|
141
|
-
schema,
|
|
142
|
-
load = ({ env }) => env,
|
|
143
|
-
transform = null,
|
|
144
|
-
validate = null,
|
|
145
|
-
coerce = false,
|
|
146
|
-
freeze = true
|
|
147
|
-
} = {}) {
|
|
148
|
-
const normalizedModuleId = normalizeModuleId(moduleId);
|
|
149
|
-
const normalizedSchema = normalizeSchema(schema);
|
|
150
|
-
|
|
151
|
-
if (typeof load !== "function") {
|
|
152
|
-
throw new TypeError(`defineModuleConfig("${normalizedModuleId}") requires load to be a function.`);
|
|
153
|
-
}
|
|
154
|
-
if (transform != null && typeof transform !== "function") {
|
|
155
|
-
throw new TypeError(`defineModuleConfig("${normalizedModuleId}") requires transform to be a function when provided.`);
|
|
156
|
-
}
|
|
157
|
-
if (validate != null && typeof validate !== "function") {
|
|
158
|
-
throw new TypeError(`defineModuleConfig("${normalizedModuleId}") requires validate to be a function when provided.`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolve(options = {}) {
|
|
162
|
-
const source = normalizeObject(options);
|
|
163
|
-
const env = source.env && typeof source.env === "object" ? source.env : process.env;
|
|
164
|
-
const context = source.context;
|
|
165
|
-
const hasRaw = Object.prototype.hasOwnProperty.call(source, "raw");
|
|
166
|
-
|
|
167
|
-
let loadedConfig;
|
|
168
|
-
try {
|
|
169
|
-
loadedConfig = hasRaw ? source.raw : load({ env, context, moduleId: normalizedModuleId });
|
|
170
|
-
} catch (cause) {
|
|
171
|
-
throw new ModuleConfigError(`Failed to load config for module "${normalizedModuleId}".`, {
|
|
172
|
-
moduleId: normalizedModuleId,
|
|
173
|
-
cause
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
let candidate = loadedConfig;
|
|
178
|
-
if (typeof transform === "function") {
|
|
179
|
-
candidate = transform(candidate, { env, context, moduleId: normalizedModuleId });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
let resolvedConfig = validateSchemaOrThrow({
|
|
183
|
-
moduleId: normalizedModuleId,
|
|
184
|
-
schema: normalizedSchema,
|
|
185
|
-
value: candidate,
|
|
186
|
-
coerce: Boolean(coerce)
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
if (typeof validate === "function") {
|
|
190
|
-
let customValidationResult;
|
|
191
|
-
try {
|
|
192
|
-
customValidationResult = validate(resolvedConfig, { env, context, moduleId: normalizedModuleId });
|
|
193
|
-
} catch (cause) {
|
|
194
|
-
throw new ModuleConfigError(`Invalid config for module "${normalizedModuleId}" from custom validation.`, {
|
|
195
|
-
moduleId: normalizedModuleId,
|
|
196
|
-
cause
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const issues = normalizeCustomValidationIssues(customValidationResult);
|
|
201
|
-
if (issues.length > 0) {
|
|
202
|
-
throw new ModuleConfigError(buildInvalidConfigMessage(normalizedModuleId, issues), {
|
|
203
|
-
moduleId: normalizedModuleId,
|
|
204
|
-
issues
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (freeze) {
|
|
210
|
-
resolvedConfig = deepFreeze(resolvedConfig);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return resolvedConfig;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return Object.freeze({
|
|
217
|
-
moduleId: normalizedModuleId,
|
|
218
|
-
schema: normalizedSchema,
|
|
219
|
-
resolve
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export { ModuleConfigError, defineModuleConfig };
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { createSchema } from "json-rest-schema";
|
|
4
|
-
|
|
5
|
-
import { ModuleConfigError, defineModuleConfig } from "./moduleConfig.js";
|
|
6
|
-
|
|
7
|
-
test("defineModuleConfig resolves valid config and freezes nested objects", () => {
|
|
8
|
-
const moduleConfig = defineModuleConfig({
|
|
9
|
-
moduleId: "contacts",
|
|
10
|
-
schema: createSchema({
|
|
11
|
-
mode: {
|
|
12
|
-
type: "string",
|
|
13
|
-
required: true,
|
|
14
|
-
enum: ["standard", "strict"]
|
|
15
|
-
},
|
|
16
|
-
maxContacts: {
|
|
17
|
-
type: "integer",
|
|
18
|
-
required: true,
|
|
19
|
-
min: 1
|
|
20
|
-
},
|
|
21
|
-
limits: {
|
|
22
|
-
type: "object",
|
|
23
|
-
required: true,
|
|
24
|
-
validator(value) {
|
|
25
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
26
|
-
return "limits must be an object";
|
|
27
|
-
}
|
|
28
|
-
const inviteExpiryHours = Number(value.inviteExpiryHours);
|
|
29
|
-
if (!Number.isInteger(inviteExpiryHours) || inviteExpiryHours < 1 || inviteExpiryHours > 168) {
|
|
30
|
-
return "inviteExpiryHours must be an integer between 1 and 168";
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}),
|
|
36
|
-
load({ env }) {
|
|
37
|
-
return {
|
|
38
|
-
mode: String(env.CONTACTS_MODE || "standard"),
|
|
39
|
-
maxContacts: Number(env.CONTACTS_MAX || 5000),
|
|
40
|
-
limits: {
|
|
41
|
-
inviteExpiryHours: Number(env.CONTACTS_INVITE_EXPIRY_HOURS || 24)
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const config = moduleConfig.resolve({
|
|
48
|
-
env: {
|
|
49
|
-
CONTACTS_MODE: "strict",
|
|
50
|
-
CONTACTS_MAX: "1000",
|
|
51
|
-
CONTACTS_INVITE_EXPIRY_HOURS: "36"
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
assert.equal(config.mode, "strict");
|
|
56
|
-
assert.equal(config.maxContacts, 1000);
|
|
57
|
-
assert.equal(config.limits.inviteExpiryHours, 36);
|
|
58
|
-
assert.equal(Object.isFrozen(config), true);
|
|
59
|
-
assert.equal(Object.isFrozen(config.limits), true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("defineModuleConfig reports schema validation issues with module-scoped details", () => {
|
|
63
|
-
const moduleConfig = defineModuleConfig({
|
|
64
|
-
moduleId: "contacts",
|
|
65
|
-
schema: createSchema({
|
|
66
|
-
maxContacts: {
|
|
67
|
-
type: "integer",
|
|
68
|
-
required: true,
|
|
69
|
-
min: 1
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
assert.throws(
|
|
75
|
-
() => moduleConfig.resolve({ raw: { maxContacts: 0, unexpected: true } }),
|
|
76
|
-
(error) => {
|
|
77
|
-
assert.equal(error instanceof ModuleConfigError, true);
|
|
78
|
-
assert.equal(error.moduleId, "contacts");
|
|
79
|
-
assert.equal(error.issues.length >= 1, true);
|
|
80
|
-
assert.equal(String(error.message).includes('module "contacts"'), true);
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("defineModuleConfig supports coercion via json-rest-schema casts", () => {
|
|
87
|
-
const moduleConfig = defineModuleConfig({
|
|
88
|
-
moduleId: "contacts",
|
|
89
|
-
coerce: true,
|
|
90
|
-
schema: createSchema({
|
|
91
|
-
maxContacts: { type: "integer", required: true, min: 1 },
|
|
92
|
-
enabled: { type: "boolean", required: true }
|
|
93
|
-
})
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const config = moduleConfig.resolve({
|
|
97
|
-
raw: {
|
|
98
|
-
maxContacts: "42",
|
|
99
|
-
enabled: "true"
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
assert.equal(config.maxContacts, 42);
|
|
104
|
-
assert.equal(config.enabled, true);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("defineModuleConfig supports custom cross-field validate hook", () => {
|
|
108
|
-
const moduleConfig = defineModuleConfig({
|
|
109
|
-
moduleId: "contacts",
|
|
110
|
-
schema: createSchema({
|
|
111
|
-
mode: {
|
|
112
|
-
type: "string",
|
|
113
|
-
required: true,
|
|
114
|
-
enum: ["standard", "strict"]
|
|
115
|
-
},
|
|
116
|
-
requireAuditTrail: {
|
|
117
|
-
type: "boolean",
|
|
118
|
-
required: true
|
|
119
|
-
}
|
|
120
|
-
}),
|
|
121
|
-
validate(value) {
|
|
122
|
-
if (value.mode === "strict" && value.requireAuditTrail !== true) {
|
|
123
|
-
return [
|
|
124
|
-
{
|
|
125
|
-
path: "requireAuditTrail",
|
|
126
|
-
message: "must be true when mode is strict"
|
|
127
|
-
}
|
|
128
|
-
];
|
|
129
|
-
}
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
assert.throws(
|
|
135
|
-
() =>
|
|
136
|
-
moduleConfig.resolve({
|
|
137
|
-
raw: {
|
|
138
|
-
mode: "strict",
|
|
139
|
-
requireAuditTrail: false
|
|
140
|
-
}
|
|
141
|
-
}),
|
|
142
|
-
/requireAuditTrail: must be true when mode is strict/
|
|
143
|
-
);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("defineModuleConfig rejects invalid module config definitions", () => {
|
|
147
|
-
assert.throws(() => defineModuleConfig({}), /moduleId/);
|
|
148
|
-
assert.throws(
|
|
149
|
-
() =>
|
|
150
|
-
defineModuleConfig({
|
|
151
|
-
moduleId: "contacts",
|
|
152
|
-
schema: null
|
|
153
|
-
}),
|
|
154
|
-
/schema/
|
|
155
|
-
);
|
|
156
|
-
assert.throws(
|
|
157
|
-
() =>
|
|
158
|
-
defineModuleConfig({
|
|
159
|
-
moduleId: "contacts",
|
|
160
|
-
schema: createSchema({}),
|
|
161
|
-
load: "not-a-function"
|
|
162
|
-
}),
|
|
163
|
-
/load/
|
|
164
|
-
);
|
|
165
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { parsePositiveInteger } from "./integers.js";
|
|
2
|
-
|
|
3
|
-
function normalizePagination(pagination = {}, { defaultPage = 1, defaultPageSize = 20, maxPageSize = 100 } = {}) {
|
|
4
|
-
const rawPage = parsePositiveInteger(pagination?.page);
|
|
5
|
-
const rawPageSize = parsePositiveInteger(pagination?.pageSize);
|
|
6
|
-
|
|
7
|
-
return {
|
|
8
|
-
page: rawPage || defaultPage,
|
|
9
|
-
pageSize: Math.max(1, Math.min(maxPageSize, rawPageSize || defaultPageSize))
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export { normalizePagination };
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { normalizePositiveInteger, normalizeText } from "../../shared/support/normalize.js";
|
|
2
|
-
|
|
3
|
-
function normalizePositiveIntegerOrNull(value) {
|
|
4
|
-
const normalized = normalizePositiveInteger(value);
|
|
5
|
-
if (normalized < 1) {
|
|
6
|
-
return null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
return normalized;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function normalizeScopeKind(value) {
|
|
13
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
14
|
-
if (!normalized || normalized === "global") {
|
|
15
|
-
return "global";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return normalized;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export { normalizePositiveIntegerOrNull, normalizeScopeKind };
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { AppError } from "./errors.js";
|
|
2
|
-
import { defaultMissingHandler } from "../support/defaultMissingHandler.js";
|
|
3
|
-
|
|
4
|
-
function normalizeIdempotencyKey(value) {
|
|
5
|
-
const normalized = String(value || "").trim();
|
|
6
|
-
return normalized || "";
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function requireIdempotencyKey(request) {
|
|
10
|
-
const idempotencyKey = normalizeIdempotencyKey(request?.headers?.["idempotency-key"]);
|
|
11
|
-
if (!idempotencyKey) {
|
|
12
|
-
throw new AppError(400, "Idempotency-Key header is required.", {
|
|
13
|
-
code: "IDEMPOTENCY_KEY_REQUIRED"
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return idempotencyKey;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export { defaultMissingHandler, normalizeIdempotencyKey, requireIdempotencyKey };
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { parsePositiveInteger } from "./integers.js";
|
|
2
|
-
import { safePathnameFromRequest, resolveClientIpAddress } from "./requestUrl.js";
|
|
3
|
-
import { normalizeSurfaceId } from "../../shared/surface/registry.js";
|
|
4
|
-
import { normalizeOpaqueId } from "../../shared/support/normalize.js";
|
|
5
|
-
import { resolveDefaultSurfaceId } from "../support/appConfig.js";
|
|
6
|
-
|
|
7
|
-
function resolveAuditSurface(pathnameValue, explicitSurface = "", resolveSurfaceFromPathname = null, defaultSurfaceId = "") {
|
|
8
|
-
const normalizedExplicit = normalizeSurfaceId(explicitSurface);
|
|
9
|
-
if (normalizedExplicit) {
|
|
10
|
-
return normalizedExplicit;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (typeof resolveSurfaceFromPathname === "function") {
|
|
14
|
-
const resolvedSurface = normalizeSurfaceId(resolveSurfaceFromPathname(pathnameValue));
|
|
15
|
-
if (resolvedSurface) {
|
|
16
|
-
return resolvedSurface;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return resolveDefaultSurfaceId(null, {
|
|
21
|
-
defaultSurfaceId
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function buildAuditEventBase(request, { resolveSurfaceFromPathname = null, defaultSurfaceId = "" } = {}) {
|
|
26
|
-
const pathnameValue = safePathnameFromRequest(request);
|
|
27
|
-
return {
|
|
28
|
-
actorId: normalizeOpaqueId(request?.user?.id),
|
|
29
|
-
actorEmail: String(request?.user?.email || "")
|
|
30
|
-
.trim()
|
|
31
|
-
.toLowerCase(),
|
|
32
|
-
surface: resolveAuditSurface(pathnameValue, request?.surface, resolveSurfaceFromPathname, defaultSurfaceId),
|
|
33
|
-
requestId: String(request?.id || "").trim(),
|
|
34
|
-
method: String(request?.method || "")
|
|
35
|
-
.trim()
|
|
36
|
-
.toUpperCase(),
|
|
37
|
-
path: pathnameValue,
|
|
38
|
-
ipAddress: resolveClientIpAddress(request),
|
|
39
|
-
userAgent: String(request?.headers?.["user-agent"] || "")
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function buildAuditError(error) {
|
|
44
|
-
const status = parsePositiveInteger(error?.status || error?.statusCode);
|
|
45
|
-
return {
|
|
46
|
-
name: String(error?.name || "Error"),
|
|
47
|
-
code: String(error?.code || ""),
|
|
48
|
-
status: status || null
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function normalizeObjectPayload(value) {
|
|
53
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
54
|
-
return {};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return value;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function resolveObjectPayload(value, context) {
|
|
61
|
-
const resolvedValue = typeof value === "function" ? value(context) : value;
|
|
62
|
-
return normalizeObjectPayload(resolvedValue);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function mergeMetadataPayloads(...payloads) {
|
|
66
|
-
const merged = {};
|
|
67
|
-
for (const payloadEntry of payloads) {
|
|
68
|
-
const payload = normalizeObjectPayload(payloadEntry);
|
|
69
|
-
for (const [key, value] of Object.entries(payload)) {
|
|
70
|
-
merged[key] = value;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return merged;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function buildEventPayload({ action, outcome, shared, event, metadata, context }) {
|
|
78
|
-
const sharedPayload = resolveObjectPayload(shared, context);
|
|
79
|
-
const eventPayload = resolveObjectPayload(event, context);
|
|
80
|
-
const sharedMetadata = resolveObjectPayload(metadata, context);
|
|
81
|
-
const eventMetadata = resolveObjectPayload(eventPayload.metadata, context);
|
|
82
|
-
const mergedMetadata = mergeMetadataPayloads(sharedMetadata, eventMetadata);
|
|
83
|
-
|
|
84
|
-
const payload = {
|
|
85
|
-
...sharedPayload,
|
|
86
|
-
...eventPayload,
|
|
87
|
-
action,
|
|
88
|
-
outcome
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
if (Object.keys(mergedMetadata).length > 0) {
|
|
92
|
-
payload.metadata = mergedMetadata;
|
|
93
|
-
} else {
|
|
94
|
-
delete payload.metadata;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return payload;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function mergeFailureMetadata(error, metadata) {
|
|
101
|
-
const normalizedMetadata =
|
|
102
|
-
metadata && typeof metadata === "object" && !Array.isArray(metadata) ? { ...metadata } : {};
|
|
103
|
-
if (!Object.hasOwn(normalizedMetadata, "error")) {
|
|
104
|
-
normalizedMetadata.error = buildAuditError(error);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return normalizedMetadata;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function logAuditFailure(request, payload, message) {
|
|
111
|
-
if (!request?.log || typeof request.log.warn !== "function") {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
request.log.warn(payload, message);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function safeBuildEventPayload({ request, action, outcome, shared, event, metadata, context, stage }) {
|
|
119
|
-
try {
|
|
120
|
-
return buildEventPayload({
|
|
121
|
-
action,
|
|
122
|
-
outcome,
|
|
123
|
-
shared,
|
|
124
|
-
event,
|
|
125
|
-
metadata,
|
|
126
|
-
context
|
|
127
|
-
});
|
|
128
|
-
} catch (error) {
|
|
129
|
-
logAuditFailure(
|
|
130
|
-
request,
|
|
131
|
-
{
|
|
132
|
-
action,
|
|
133
|
-
outcome,
|
|
134
|
-
stage,
|
|
135
|
-
callbackError: buildAuditError(error)
|
|
136
|
-
},
|
|
137
|
-
"security.audit.callback_failed"
|
|
138
|
-
);
|
|
139
|
-
return {
|
|
140
|
-
action,
|
|
141
|
-
outcome
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function recordAuditEvent({ auditService, request, event, resolveSurfaceFromPathname = null, defaultSurfaceId = "" }) {
|
|
147
|
-
await auditService.recordSafe(
|
|
148
|
-
{
|
|
149
|
-
...buildAuditEventBase(request, {
|
|
150
|
-
resolveSurfaceFromPathname,
|
|
151
|
-
defaultSurfaceId
|
|
152
|
-
}),
|
|
153
|
-
...event
|
|
154
|
-
},
|
|
155
|
-
request?.log
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function safeRecordAuditEvent({ auditService, request, event, resolveSurfaceFromPathname = null, defaultSurfaceId = "" }) {
|
|
160
|
-
try {
|
|
161
|
-
await recordAuditEvent({
|
|
162
|
-
auditService,
|
|
163
|
-
request,
|
|
164
|
-
event,
|
|
165
|
-
resolveSurfaceFromPathname,
|
|
166
|
-
defaultSurfaceId
|
|
167
|
-
});
|
|
168
|
-
} catch (error) {
|
|
169
|
-
logAuditFailure(
|
|
170
|
-
request,
|
|
171
|
-
{
|
|
172
|
-
action: String(event?.action || ""),
|
|
173
|
-
outcome: String(event?.outcome || ""),
|
|
174
|
-
recordError: buildAuditError(error)
|
|
175
|
-
},
|
|
176
|
-
"security.audit.record_unexpected_failure"
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function withAuditEvent({
|
|
182
|
-
auditService,
|
|
183
|
-
request,
|
|
184
|
-
action,
|
|
185
|
-
execute,
|
|
186
|
-
shared,
|
|
187
|
-
metadata,
|
|
188
|
-
onSuccess,
|
|
189
|
-
onFailure,
|
|
190
|
-
resolveSurfaceFromPathname = null,
|
|
191
|
-
defaultSurfaceId = ""
|
|
192
|
-
}) {
|
|
193
|
-
if (!auditService || typeof auditService.recordSafe !== "function") {
|
|
194
|
-
throw new TypeError("withAuditEvent auditService.recordSafe is required.");
|
|
195
|
-
}
|
|
196
|
-
if (typeof execute !== "function") {
|
|
197
|
-
throw new TypeError("withAuditEvent execute callback is required.");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
const result = await execute();
|
|
202
|
-
const successEvent = safeBuildEventPayload({
|
|
203
|
-
request,
|
|
204
|
-
action,
|
|
205
|
-
outcome: "success",
|
|
206
|
-
shared,
|
|
207
|
-
event: onSuccess,
|
|
208
|
-
metadata,
|
|
209
|
-
context: {
|
|
210
|
-
request,
|
|
211
|
-
result,
|
|
212
|
-
error: null,
|
|
213
|
-
outcome: "success"
|
|
214
|
-
},
|
|
215
|
-
stage: "success"
|
|
216
|
-
});
|
|
217
|
-
await safeRecordAuditEvent({
|
|
218
|
-
auditService,
|
|
219
|
-
request,
|
|
220
|
-
event: successEvent,
|
|
221
|
-
resolveSurfaceFromPathname,
|
|
222
|
-
defaultSurfaceId
|
|
223
|
-
});
|
|
224
|
-
return result;
|
|
225
|
-
} catch (error) {
|
|
226
|
-
const failureEvent = safeBuildEventPayload({
|
|
227
|
-
request,
|
|
228
|
-
action,
|
|
229
|
-
outcome: "failure",
|
|
230
|
-
shared,
|
|
231
|
-
event: onFailure,
|
|
232
|
-
metadata,
|
|
233
|
-
context: {
|
|
234
|
-
request,
|
|
235
|
-
result: null,
|
|
236
|
-
error,
|
|
237
|
-
outcome: "failure"
|
|
238
|
-
},
|
|
239
|
-
stage: "failure"
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
await safeRecordAuditEvent({
|
|
243
|
-
auditService,
|
|
244
|
-
request,
|
|
245
|
-
event: {
|
|
246
|
-
...failureEvent,
|
|
247
|
-
metadata: mergeFailureMetadata(error, failureEvent.metadata)
|
|
248
|
-
},
|
|
249
|
-
resolveSurfaceFromPathname,
|
|
250
|
-
defaultSurfaceId
|
|
251
|
-
});
|
|
252
|
-
throw error;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const __testables = {
|
|
257
|
-
normalizeSurfaceId,
|
|
258
|
-
resolveAuditSurface,
|
|
259
|
-
buildAuditEventBase,
|
|
260
|
-
buildAuditError,
|
|
261
|
-
normalizeObjectPayload,
|
|
262
|
-
resolveObjectPayload,
|
|
263
|
-
mergeMetadataPayloads,
|
|
264
|
-
buildEventPayload,
|
|
265
|
-
mergeFailureMetadata,
|
|
266
|
-
safeBuildEventPayload
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
export { buildAuditEventBase, buildAuditError, recordAuditEvent, withAuditEvent, __testables };
|