@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,269 @@
|
|
|
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 };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { buildAuditEventBase, __testables } from "./securityAudit.js";
|
|
5
|
+
|
|
6
|
+
const { resolveAuditSurface } = __testables;
|
|
7
|
+
|
|
8
|
+
test("resolveAuditSurface prefers explicit surface", () => {
|
|
9
|
+
assert.equal(resolveAuditSurface("/", " Admin "), "admin");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("resolveAuditSurface resolves surface from pathname resolver before default", () => {
|
|
13
|
+
const resolved = resolveAuditSurface(
|
|
14
|
+
"/admin/users",
|
|
15
|
+
"",
|
|
16
|
+
(pathname) => (pathname.startsWith("/admin") ? "console" : ""),
|
|
17
|
+
"home"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
assert.equal(resolved, "console");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("resolveAuditSurface uses configured default and falls back to empty surface", () => {
|
|
24
|
+
assert.equal(resolveAuditSurface("/", "", null, "home"), "home");
|
|
25
|
+
assert.equal(resolveAuditSurface("/", "", null, ""), "");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("buildAuditEventBase uses default surface id when request surface is missing", () => {
|
|
29
|
+
const event = buildAuditEventBase(
|
|
30
|
+
{
|
|
31
|
+
id: "req-1",
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: {}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
defaultSurfaceId: "console"
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
assert.equal(event.surface, "console");
|
|
41
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { normalizeObject, normalizeOpaqueId, normalizeText } from "../../shared/support/normalize.js";
|
|
2
|
+
import { hasPermission, normalizePermissionList } from "../../shared/support/permissions.js";
|
|
3
|
+
import { AppError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
const AUTH_REQUIRE_MODES = new Set(["none", "authenticated", "all", "any"]);
|
|
6
|
+
|
|
7
|
+
function resolveServiceContext(source = {}) {
|
|
8
|
+
const payload = normalizeObject(source);
|
|
9
|
+
|
|
10
|
+
if (payload.context && typeof payload.context === "object" && !Array.isArray(payload.context)) {
|
|
11
|
+
return payload.context;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (payload.actionContext && typeof payload.actionContext === "object" && !Array.isArray(payload.actionContext)) {
|
|
15
|
+
return payload.actionContext;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return payload;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveRequireMode(value) {
|
|
22
|
+
const mode = normalizeText(value || "authenticated").toLowerCase();
|
|
23
|
+
if (!AUTH_REQUIRE_MODES.has(mode)) {
|
|
24
|
+
throw new TypeError("requireAuth options.require must be one of: none, authenticated, all, any.");
|
|
25
|
+
}
|
|
26
|
+
return mode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRequireAuthOptions(options = {}, { context = "requireAuth options" } = {}) {
|
|
30
|
+
const source = normalizeObject(options);
|
|
31
|
+
const mode = resolveRequireMode(source.require);
|
|
32
|
+
const permissions = normalizePermissionList(source.permissions);
|
|
33
|
+
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
require: mode,
|
|
36
|
+
permissions: Object.freeze(permissions),
|
|
37
|
+
message: normalizeText(source.message),
|
|
38
|
+
code: normalizeText(source.code),
|
|
39
|
+
context
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireAuthenticatedActor(context = {}, options = {}) {
|
|
44
|
+
const actor = normalizeObject(context.actor);
|
|
45
|
+
const actorId = normalizeOpaqueId(actor.id);
|
|
46
|
+
|
|
47
|
+
if (actorId == null) {
|
|
48
|
+
throw new AppError(401, options.message || "Authentication required.", {
|
|
49
|
+
code: options.code || "AUTHENTICATION_REQUIRED"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...actor,
|
|
55
|
+
id: actorId
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requireAuth(source = {}, options = {}) {
|
|
60
|
+
const context = resolveServiceContext(source);
|
|
61
|
+
const settings = normalizeRequireAuthOptions(options);
|
|
62
|
+
const mode = settings.require;
|
|
63
|
+
|
|
64
|
+
if (mode === "none") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const actor = requireAuthenticatedActor(context, settings);
|
|
69
|
+
|
|
70
|
+
if (mode === "authenticated") {
|
|
71
|
+
return actor;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const requiredPermissions = normalizePermissionList(settings.permissions);
|
|
75
|
+
if (requiredPermissions.length < 1) {
|
|
76
|
+
return actor;
|
|
77
|
+
}
|
|
78
|
+
const actorPermissions = normalizePermissionList(context.permissions);
|
|
79
|
+
|
|
80
|
+
if (mode === "all") {
|
|
81
|
+
for (const permission of requiredPermissions) {
|
|
82
|
+
if (hasPermission(actorPermissions, permission)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new AppError(403, settings.message || "Forbidden.", {
|
|
87
|
+
code: settings.code || "PERMISSION_DENIED",
|
|
88
|
+
details: {
|
|
89
|
+
permission
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return actor;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const allowed = requiredPermissions.some((permission) => hasPermission(actorPermissions, permission));
|
|
97
|
+
if (!allowed) {
|
|
98
|
+
throw new AppError(403, settings.message || "Forbidden.", {
|
|
99
|
+
code: settings.code || "PERMISSION_DENIED",
|
|
100
|
+
details: {
|
|
101
|
+
requiredPermissions
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return actor;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
resolveServiceContext,
|
|
111
|
+
hasPermission,
|
|
112
|
+
requireAuth
|
|
113
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { requireAuth } from "./serviceAuthorization.js";
|
|
4
|
+
|
|
5
|
+
test("requireAuth allows public mode without actor", () => {
|
|
6
|
+
assert.doesNotThrow(() =>
|
|
7
|
+
requireAuth(
|
|
8
|
+
{
|
|
9
|
+
context: {}
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
require: "none"
|
|
13
|
+
}
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("requireAuth accepts actor context", () => {
|
|
19
|
+
const actor = requireAuth({
|
|
20
|
+
context: {
|
|
21
|
+
actor: {
|
|
22
|
+
id: 7
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(actor.id, 7);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("requireAuth accepts non-numeric actor ids", () => {
|
|
31
|
+
const actor = requireAuth({
|
|
32
|
+
context: {
|
|
33
|
+
actor: {
|
|
34
|
+
id: "user-7"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.equal(actor.id, "user-7");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("requireAuth throws when actor is missing", () => {
|
|
43
|
+
assert.throws(
|
|
44
|
+
() => requireAuth({ context: {} }),
|
|
45
|
+
(error) => error?.statusCode === 401 && error?.code === "AUTHENTICATION_REQUIRED"
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("requireAuth enforces all permissions", () => {
|
|
50
|
+
assert.throws(
|
|
51
|
+
() =>
|
|
52
|
+
requireAuth(
|
|
53
|
+
{
|
|
54
|
+
context: {
|
|
55
|
+
actor: { id: 1 },
|
|
56
|
+
permissions: ["workspace.settings.view"]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
require: "all",
|
|
61
|
+
permissions: ["workspace.settings.update"]
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
(error) => error?.statusCode === 403 && error?.code === "PERMISSION_DENIED"
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("requireAuth allows wildcard permission for any mode", () => {
|
|
69
|
+
assert.doesNotThrow(() =>
|
|
70
|
+
requireAuth(
|
|
71
|
+
{
|
|
72
|
+
context: {
|
|
73
|
+
actor: { id: 1 },
|
|
74
|
+
permissions: ["*"]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
require: "any",
|
|
79
|
+
permissions: ["workspace.settings.view", "workspace.settings.update"]
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("requireAuth allows namespace wildcard permissions", () => {
|
|
86
|
+
assert.doesNotThrow(() =>
|
|
87
|
+
requireAuth(
|
|
88
|
+
{
|
|
89
|
+
context: {
|
|
90
|
+
actor: { id: 1 },
|
|
91
|
+
permissions: ["crud_contacts.*"]
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
require: "all",
|
|
96
|
+
permissions: ["crud_contacts.update"]
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createContainer } from "../container/index.js";
|
|
4
|
+
import {
|
|
5
|
+
installServiceRegistrationApi,
|
|
6
|
+
resolveServiceRegistrations
|
|
7
|
+
} from "../registries/serviceRegistrationRegistry.js";
|
|
8
|
+
|
|
9
|
+
test("installServiceRegistrationApi exposes app.service and publishes declared events", async () => {
|
|
10
|
+
const app = createContainer();
|
|
11
|
+
const published = [];
|
|
12
|
+
app.singleton("domainEvents", () => ({
|
|
13
|
+
async publish(payload) {
|
|
14
|
+
published.push(payload);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
installServiceRegistrationApi(app);
|
|
20
|
+
assert.equal(typeof app.service, "function");
|
|
21
|
+
|
|
22
|
+
app.service(
|
|
23
|
+
"test.customers.service",
|
|
24
|
+
() => ({
|
|
25
|
+
async createRecord() {
|
|
26
|
+
return { id: 41 };
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
{
|
|
30
|
+
events: {
|
|
31
|
+
createRecord: [
|
|
32
|
+
{
|
|
33
|
+
type: "entity.changed",
|
|
34
|
+
source: "crud",
|
|
35
|
+
entity: "record",
|
|
36
|
+
operation: "created",
|
|
37
|
+
realtime: {
|
|
38
|
+
event: "customers.record.changed"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const service = app.make("test.customers.service");
|
|
47
|
+
const result = await service.createRecord({
|
|
48
|
+
context: {
|
|
49
|
+
actor: { id: 7 },
|
|
50
|
+
visibilityContext: {
|
|
51
|
+
scopeOwnerId: 13
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.equal(result.id, 41);
|
|
57
|
+
assert.equal(service.serviceEvents.createRecord[0].realtime.audience, "event_scope");
|
|
58
|
+
assert.equal(published.length, 1);
|
|
59
|
+
assert.equal(published[0].source, "crud");
|
|
60
|
+
assert.equal(published[0].operation, "created");
|
|
61
|
+
assert.equal(published[0].meta?.service?.token, "test.customers.service");
|
|
62
|
+
assert.equal(published[0].meta?.service?.method, "createRecord");
|
|
63
|
+
assert.equal(published[0].meta?.realtime?.event, "customers.record.changed");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("app.service rejects deprecated permissions metadata", () => {
|
|
67
|
+
const app = createContainer();
|
|
68
|
+
app.singleton("domainEvents", () => ({
|
|
69
|
+
async publish() {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
installServiceRegistrationApi(app);
|
|
74
|
+
|
|
75
|
+
assert.throws(
|
|
76
|
+
() =>
|
|
77
|
+
app.service(
|
|
78
|
+
"test.secure.service",
|
|
79
|
+
() => ({
|
|
80
|
+
async listRecords() {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
{
|
|
85
|
+
permissions: {
|
|
86
|
+
listRecords: {
|
|
87
|
+
require: "authenticated"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
),
|
|
92
|
+
/metadata\.permissions is no longer supported/
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("resolveServiceRegistrations returns declared service metadata", () => {
|
|
97
|
+
const app = createContainer();
|
|
98
|
+
app.singleton("domainEvents", () => ({
|
|
99
|
+
async publish() {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
installServiceRegistrationApi(app);
|
|
104
|
+
|
|
105
|
+
app.service(
|
|
106
|
+
"test.registry.service",
|
|
107
|
+
() => ({
|
|
108
|
+
async createRecord() {
|
|
109
|
+
return { id: 1 };
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const entries = resolveServiceRegistrations(app);
|
|
115
|
+
assert.equal(entries.length, 1);
|
|
116
|
+
assert.equal(entries[0].serviceToken, "test.registry.service");
|
|
117
|
+
assert.deepEqual(entries[0].metadata, {
|
|
118
|
+
events: {}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("app.service keeps realtime audience callbacks", () => {
|
|
123
|
+
const app = createContainer();
|
|
124
|
+
app.singleton("domainEvents", () => ({
|
|
125
|
+
async publish() {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}));
|
|
129
|
+
installServiceRegistrationApi(app);
|
|
130
|
+
|
|
131
|
+
const audience = () => ({ userId: 7 });
|
|
132
|
+
app.service(
|
|
133
|
+
"test.audience.service",
|
|
134
|
+
() => ({
|
|
135
|
+
async updateRecord() {
|
|
136
|
+
return { id: 2 };
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
{
|
|
140
|
+
events: {
|
|
141
|
+
updateRecord: [
|
|
142
|
+
{
|
|
143
|
+
type: "entity.changed",
|
|
144
|
+
source: "crud",
|
|
145
|
+
entity: "record",
|
|
146
|
+
operation: "updated",
|
|
147
|
+
realtime: {
|
|
148
|
+
event: "customers.record.changed",
|
|
149
|
+
audience
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const service = app.make("test.audience.service");
|
|
158
|
+
assert.equal(typeof service.serviceEvents.updateRecord[0].realtime.audience, "function");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("app.service accepts opaque realtime audience string presets", () => {
|
|
162
|
+
const app = createContainer();
|
|
163
|
+
app.singleton("domainEvents", () => ({
|
|
164
|
+
async publish() {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
installServiceRegistrationApi(app);
|
|
169
|
+
|
|
170
|
+
app.service(
|
|
171
|
+
"test.opaque.audience.service",
|
|
172
|
+
() => ({
|
|
173
|
+
async updateRecord() {
|
|
174
|
+
return { id: 2 };
|
|
175
|
+
}
|
|
176
|
+
}),
|
|
177
|
+
{
|
|
178
|
+
events: {
|
|
179
|
+
updateRecord: [
|
|
180
|
+
{
|
|
181
|
+
type: "entity.changed",
|
|
182
|
+
source: "crud",
|
|
183
|
+
entity: "record",
|
|
184
|
+
operation: "updated",
|
|
185
|
+
realtime: {
|
|
186
|
+
event: "customers.record.changed",
|
|
187
|
+
audience: "workspace_member"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const service = app.make("test.opaque.audience.service");
|
|
196
|
+
assert.equal(service.serviceEvents.updateRecord[0].realtime.audience, "workspace_member");
|
|
197
|
+
});
|