@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.
- package/client/appConfig.js +2 -1
- package/client/asyncModuleRecovery.js +2 -4
- package/client/index.d.ts +18 -76
- package/client/index.js +7 -2
- package/client/logging.js +8 -2
- package/client/moduleBootstrap.js +41 -33
- package/client/moduleBootstrap.test.js +35 -0
- package/client/shellBootstrap.js +38 -10
- package/client/shellBootstrap.test.js +85 -6
- 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 +16 -20
- package/shared/support/returnToPath.test.js +5 -33
- 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 +5 -8
- 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,230 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { normalizeIncomingAppUrl, registerMobileLaunchRouting } from "./mobileLaunchRouting.js";
|
|
4
|
-
|
|
5
|
-
test("normalizeIncomingAppUrl normalizes custom-scheme auth callback routes into router paths", () => {
|
|
6
|
-
const normalized = normalizeIncomingAppUrl("convict://auth/login?code=abc&oauthProvider=google", {
|
|
7
|
-
enabled: true,
|
|
8
|
-
auth: {
|
|
9
|
-
customScheme: "convict"
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
assert.equal(normalized, "/auth/login?code=abc&oauthProvider=google");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("normalizeIncomingAppUrl normalizes custom-scheme workspace routes", () => {
|
|
17
|
-
const normalized = normalizeIncomingAppUrl("convict://w/acme/workouts/2026-05-07?tab=today", {
|
|
18
|
-
enabled: true,
|
|
19
|
-
auth: {
|
|
20
|
-
customScheme: "convict"
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
assert.equal(normalized, "/w/acme/workouts/2026-05-07?tab=today");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("normalizeIncomingAppUrl normalizes allowed HTTPS app links", () => {
|
|
28
|
-
const normalized = normalizeIncomingAppUrl("https://app.example.com/auth/login?code=abc", {
|
|
29
|
-
enabled: true,
|
|
30
|
-
auth: {
|
|
31
|
-
appLinkDomains: ["app.example.com"]
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
assert.equal(normalized, "/auth/login?code=abc");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("normalizeIncomingAppUrl accepts same-origin HTTP URLs when explicitly allowed", () => {
|
|
39
|
-
const normalized = normalizeIncomingAppUrl(
|
|
40
|
-
"http://192.168.1.10:5173/w/acme",
|
|
41
|
-
{
|
|
42
|
-
enabled: true
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
allowedHttpOrigins: ["http://192.168.1.10:5173"]
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
assert.equal(normalized, "/w/acme");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("normalizeIncomingAppUrl rejects unowned schemes and domains", () => {
|
|
53
|
-
assert.equal(
|
|
54
|
-
normalizeIncomingAppUrl("otherapp://auth/login?code=abc", {
|
|
55
|
-
enabled: true,
|
|
56
|
-
auth: {
|
|
57
|
-
customScheme: "convict"
|
|
58
|
-
}
|
|
59
|
-
}),
|
|
60
|
-
""
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
assert.equal(
|
|
64
|
-
normalizeIncomingAppUrl("https://evil.example.com/auth/login?code=abc", {
|
|
65
|
-
enabled: true,
|
|
66
|
-
auth: {
|
|
67
|
-
appLinkDomains: ["app.example.com"]
|
|
68
|
-
}
|
|
69
|
-
}),
|
|
70
|
-
""
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("registerMobileLaunchRouting initializes and applies the initial launch URL", async () => {
|
|
75
|
-
const replaceCalls = [];
|
|
76
|
-
const runtime = registerMobileLaunchRouting({
|
|
77
|
-
router: {
|
|
78
|
-
currentRoute: {
|
|
79
|
-
value: {
|
|
80
|
-
fullPath: "/home"
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
async replace(target) {
|
|
84
|
-
replaceCalls.push(target);
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
mobileConfig: {
|
|
88
|
-
enabled: true,
|
|
89
|
-
auth: {
|
|
90
|
-
customScheme: "convict"
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
getInitialLaunchUrl: async () => "convict://auth/login?code=abc"
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const targetPath = await runtime.initialize();
|
|
97
|
-
|
|
98
|
-
assert.equal(targetPath, "/auth/login?code=abc");
|
|
99
|
-
assert.deepEqual(replaceCalls, ["/auth/login?code=abc"]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("registerMobileLaunchRouting subscribes to later launch URLs and routes them", async () => {
|
|
103
|
-
const replaceCalls = [];
|
|
104
|
-
let listener = null;
|
|
105
|
-
const runtime = registerMobileLaunchRouting({
|
|
106
|
-
router: {
|
|
107
|
-
currentRoute: {
|
|
108
|
-
value: {
|
|
109
|
-
fullPath: "/home"
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
async replace(target) {
|
|
113
|
-
replaceCalls.push(target);
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
mobileConfig: {
|
|
117
|
-
enabled: true,
|
|
118
|
-
auth: {
|
|
119
|
-
customScheme: "convict"
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
subscribeToLaunchUrls(handler) {
|
|
123
|
-
listener = handler;
|
|
124
|
-
return () => {
|
|
125
|
-
listener = null;
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
assert.equal(typeof listener, "function");
|
|
131
|
-
listener("convict://w/acme");
|
|
132
|
-
await Promise.resolve();
|
|
133
|
-
await Promise.resolve();
|
|
134
|
-
assert.deepEqual(replaceCalls, ["/w/acme"]);
|
|
135
|
-
|
|
136
|
-
runtime.dispose();
|
|
137
|
-
assert.equal(listener, null);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("registerMobileLaunchRouting lets a resolver override the final route target", async () => {
|
|
141
|
-
const replaceCalls = [];
|
|
142
|
-
const runtime = registerMobileLaunchRouting({
|
|
143
|
-
router: {
|
|
144
|
-
currentRoute: {
|
|
145
|
-
value: {
|
|
146
|
-
fullPath: "/home"
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
async replace(target) {
|
|
150
|
-
replaceCalls.push(target);
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
mobileConfig: {
|
|
154
|
-
enabled: true,
|
|
155
|
-
auth: {
|
|
156
|
-
customScheme: "convict"
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
getInitialLaunchUrl: async () => "convict://auth/login?code=abc",
|
|
160
|
-
resolveTargetPath({ originalUrl, normalizedTargetPath, reason }) {
|
|
161
|
-
assert.equal(originalUrl, "convict://auth/login?code=abc");
|
|
162
|
-
assert.equal(normalizedTargetPath, "/auth/login?code=abc");
|
|
163
|
-
assert.equal(reason, "initial-launch");
|
|
164
|
-
return "/w/acme";
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
const targetPath = await runtime.initialize();
|
|
169
|
-
|
|
170
|
-
assert.equal(targetPath, "/w/acme");
|
|
171
|
-
assert.deepEqual(replaceCalls, ["/w/acme"]);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("registerMobileLaunchRouting forwards unknown deep-link paths to the normal router", async () => {
|
|
175
|
-
const replaceCalls = [];
|
|
176
|
-
const runtime = registerMobileLaunchRouting({
|
|
177
|
-
router: {
|
|
178
|
-
currentRoute: {
|
|
179
|
-
value: {
|
|
180
|
-
fullPath: "/home"
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
async replace(target) {
|
|
184
|
-
replaceCalls.push(target);
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
mobileConfig: {
|
|
188
|
-
enabled: true,
|
|
189
|
-
auth: {
|
|
190
|
-
customScheme: "convict"
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
getInitialLaunchUrl: async () => "convict://w/acme/does-not-exist"
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const targetPath = await runtime.initialize();
|
|
197
|
-
|
|
198
|
-
assert.equal(targetPath, "/w/acme/does-not-exist");
|
|
199
|
-
assert.deepEqual(replaceCalls, ["/w/acme/does-not-exist"]);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("registerMobileLaunchRouting no-ops when mobile config is disabled", async () => {
|
|
203
|
-
const replaceCalls = [];
|
|
204
|
-
const runtime = registerMobileLaunchRouting({
|
|
205
|
-
router: {
|
|
206
|
-
currentRoute: {
|
|
207
|
-
value: {
|
|
208
|
-
fullPath: "/home"
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
async replace(target) {
|
|
212
|
-
replaceCalls.push(target);
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
mobileConfig: {
|
|
216
|
-
enabled: false,
|
|
217
|
-
auth: {
|
|
218
|
-
customScheme: "convict"
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
getInitialLaunchUrl: async () => "convict://auth/login?code=abc",
|
|
222
|
-
subscribeToLaunchUrls() {
|
|
223
|
-
throw new Error("subscribeToLaunchUrls should not be called when mobile is disabled");
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const targetPath = await runtime.initialize();
|
|
228
|
-
assert.equal(targetPath, "");
|
|
229
|
-
assert.deepEqual(replaceCalls, []);
|
|
230
|
-
});
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
const DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE = Object.freeze({
|
|
2
|
-
domain_validation_failed: 422,
|
|
3
|
-
validation_failed: 422,
|
|
4
|
-
invalid_input: 422,
|
|
5
|
-
duplicate: 409,
|
|
6
|
-
conflict: 409,
|
|
7
|
-
not_found: 404,
|
|
8
|
-
forbidden: 403,
|
|
9
|
-
unauthorized: 401,
|
|
10
|
-
rate_limited: 429
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
function normalizeStatus(status, fallbackStatus) {
|
|
14
|
-
const numeric = Number(status);
|
|
15
|
-
if (Number.isInteger(numeric) && numeric >= 100 && numeric <= 599) {
|
|
16
|
-
return numeric;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const fallbackNumeric = Number(fallbackStatus);
|
|
20
|
-
if (Number.isInteger(fallbackNumeric) && fallbackNumeric >= 100 && fallbackNumeric <= 599) {
|
|
21
|
-
return fallbackNumeric;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return 500;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeCode(code) {
|
|
28
|
-
const normalized = String(code || "")
|
|
29
|
-
.trim()
|
|
30
|
-
.toLowerCase();
|
|
31
|
-
return normalized || "";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function resolveDomainErrorStatus({
|
|
35
|
-
status,
|
|
36
|
-
code,
|
|
37
|
-
domainErrorStatusByCode = DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE,
|
|
38
|
-
fallbackStatus = 422
|
|
39
|
-
} = {}) {
|
|
40
|
-
const explicitStatus = Number(status);
|
|
41
|
-
if (Number.isInteger(explicitStatus) && explicitStatus >= 100 && explicitStatus <= 599) {
|
|
42
|
-
return explicitStatus;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const normalizedCode = normalizeCode(code);
|
|
46
|
-
if (normalizedCode) {
|
|
47
|
-
const mapSource =
|
|
48
|
-
domainErrorStatusByCode && typeof domainErrorStatusByCode === "object"
|
|
49
|
-
? domainErrorStatusByCode
|
|
50
|
-
: DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE;
|
|
51
|
-
const mappedStatus = Number(mapSource[normalizedCode]);
|
|
52
|
-
if (Number.isInteger(mappedStatus) && mappedStatus >= 100 && mappedStatus <= 599) {
|
|
53
|
-
return mappedStatus;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return normalizeStatus(fallbackStatus, 422);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function ensureReply(reply, methodName) {
|
|
61
|
-
if (!reply || typeof reply.code !== "function" || typeof reply.send !== "function") {
|
|
62
|
-
throw new TypeError(`${methodName} requires a Fastify reply-like object with code() and send().`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function applyHeaders(reply, headers) {
|
|
67
|
-
if (!headers || typeof headers !== "object" || typeof reply.header !== "function") {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
72
|
-
reply.header(name, value);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
class BaseController {
|
|
77
|
-
constructor({ domainErrorStatusByCode = DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE } = {}) {
|
|
78
|
-
this.domainErrorStatusByCode =
|
|
79
|
-
domainErrorStatusByCode && typeof domainErrorStatusByCode === "object"
|
|
80
|
-
? { ...DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE, ...domainErrorStatusByCode }
|
|
81
|
-
: { ...DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
ok(reply, payload, { status = 200, headers = null } = {}) {
|
|
85
|
-
ensureReply(reply, "BaseController.ok");
|
|
86
|
-
applyHeaders(reply, headers);
|
|
87
|
-
|
|
88
|
-
const normalizedStatus = normalizeStatus(status, 200);
|
|
89
|
-
reply.code(normalizedStatus);
|
|
90
|
-
|
|
91
|
-
if (normalizedStatus === 204) {
|
|
92
|
-
return reply.send();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return reply.send(payload);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
created(reply, payload, { headers = null } = {}) {
|
|
99
|
-
return this.ok(reply, payload, {
|
|
100
|
-
status: 201,
|
|
101
|
-
headers
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
noContent(reply, { headers = null } = {}) {
|
|
106
|
-
return this.ok(reply, undefined, {
|
|
107
|
-
status: 204,
|
|
108
|
-
headers
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
fail(
|
|
113
|
-
reply,
|
|
114
|
-
{ status, code, message = "Request failed.", details, fieldErrors, headers = null, fallbackStatus = 422 } = {}
|
|
115
|
-
) {
|
|
116
|
-
ensureReply(reply, "BaseController.fail");
|
|
117
|
-
|
|
118
|
-
const resolvedStatus = resolveDomainErrorStatus({
|
|
119
|
-
status,
|
|
120
|
-
code,
|
|
121
|
-
domainErrorStatusByCode: this.domainErrorStatusByCode,
|
|
122
|
-
fallbackStatus
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const payload = {
|
|
126
|
-
error: String(message || "Request failed.")
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const normalizedCode = normalizeCode(code);
|
|
130
|
-
if (normalizedCode) {
|
|
131
|
-
payload.code = String(code || "").trim();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (details !== undefined) {
|
|
135
|
-
payload.details = details;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (fieldErrors !== undefined) {
|
|
139
|
-
payload.fieldErrors = fieldErrors;
|
|
140
|
-
if (payload.details === undefined) {
|
|
141
|
-
payload.details = { fieldErrors };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
applyHeaders(reply, headers);
|
|
146
|
-
reply.code(resolvedStatus);
|
|
147
|
-
return reply.send(payload);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
sendActionResult(
|
|
151
|
-
reply,
|
|
152
|
-
result,
|
|
153
|
-
{ successStatus = 200, defaultErrorStatus = 422, defaultErrorMessage = "Request failed." } = {}
|
|
154
|
-
) {
|
|
155
|
-
if (!result || typeof result !== "object") {
|
|
156
|
-
throw new TypeError("BaseController.sendActionResult requires an object result.");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (result.ok === true) {
|
|
160
|
-
const payload = Object.prototype.hasOwnProperty.call(result, "data") ? result.data : undefined;
|
|
161
|
-
return this.ok(reply, payload, {
|
|
162
|
-
status: result.status ?? successStatus,
|
|
163
|
-
headers: result.headers
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (result.ok === false) {
|
|
168
|
-
return this.fail(reply, {
|
|
169
|
-
status: result.status,
|
|
170
|
-
code: result.code,
|
|
171
|
-
message: result.message || defaultErrorMessage,
|
|
172
|
-
details: result.details,
|
|
173
|
-
fieldErrors: result.fieldErrors,
|
|
174
|
-
headers: result.headers,
|
|
175
|
-
fallbackStatus: defaultErrorStatus
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
throw new TypeError("BaseController.sendActionResult expects result.ok to be true or false.");
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export { BaseController, DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE, resolveDomainErrorStatus };
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
|
|
4
|
-
import { BaseController, resolveDomainErrorStatus } from "./controller.js";
|
|
5
|
-
|
|
6
|
-
function createReplyStub() {
|
|
7
|
-
const state = {
|
|
8
|
-
statusCode: 200,
|
|
9
|
-
payload: undefined,
|
|
10
|
-
headers: {}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
state,
|
|
15
|
-
code(statusCode) {
|
|
16
|
-
state.statusCode = Number(statusCode);
|
|
17
|
-
return this;
|
|
18
|
-
},
|
|
19
|
-
header(name, value) {
|
|
20
|
-
state.headers[name] = value;
|
|
21
|
-
return this;
|
|
22
|
-
},
|
|
23
|
-
send(payload) {
|
|
24
|
-
state.payload = payload;
|
|
25
|
-
return this;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
test("resolveDomainErrorStatus prefers explicit status", () => {
|
|
31
|
-
const status = resolveDomainErrorStatus({
|
|
32
|
-
status: 418,
|
|
33
|
-
code: "duplicate"
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
assert.equal(status, 418);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("resolveDomainErrorStatus maps known domain codes", () => {
|
|
40
|
-
assert.equal(resolveDomainErrorStatus({ code: "duplicate" }), 409);
|
|
41
|
-
assert.equal(resolveDomainErrorStatus({ code: "domain_validation_failed" }), 422);
|
|
42
|
-
assert.equal(resolveDomainErrorStatus({ code: "not_found" }), 404);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("BaseController.sendActionResult sends success payload and headers", () => {
|
|
46
|
-
const controller = new BaseController();
|
|
47
|
-
const reply = createReplyStub();
|
|
48
|
-
|
|
49
|
-
controller.sendActionResult(reply, {
|
|
50
|
-
ok: true,
|
|
51
|
-
data: { ok: true, value: 42 },
|
|
52
|
-
status: 201,
|
|
53
|
-
headers: {
|
|
54
|
-
"x-test": "success"
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
assert.equal(reply.state.statusCode, 201);
|
|
59
|
-
assert.deepEqual(reply.state.payload, { ok: true, value: 42 });
|
|
60
|
-
assert.equal(reply.state.headers["x-test"], "success");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("BaseController.sendActionResult maps domain failure", () => {
|
|
64
|
-
const controller = new BaseController();
|
|
65
|
-
const reply = createReplyStub();
|
|
66
|
-
|
|
67
|
-
controller.sendActionResult(reply, {
|
|
68
|
-
ok: false,
|
|
69
|
-
code: "duplicate",
|
|
70
|
-
details: ["already exists"]
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
assert.equal(reply.state.statusCode, 409);
|
|
74
|
-
assert.deepEqual(reply.state.payload, {
|
|
75
|
-
error: "Request failed.",
|
|
76
|
-
code: "duplicate",
|
|
77
|
-
details: ["already exists"]
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("BaseController supports module-specific domain error codes through overrides", () => {
|
|
82
|
-
const controller = new BaseController({
|
|
83
|
-
domainErrorStatusByCode: {
|
|
84
|
-
duplicate_contact: 409
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
const reply = createReplyStub();
|
|
88
|
-
|
|
89
|
-
controller.sendActionResult(reply, {
|
|
90
|
-
ok: false,
|
|
91
|
-
code: "duplicate_contact",
|
|
92
|
-
details: ["already exists"]
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
assert.equal(reply.state.statusCode, 409);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("BaseController.fail includes fieldErrors and default details shape", () => {
|
|
99
|
-
const controller = new BaseController();
|
|
100
|
-
const reply = createReplyStub();
|
|
101
|
-
|
|
102
|
-
controller.fail(reply, {
|
|
103
|
-
code: "domain_validation_failed",
|
|
104
|
-
message: "Validation failed.",
|
|
105
|
-
fieldErrors: {
|
|
106
|
-
email: "Invalid email."
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
assert.equal(reply.state.statusCode, 422);
|
|
111
|
-
assert.deepEqual(reply.state.payload, {
|
|
112
|
-
error: "Validation failed.",
|
|
113
|
-
code: "domain_validation_failed",
|
|
114
|
-
fieldErrors: {
|
|
115
|
-
email: "Invalid email."
|
|
116
|
-
},
|
|
117
|
-
details: {
|
|
118
|
-
fieldErrors: {
|
|
119
|
-
email: "Invalid email."
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("BaseController.noContent sends HTTP 204", () => {
|
|
126
|
-
const controller = new BaseController();
|
|
127
|
-
const reply = createReplyStub();
|
|
128
|
-
|
|
129
|
-
controller.noContent(reply);
|
|
130
|
-
|
|
131
|
-
assert.equal(reply.state.statusCode, 204);
|
|
132
|
-
assert.equal(reply.state.payload, undefined);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("BaseController.sendActionResult rejects invalid result shape", () => {
|
|
136
|
-
const controller = new BaseController();
|
|
137
|
-
const reply = createReplyStub();
|
|
138
|
-
|
|
139
|
-
assert.throws(
|
|
140
|
-
() => controller.sendActionResult(reply, { data: { ok: true } }),
|
|
141
|
-
/expects result\.ok to be true or false/
|
|
142
|
-
);
|
|
143
|
-
});
|
package/server/http/lib/index.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { HttpKernelError, RouteDefinitionError, RouteRegistrationError } from "./errors.js";
|
|
2
|
-
export { HttpRouter, createRouter, joinPath } from "./router.js";
|
|
3
|
-
export { defineRouteValidator, compileRouteValidator, resolveRouteValidatorOptions } from "./routeValidator.js";
|
|
4
|
-
export { BaseController, DEFAULT_DOMAIN_ERROR_STATUS_BY_CODE, resolveDomainErrorStatus } from "./controller.js";
|
|
5
|
-
export { defaultMissingHandler, registerRoutes } from "./routeRegistration.js";
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
|
|
3
|
-
function isPlainObject(value) {
|
|
4
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && value.constructor === Object;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function sortValue(value) {
|
|
8
|
-
if (Array.isArray(value)) {
|
|
9
|
-
return value.map(sortValue);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (isPlainObject(value)) {
|
|
13
|
-
const sorted = {};
|
|
14
|
-
for (const key of Object.keys(value).sort()) {
|
|
15
|
-
const next = value[key];
|
|
16
|
-
if (next === undefined) {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
sorted[key] = sortValue(next);
|
|
20
|
-
}
|
|
21
|
-
return sorted;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function toCanonicalJson(value) {
|
|
28
|
-
return JSON.stringify(sortValue(value));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function toSha256Hex(value) {
|
|
32
|
-
const source = String(value || "");
|
|
33
|
-
return crypto.createHash("sha256").update(source).digest("hex");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function toHmacSha256Hex(secret, value) {
|
|
37
|
-
const normalizedSecret = String(secret || "").trim();
|
|
38
|
-
if (!normalizedSecret) {
|
|
39
|
-
throw new Error("HMAC secret is required.");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return crypto
|
|
43
|
-
.createHmac("sha256", normalizedSecret)
|
|
44
|
-
.update(String(value || ""))
|
|
45
|
-
.digest("hex");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function safeParseJson(value, fallback = null) {
|
|
49
|
-
if (value == null) {
|
|
50
|
-
return fallback;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (typeof value === "object") {
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const text = String(value || "").trim();
|
|
58
|
-
if (!text) {
|
|
59
|
-
return fallback;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
return JSON.parse(text);
|
|
64
|
-
} catch {
|
|
65
|
-
return fallback;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const __testables = {
|
|
70
|
-
isPlainObject,
|
|
71
|
-
sortValue
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export { toCanonicalJson, toSha256Hex, toHmacSha256Hex, safeParseJson, __testables };
|