@jskit-ai/kernel 0.1.244 → 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.
Files changed (101) hide show
  1. package/client/appConfig.js +2 -1
  2. package/client/asyncModuleRecovery.js +2 -4
  3. package/client/index.d.ts +18 -76
  4. package/client/index.js +7 -2
  5. package/client/logging.js +8 -2
  6. package/client/moduleBootstrap.js +41 -33
  7. package/client/moduleBootstrap.test.js +35 -0
  8. package/client/shellBootstrap.js +38 -10
  9. package/client/shellBootstrap.test.js +85 -6
  10. package/client/shellRouting.js +16 -61
  11. package/client/shellRouting.test.js +30 -1
  12. package/client/vite/clientBootstrapPlugin.js +0 -6
  13. package/client/vite/clientBootstrapPlugin.test.js +2 -3
  14. package/internal/node/installedPackages.js +14 -58
  15. package/package.json +2 -4
  16. package/server/actions/actionCatalogue.js +13 -34
  17. package/{shared → server}/actions/actionContributorHelpers.js +0 -5
  18. package/{shared → server}/actions/actionDefinitions.js +24 -131
  19. package/{shared → server}/actions/actionDefinitions.test.js +75 -21
  20. package/server/actions/actionEvents.js +97 -2
  21. package/server/actions/actionEvents.test.js +79 -0
  22. package/{shared → server}/actions/executionContext.js +3 -13
  23. package/server/actions/idempotency.js +24 -0
  24. package/server/actions/pipeline.js +87 -0
  25. package/{shared → server}/actions/policies.js +20 -33
  26. package/{shared → server}/actions/policies.test.js +6 -6
  27. package/{shared → server}/actions/registry.js +3 -22
  28. package/{shared → server}/actions/registry.test.js +70 -32
  29. package/server/actions/requestMeta.js +17 -0
  30. package/server/http/capabilityHttpRuntime.js +1 -1
  31. package/server/http/capabilityHttpRuntime.test.js +56 -0
  32. package/server/http/errorHandling.js +291 -0
  33. package/server/{runtime/fastifyBootstrap.test.js → http/errorHandling.test.js} +72 -120
  34. package/server/http/lib/routeRegistration.js +36 -150
  35. package/server/http/lib/routeTransport.js +2 -0
  36. package/server/http/lib/routeValidator.js +5 -47
  37. package/server/http/lib/routeValidator.test.js +15 -17
  38. package/server/http/lib/router.js +10 -94
  39. package/server/platform/providerRuntime/capabilityProviderLoader.js +2 -1
  40. package/server/platform/providerRuntime/helpers.js +1 -34
  41. package/server/runtime/integers.js +1 -5
  42. package/server/runtime/serviceAuthorization.js +4 -9
  43. package/server/support/appConfig.js +2 -1
  44. package/server/support/appConfigFiles.js +1 -38
  45. package/server/support/appConfigFiles.test.js +3 -39
  46. package/server/support/index.js +1 -20
  47. package/shared/capabilities/defineProvider.js +1 -1
  48. package/shared/capabilities/index.js +1 -1
  49. package/shared/index.js +1 -1
  50. package/shared/support/index.js +1 -4
  51. package/shared/support/mobileConfig.js +99 -0
  52. package/shared/support/mobileConfig.test.js +113 -0
  53. package/shared/support/normalize.js +0 -108
  54. package/shared/support/normalize.test.js +0 -134
  55. package/shared/{actions/actionContributorHelpers.test.js → support/permissions.test.js} +1 -1
  56. package/shared/support/returnToPath.js +16 -20
  57. package/shared/support/returnToPath.test.js +5 -33
  58. package/shared/support/shellLayoutTargets.js +2 -140
  59. package/shared/support/shellLayoutTargets.test.js +1 -110
  60. package/shared/support/sorting.js +1 -9
  61. package/shared/surface/apiPaths.js +1 -82
  62. package/shared/surface/paths.js +38 -171
  63. package/shared/surface/registry.js +2 -1
  64. package/shared/surface/runtime.js +18 -109
  65. package/shared/surface/runtime.test.js +57 -0
  66. package/shared/validators/index.js +1 -4
  67. package/shared/validators/jsonRestSchemaSupport.js +0 -9
  68. package/shared/validators/recordIdParamsValidator.js +0 -6
  69. package/shared/validators/schemaDefinitions.js +4 -30
  70. package/shared/validators/schemaPayloadValidation.js +2 -6
  71. package/test/barrelExposure.test.js +5 -8
  72. package/test/exportsContract.test.js +1 -3
  73. package/client/mobileLaunchRouting.js +0 -231
  74. package/client/mobileLaunchRouting.test.js +0 -230
  75. package/server/http/lib/controller.js +0 -183
  76. package/server/http/lib/controller.test.js +0 -143
  77. package/server/http/lib/index.js +0 -5
  78. package/server/runtime/canonicalJson.js +0 -74
  79. package/server/runtime/entityChangeEvents.js +0 -307
  80. package/server/runtime/entityChangeEvents.test.js +0 -274
  81. package/server/runtime/fastifyBootstrap.js +0 -553
  82. package/server/runtime/moduleConfig.js +0 -223
  83. package/server/runtime/moduleConfig.test.js +0 -165
  84. package/server/runtime/pagination.js +0 -13
  85. package/server/runtime/realtimeNormalization.js +0 -21
  86. package/server/runtime/routeUtils.js +0 -20
  87. package/server/runtime/securityAudit.js +0 -269
  88. package/server/runtime/securityAudit.test.js +0 -41
  89. package/server/support/pageTargets.js +0 -772
  90. package/server/support/pageTargets.test.js +0 -659
  91. package/server/support/shellOutlets.js +0 -576
  92. package/server/support/shellOutlets.test.js +0 -336
  93. package/server/surface/index.js +0 -18
  94. package/shared/actions/audit.js +0 -7
  95. package/shared/actions/idempotency.js +0 -62
  96. package/shared/actions/observability.js +0 -10
  97. package/shared/actions/pipeline.js +0 -309
  98. package/shared/actions/requestMeta.js +0 -36
  99. package/shared/actions/textNormalization.js +0 -3
  100. package/shared/actions/withActionDefaults.js +0 -34
  101. /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 };