cabloy 5.1.216 → 5.1.217
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/.cabloy-version +1 -1
- package/.github/workflows/playwright-e2e.yml +163 -13
- package/.github/workflows/vona-test-sqlite3.yml +77 -7
- package/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +4 -2
- package/package.json +1 -1
- package/repo-docs/frontend/scripts.md +2 -0
- package/repo-docs/reference/repo-scripts.md +27 -1
- package/repo-e2e/scripts/e2e.test.ts +1 -1
- package/repo-e2e/scripts/managedTest.test.ts +115 -3
- package/repo-e2e/scripts/managedTest.ts +69 -20
- package/repo-e2e/scripts/managedTestImports.test.ts +55 -5
- package/repo-e2e/scripts/runE2e.test.ts +109 -0
- package/repo-e2e/scripts/runE2e.ts +58 -39
- package/repo-e2e/scripts/runE2eArgs.test.ts +16 -1
- package/repo-e2e/scripts/runE2eArgs.ts +5 -0
- package/repo-e2e/specs/a-commerce.spec.ts +10 -2
- package/repo-e2e/specs/home-user-account.spec.ts +4 -0
- package/repo-e2e/specs/markdown.spec.ts +14 -3
- package/scripts/generateOpenapiIsolated.test.ts +5 -1
- package/scripts/runIsolatedTest.test.ts +403 -36
- package/scripts/runIsolatedTest.ts +139 -12
- package/scripts/testBrowserGuardianWindows.test.ts +327 -0
- package/scripts/testClaimCleanup.test.ts +25 -9
- package/scripts/testClaimCleanup.ts +14 -45
- package/scripts/testClaimProcesses.test.ts +81 -2
- package/scripts/testClaimProcesses.ts +46 -10
- package/scripts/testProcessGuardianWindows.test.ts +602 -0
- package/scripts/testProcessGuardianWindows.ts +155 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +4 -1
- package/vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts +159 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +298 -17
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +205 -97
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts +247 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.ts +224 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.cs +229 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ps1 +20 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ts +200 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.cs +336 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ps1 +20 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts +531 -0
- package/vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts +61 -0
- package/vona/packages-vona/vona-core/src/lib/framework/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processWorker.test.ts +59 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processWorker.ts +8 -5
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts +84 -0
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +2 -2
- package/vona/packages-vona/vona-core/src/lib/utils/util.ts +1 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +22 -22
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +2 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +16 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +6 -3
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +80 -0
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +73 -2
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts +309 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transactionAsyncLocalStorage_.ts +3 -3
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-static/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-static/src/bean/middlewareSystem.static.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-static/test/middlewareSystem.static.test.ts +101 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +2 -2
- package/zova/packages-utils/zova-vite/package.json +1 -1
- package/zova/packages-utils/zova-vite/src/configUtils.ts +1 -1
- package/zova/packages-utils/zova-vite/test/configUtils.test.ts +55 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +8 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/test/lib/cartModel.test.ts +71 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
|
|
5
|
+
const useAppUrl = new URL('./useApp.ts', import.meta.url).href;
|
|
6
|
+
|
|
7
|
+
for (const platform of ['win32', 'linux', 'darwin']) {
|
|
8
|
+
test(`general apps use the single-app entry and await close on ${platform}`, () => {
|
|
9
|
+
const source = `
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { registerHooks } from 'node:module';
|
|
15
|
+
const useAppUrl = ${JSON.stringify(useAppUrl)};
|
|
16
|
+
const projectPath = mkdtempSync(path.join(tmpdir(), 'vona-general-app-'));
|
|
17
|
+
const appSource =
|
|
18
|
+
'export async function createSingleApp() {' +
|
|
19
|
+
' if (globalThis.failure) throw globalThis.failure;' +
|
|
20
|
+
' globalThis.factoryCalls++;' +
|
|
21
|
+
' return globalThis.__app__ = { meta: { async close() {' +
|
|
22
|
+
' await new Promise(resolve => setTimeout(resolve, 10));' +
|
|
23
|
+
' globalThis.closed = true;' +
|
|
24
|
+
' } } };' +
|
|
25
|
+
'}';
|
|
26
|
+
try {
|
|
27
|
+
writeFileSync(path.join(projectPath, 'package.json'), '{"type":"module"}');
|
|
28
|
+
for (const flavor of ['ci', 'other']) {
|
|
29
|
+
const directory = path.join(projectPath, 'dist', flavor);
|
|
30
|
+
mkdirSync(directory, { recursive: true });
|
|
31
|
+
writeFileSync(path.join(directory, 'app.js'), appSource);
|
|
32
|
+
writeFileSync(path.join(directory, 'bootstrap.js'), 'throw new Error("General apps must not bootstrap a server");');
|
|
33
|
+
}
|
|
34
|
+
const generatedRoot = path.join(projectPath, '.vona');
|
|
35
|
+
mkdirSync(generatedRoot);
|
|
36
|
+
writeFileSync(path.join(generatedRoot, 'app.ts'), appSource);
|
|
37
|
+
const mocks = {
|
|
38
|
+
'@cabloy/utils': 'export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));',
|
|
39
|
+
'../utils/util.ts': 'import { pathToFileURL } from "node:url"; export const pathToHref = file => pathToFileURL(file).href;',
|
|
40
|
+
'../core/testIsolation.ts': 'export function resolveVonaLocalPaths(projectPath, mode) { globalThis.resolvedMode = mode; return { generatedRoot: globalThis.generatedRoot }; }',
|
|
41
|
+
};
|
|
42
|
+
registerHooks({
|
|
43
|
+
resolve(specifier, context, nextResolve) {
|
|
44
|
+
if (context.parentURL === useAppUrl && mocks[specifier]) {
|
|
45
|
+
return { url: 'data:text/javascript,' + encodeURIComponent(mocks[specifier]), shortCircuit: true };
|
|
46
|
+
}
|
|
47
|
+
return nextResolve(specifier, context);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(process, 'platform', { value: ${JSON.stringify(platform)} });
|
|
51
|
+
globalThis.generatedRoot = generatedRoot;
|
|
52
|
+
globalThis.factoryCalls = 0;
|
|
53
|
+
const { createGeneralApp, closeApp, useApp } = await import(useAppUrl);
|
|
54
|
+
for (const mode of ['prod', 'dev', 'test']) {
|
|
55
|
+
globalThis.closed = false;
|
|
56
|
+
const app = await createGeneralApp(projectPath, { META_MODE: mode, META_FLAVOR: 'ci' });
|
|
57
|
+
assert.equal(app, useApp());
|
|
58
|
+
assert.equal(globalThis.factoryCalls, 1);
|
|
59
|
+
assert.equal(globalThis.closed, false);
|
|
60
|
+
if (mode !== 'prod') assert.equal(globalThis.resolvedMode, mode);
|
|
61
|
+
await closeApp();
|
|
62
|
+
assert.equal(globalThis.closed, true, 'close must await application cleanup');
|
|
63
|
+
assert.equal(useApp(), undefined);
|
|
64
|
+
globalThis.factoryCalls = 0;
|
|
65
|
+
}
|
|
66
|
+
const failure = globalThis.failure = new Error('single-app startup failed');
|
|
67
|
+
await assert.rejects(
|
|
68
|
+
createGeneralApp(projectPath, { META_MODE: 'prod', META_FLAVOR: 'other' }),
|
|
69
|
+
error => error === failure,
|
|
70
|
+
);
|
|
71
|
+
assert.equal(useApp(), undefined);
|
|
72
|
+
} finally {
|
|
73
|
+
rmSync(projectPath, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', source], {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
timeout: 5_000,
|
|
79
|
+
});
|
|
80
|
+
assert.ifError(result.error);
|
|
81
|
+
assert.equal(result.signal, null, result.stderr);
|
|
82
|
+
assert.equal(result.status, 0, result.stderr);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -35,9 +35,9 @@ export async function createGeneralApp(
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
if (process.env.META_MODE === 'prod') {
|
|
38
|
-
const testFile = path.join(projectPath, `dist/${process.env.META_FLAVOR}/
|
|
38
|
+
const testFile = path.join(projectPath, `dist/${process.env.META_FLAVOR}/app.js`);
|
|
39
39
|
const testInstance = await import(pathToHref(testFile));
|
|
40
|
-
return await testInstance.
|
|
40
|
+
return await testInstance.createSingleApp();
|
|
41
41
|
} else {
|
|
42
42
|
const testFile = path.join(
|
|
43
43
|
resolveVonaLocalPaths(projectPath, process.env.META_MODE).generatedRoot,
|
|
@@ -122,7 +122,7 @@ export class AppUtil extends BeanSimple {
|
|
|
122
122
|
const globalPrefix = '/api/static';
|
|
123
123
|
if (!staticPath) staticPath = '';
|
|
124
124
|
// safe
|
|
125
|
-
staticPath = path.normalize(staticPath);
|
|
125
|
+
staticPath = path.posix.normalize(staticPath.replace(/\\/g, '/'));
|
|
126
126
|
if (staticPath.includes('..')) throw new Error(`unsafe path: ${staticPath}`);
|
|
127
127
|
// ignore globalPrefix
|
|
128
128
|
if (staticPath.startsWith('//')) return staticPath.substring(1);
|
package/vona/pnpm-lock.yaml
CHANGED
|
@@ -573,7 +573,7 @@ importers:
|
|
|
573
573
|
specifier: npm:@cabloy/zod@4.3.8
|
|
574
574
|
version: '@cabloy/zod@4.3.8'
|
|
575
575
|
zova:
|
|
576
|
-
specifier: ^5.1.
|
|
576
|
+
specifier: ^5.1.171
|
|
577
577
|
version: 5.1.171(typescript@5.9.3)(vue@3.5.43(typescript@5.9.3))
|
|
578
578
|
zova-jsx:
|
|
579
579
|
specifier: ^1.1.92
|
|
@@ -606,8 +606,8 @@ importers:
|
|
|
606
606
|
specifier: ^5.1.38
|
|
607
607
|
version: 5.1.38(vue@3.5.43(typescript@5.9.3))
|
|
608
608
|
zova-module-a-openapi:
|
|
609
|
-
specifier:
|
|
610
|
-
version:
|
|
609
|
+
specifier: ^5.1.57
|
|
610
|
+
version: 5.1.57
|
|
611
611
|
zova-module-a-pay:
|
|
612
612
|
specifier: ^5.0.9
|
|
613
613
|
version: 5.0.9
|
|
@@ -741,8 +741,8 @@ importers:
|
|
|
741
741
|
specifier: npm:@cabloy/zod@4.3.8
|
|
742
742
|
version: '@cabloy/zod@4.3.8'
|
|
743
743
|
zova:
|
|
744
|
-
specifier: ^5.1.
|
|
745
|
-
version: 5.1.
|
|
744
|
+
specifier: ^5.1.171
|
|
745
|
+
version: 5.1.171(typescript@5.9.3)(vue@3.5.43(typescript@5.9.3))
|
|
746
746
|
zova-jsx:
|
|
747
747
|
specifier: ^1.1.92
|
|
748
748
|
version: 1.1.92(typescript@5.9.3)
|
|
@@ -759,8 +759,8 @@ importers:
|
|
|
759
759
|
specifier: ^5.1.35
|
|
760
760
|
version: 5.1.35
|
|
761
761
|
zova-module-a-form:
|
|
762
|
-
specifier: ^5.1.
|
|
763
|
-
version: 5.1.
|
|
762
|
+
specifier: ^5.1.54
|
|
763
|
+
version: 5.1.54(vue@3.5.43(typescript@5.9.3))
|
|
764
764
|
zova-module-a-icon:
|
|
765
765
|
specifier: ^5.1.27
|
|
766
766
|
version: 5.1.27
|
|
@@ -771,20 +771,20 @@ importers:
|
|
|
771
771
|
specifier: ^5.0.3
|
|
772
772
|
version: 5.0.3
|
|
773
773
|
zova-module-a-model:
|
|
774
|
-
specifier: ^5.1.
|
|
775
|
-
version: 5.1.
|
|
774
|
+
specifier: ^5.1.38
|
|
775
|
+
version: 5.1.38(vue@3.5.43(typescript@5.9.3))
|
|
776
776
|
zova-module-a-openapi:
|
|
777
|
-
specifier: ^5.1.
|
|
778
|
-
version: 5.1.
|
|
777
|
+
specifier: ^5.1.57
|
|
778
|
+
version: 5.1.57
|
|
779
779
|
zova-module-a-pay:
|
|
780
780
|
specifier: ^5.0.9
|
|
781
781
|
version: 5.0.9
|
|
782
782
|
zova-module-a-router:
|
|
783
|
-
specifier: ^5.1.
|
|
784
|
-
version: 5.1.
|
|
783
|
+
specifier: ^5.1.42
|
|
784
|
+
version: 5.1.42
|
|
785
785
|
zova-module-a-routertabs:
|
|
786
|
-
specifier: ^5.1.
|
|
787
|
-
version: 5.1.
|
|
786
|
+
specifier: ^5.1.35
|
|
787
|
+
version: 5.1.35
|
|
788
788
|
zova-module-a-ssr:
|
|
789
789
|
specifier: ^5.1.40
|
|
790
790
|
version: 5.1.40
|
|
@@ -798,8 +798,8 @@ importers:
|
|
|
798
798
|
specifier: ^5.0.5
|
|
799
799
|
version: 5.0.5
|
|
800
800
|
zova-module-rest-resource:
|
|
801
|
-
specifier: ^5.1.
|
|
802
|
-
version: 5.1.
|
|
801
|
+
specifier: ^5.1.56
|
|
802
|
+
version: 5.1.56
|
|
803
803
|
|
|
804
804
|
.zova-rest/cabloy-commerce-admin:
|
|
805
805
|
dependencies:
|
|
@@ -15300,16 +15300,16 @@ snapshots:
|
|
|
15300
15300
|
zova-module-a-behaviors: 5.1.22
|
|
15301
15301
|
zova-module-a-command: 5.1.35
|
|
15302
15302
|
zova-module-a-fetch: 5.1.25
|
|
15303
|
-
zova-module-a-form: 5.1.
|
|
15303
|
+
zova-module-a-form: 5.1.54(vue@3.5.43(typescript@5.9.3))
|
|
15304
15304
|
zova-module-a-icon: 5.1.27
|
|
15305
15305
|
zova-module-a-interceptor: 5.1.31
|
|
15306
15306
|
zova-module-a-logger: 5.1.28
|
|
15307
15307
|
zova-module-a-meta: 5.1.22
|
|
15308
|
-
zova-module-a-model: 5.1.
|
|
15309
|
-
zova-module-a-openapi: 5.1.
|
|
15310
|
-
zova-module-a-router: 5.1.
|
|
15308
|
+
zova-module-a-model: 5.1.38(vue@3.5.43(typescript@5.9.3))
|
|
15309
|
+
zova-module-a-openapi: 5.1.57
|
|
15310
|
+
zova-module-a-router: 5.1.42
|
|
15311
15311
|
zova-module-a-routerstack: 5.1.29
|
|
15312
|
-
zova-module-a-routertabs: 5.1.
|
|
15312
|
+
zova-module-a-routertabs: 5.1.35
|
|
15313
15313
|
zova-module-a-ssr: 5.1.40
|
|
15314
15314
|
zova-module-a-ssrhmr: 5.1.23
|
|
15315
15315
|
zova-module-a-ssrserver: 5.1.26
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona-suite-a-cabloy",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.63",
|
|
4
4
|
"gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"vona-module-a-cabloy": "workspace:^",
|
|
11
11
|
"vona-module-a-layoutprofile": "^5.0.2",
|
|
12
12
|
"vona-module-a-markdown": "^5.0.3",
|
|
13
|
-
"vona-module-a-rbac": "^5.0.
|
|
13
|
+
"vona-module-a-rbac": "^5.0.12",
|
|
14
14
|
"vona-module-a-socket": "workspace:^",
|
|
15
15
|
"vona-module-a-ssr": "workspace:^",
|
|
16
16
|
"vona-module-a-ssrhmr": "workspace:^"
|
|
@@ -4,6 +4,7 @@ import type { IApiPathRecordMethodMap } from 'vona-module-a-web';
|
|
|
4
4
|
import { isNil } from '@cabloy/utils';
|
|
5
5
|
import { BeanBase } from 'vona';
|
|
6
6
|
import { Bean } from 'vona-module-a-bean';
|
|
7
|
+
import { ServiceTransactionAsyncLocalStorage } from 'vona-module-a-orm';
|
|
7
8
|
|
|
8
9
|
import type { INewCtxOptions, IPerformActionOptions } from '../types/executor.ts';
|
|
9
10
|
|
|
@@ -113,7 +114,7 @@ export class BeanExecutor extends BeanBase {
|
|
|
113
114
|
__delegateProperties(ctx, options.extraData);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
return await
|
|
117
|
+
return await this.bean._getBean(ServiceTransactionAsyncLocalStorage).run(fn, isolate);
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -22,7 +22,7 @@ export class MiddlewareSystemHttpLog extends BeanBase implements IMiddlewareSyst
|
|
|
22
22
|
req.headers,
|
|
23
23
|
ctx.path,
|
|
24
24
|
this.app.meta.env.VONA_TEST_RUN_ID,
|
|
25
|
-
'x-vona-test-challenge',
|
|
25
|
+
['x-vona-test-challenge', 'x-vona-test-stop-nonce', 'x-vona-test-stop-authorization'],
|
|
26
26
|
);
|
|
27
27
|
this.$loggerChild('req').http(() => JSON.stringify(req));
|
|
28
28
|
const profiler = this.$loggerChild('res').startTimer();
|
|
@@ -33,7 +33,7 @@ export class MiddlewareSystemHttpLog extends BeanBase implements IMiddlewareSyst
|
|
|
33
33
|
ctx.res.getHeaders(),
|
|
34
34
|
ctx.path,
|
|
35
35
|
this.app.meta.env.VONA_TEST_RUN_ID,
|
|
36
|
-
'x-vona-test-proof',
|
|
36
|
+
['x-vona-test-proof', 'x-vona-test-stop-proof'],
|
|
37
37
|
);
|
|
38
38
|
const res = {
|
|
39
39
|
url: ctx.req.url,
|
package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts
CHANGED
|
@@ -3,6 +3,22 @@ import { test } from 'node:test';
|
|
|
3
3
|
|
|
4
4
|
import { redactIsolatedReadyHeader } from './redactIsolatedReadyHeader.ts';
|
|
5
5
|
|
|
6
|
+
test('redacts isolated shutdown authorization and proof case-insensitively', () => {
|
|
7
|
+
const headers = {
|
|
8
|
+
'X-Vona-Test-Stop-Authorization': 'authorization-value',
|
|
9
|
+
'x-vona-test-stop-nonce': 'nonce-value',
|
|
10
|
+
'X-VONA-TEST-STOP-PROOF': 'proof-value',
|
|
11
|
+
'content-type': 'application/json',
|
|
12
|
+
};
|
|
13
|
+
const logged = redactIsolatedReadyHeader(headers, '/health/test-stop', 'managed-run', [
|
|
14
|
+
'x-vona-test-stop-authorization',
|
|
15
|
+
'x-vona-test-stop-nonce',
|
|
16
|
+
'x-vona-test-stop-proof',
|
|
17
|
+
]);
|
|
18
|
+
assert.deepEqual(logged, { 'content-type': 'application/json' });
|
|
19
|
+
assert.equal(headers['X-Vona-Test-Stop-Authorization'], 'authorization-value');
|
|
20
|
+
});
|
|
21
|
+
|
|
6
22
|
test('redacts isolated readiness challenge and proof without changing live headers', () => {
|
|
7
23
|
const runId = '20261014010203-a1b2c3d4';
|
|
8
24
|
const req = { 'x-vona-test-challenge': 'challenge-value', 'accept': 'application/json' };
|
|
@@ -2,10 +2,13 @@ export function redactIsolatedReadyHeader<HEADERS extends Record<string, unknown
|
|
|
2
2
|
headers: HEADERS,
|
|
3
3
|
path: string,
|
|
4
4
|
runId: string | undefined,
|
|
5
|
-
header: string,
|
|
5
|
+
header: string | string[],
|
|
6
6
|
): HEADERS {
|
|
7
|
-
if (
|
|
7
|
+
if (!['/health/ready', '/health/test-stop'].includes(path) || !runId) return headers;
|
|
8
8
|
const redacted = { ...headers };
|
|
9
|
-
|
|
9
|
+
const names = (Array.isArray(header) ? header : [header]).map(name => name.toLowerCase());
|
|
10
|
+
for (const name of Object.keys(redacted)) {
|
|
11
|
+
if (names.includes(name.toLowerCase())) delete redacted[name];
|
|
12
|
+
}
|
|
10
13
|
return redacted;
|
|
11
14
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { registerHooks } from 'node:module';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
|
|
7
|
+
const middlewareUrl = new URL(
|
|
8
|
+
'../src/bean/middlewareSystem.httpLog.ts?fixture=httpLog',
|
|
9
|
+
import.meta.url,
|
|
10
|
+
).href;
|
|
11
|
+
const hooks = registerHooks({
|
|
12
|
+
resolve(specifier, context, nextResolve) {
|
|
13
|
+
if (context.parentURL === middlewareUrl) {
|
|
14
|
+
const source = {
|
|
15
|
+
'vona': 'export class BeanBase {}',
|
|
16
|
+
'vona-module-a-aspect': 'export function MiddlewareSystem() { return () => {}; }',
|
|
17
|
+
'pino-std-serializers':
|
|
18
|
+
'export default { req: req => ({ headers: req.headers, url: req.url }) };',
|
|
19
|
+
}[specifier];
|
|
20
|
+
if (source)
|
|
21
|
+
return { url: `data:text/javascript,${encodeURIComponent(source)}`, shortCircuit: true };
|
|
22
|
+
}
|
|
23
|
+
return nextResolve(specifier, context);
|
|
24
|
+
},
|
|
25
|
+
load(url, context, nextLoad) {
|
|
26
|
+
if (url === middlewareUrl) {
|
|
27
|
+
return {
|
|
28
|
+
format: 'module',
|
|
29
|
+
source: ts.transpileModule(readFileSync(new URL(url), 'utf8'), {
|
|
30
|
+
compilerOptions: {
|
|
31
|
+
experimentalDecorators: true,
|
|
32
|
+
target: ts.ScriptTarget.ESNext,
|
|
33
|
+
module: ts.ModuleKind.ESNext,
|
|
34
|
+
},
|
|
35
|
+
}).outputText,
|
|
36
|
+
shortCircuit: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return nextLoad(url, context);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const { MiddlewareSystemHttpLog } = await import(middlewareUrl);
|
|
43
|
+
hooks.deregister();
|
|
44
|
+
|
|
45
|
+
for (const path of ['/health/ready', '/health/test-stop']) {
|
|
46
|
+
test(`HTTP logger redacts request authorization and response proof at ${path} without mutating headers`, async () => {
|
|
47
|
+
const headers = {
|
|
48
|
+
'x-vona-test-challenge': 'secret-challenge',
|
|
49
|
+
'X-Vona-Test-Stop-Authorization': 'secret-authorization',
|
|
50
|
+
'x-vona-test-stop-nonce': 'secret-nonce',
|
|
51
|
+
'accept': 'application/json',
|
|
52
|
+
};
|
|
53
|
+
const responseHeaders = {
|
|
54
|
+
'x-vona-test-proof': 'secret-readiness',
|
|
55
|
+
'X-Vona-Test-Stop-Proof': 'secret-ack',
|
|
56
|
+
'content-type': 'application/json',
|
|
57
|
+
};
|
|
58
|
+
const logs: string[] = [];
|
|
59
|
+
const middleware = Object.create(MiddlewareSystemHttpLog.prototype);
|
|
60
|
+
Object.assign(middleware, {
|
|
61
|
+
app: { meta: { env: { VONA_TEST_RUN_ID: '20261014010203-a1b2c3d4' } } },
|
|
62
|
+
ctx: {
|
|
63
|
+
path,
|
|
64
|
+
req: { url: path, headers },
|
|
65
|
+
res: { statusCode: 200, getHeaders: () => responseHeaders },
|
|
66
|
+
},
|
|
67
|
+
$loggerChild: () => ({
|
|
68
|
+
http: (message: () => string) => logs.push(message()),
|
|
69
|
+
startTimer: () => ({ done: ({ message }: { message: string }) => logs.push(message) }),
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
await middleware.execute({}, async () => {});
|
|
73
|
+
assert.equal(logs.length, 2);
|
|
74
|
+
assert.equal(logs.join('\n').includes('secret-'), false);
|
|
75
|
+
assert.match(logs[0], /application\/json/);
|
|
76
|
+
assert.match(logs[1], /application\/json/);
|
|
77
|
+
assert.equal(headers['X-Vona-Test-Stop-Authorization'], 'secret-authorization');
|
|
78
|
+
assert.equal(responseHeaders['X-Vona-Test-Stop-Proof'], 'secret-ack');
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -4,10 +4,15 @@ import type {
|
|
|
4
4
|
IMiddlewareSystemExecute,
|
|
5
5
|
} from 'vona-module-a-aspect';
|
|
6
6
|
|
|
7
|
-
import { createHmac } from 'node:crypto';
|
|
8
|
-
import { BeanBase } from 'vona';
|
|
7
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
8
|
+
import { assertTestAppIdentity, BeanBase, closeProcessWorker } from 'vona';
|
|
9
9
|
import { MiddlewareSystem } from 'vona-module-a-aspect';
|
|
10
10
|
|
|
11
|
+
// An accepted shutdown consumes this app's authorization before yielding. This also
|
|
12
|
+
// rejects concurrent/replayed requests across request-scoped middleware instances.
|
|
13
|
+
const stoppingApps = new WeakSet<object>();
|
|
14
|
+
const STOP_PATH = '/health/test-stop';
|
|
15
|
+
|
|
11
16
|
export interface IMiddlewareSystemOptionsHealth extends IDecoratorMiddlewareSystemOptions {}
|
|
12
17
|
|
|
13
18
|
@MiddlewareSystem<IMiddlewareSystemOptionsHealth>({
|
|
@@ -16,6 +21,10 @@ export interface IMiddlewareSystemOptionsHealth extends IDecoratorMiddlewareSyst
|
|
|
16
21
|
export class MiddlewareSystemHealth extends BeanBase implements IMiddlewareSystemExecute {
|
|
17
22
|
async execute(_options: IMiddlewareSystemOptionsHealth, next: Next) {
|
|
18
23
|
const path = this.ctx.path;
|
|
24
|
+
if (path === STOP_PATH) {
|
|
25
|
+
this._testStop();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
19
28
|
if (!['/health/live', '/health/ready', '/health/startup'].includes(path)) return next();
|
|
20
29
|
const health = await this.$scope.metrics.service.health.status(path);
|
|
21
30
|
this.ctx.status = health.code;
|
|
@@ -36,4 +45,66 @@ export class MiddlewareSystemHealth extends BeanBase implements IMiddlewareSyste
|
|
|
36
45
|
}
|
|
37
46
|
this.ctx.body = { status: health.status };
|
|
38
47
|
}
|
|
48
|
+
|
|
49
|
+
private _testStop() {
|
|
50
|
+
const ctx = this.ctx;
|
|
51
|
+
// Never trust forwarded IP headers, even when the app trusts a proxy.
|
|
52
|
+
const address = ctx.req.socket.remoteAddress;
|
|
53
|
+
const env = this.app.meta.env;
|
|
54
|
+
const key = process.env.VONA_TEST_READY_KEY;
|
|
55
|
+
let runId: string | undefined;
|
|
56
|
+
try {
|
|
57
|
+
runId = assertTestAppIdentity(env.META_MODE, env.VONA_TEST_RUN_ID ?? '', this.app.name);
|
|
58
|
+
} catch {
|
|
59
|
+
// Invalid or ordinary app identities must not expose this operation.
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
!runId ||
|
|
63
|
+
process.env.META_MODE !== 'test' ||
|
|
64
|
+
process.env.VONA_TEST_RUN_ID !== runId ||
|
|
65
|
+
this.app.config.server.workers !== 1 ||
|
|
66
|
+
!key ||
|
|
67
|
+
!/^[a-f0-9]{64}$/.test(key) ||
|
|
68
|
+
!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(address ?? '')
|
|
69
|
+
) {
|
|
70
|
+
ctx.status = 404;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const nonce = ctx.get('x-vona-test-stop-nonce');
|
|
74
|
+
const authorization = ctx.get('x-vona-test-stop-authorization');
|
|
75
|
+
if (
|
|
76
|
+
ctx.method !== 'POST' ||
|
|
77
|
+
ctx.req.url !== STOP_PATH ||
|
|
78
|
+
!/^[a-f0-9]{32}$/.test(nonce) ||
|
|
79
|
+
!/^[a-f0-9]{64}$/.test(authorization)
|
|
80
|
+
) {
|
|
81
|
+
ctx.status = 403;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Readiness signs only a hex challenge. Neither that signing oracle nor the
|
|
85
|
+
// acknowledgment domain can produce a shutdown request authorization.
|
|
86
|
+
const payload = `${runId}\nPOST\n${STOP_PATH}\n${nonce}`;
|
|
87
|
+
const expected = createHmac('sha256', key)
|
|
88
|
+
.update(`vona-test-stop-request-v1\n${payload}`)
|
|
89
|
+
.digest();
|
|
90
|
+
if (!timingSafeEqual(expected, Buffer.from(authorization, 'hex'))) {
|
|
91
|
+
ctx.status = 403;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (stoppingApps.has(this.app)) {
|
|
95
|
+
ctx.status = 409;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
stoppingApps.add(this.app);
|
|
99
|
+
ctx.status = 200;
|
|
100
|
+
ctx.type = 'application/json';
|
|
101
|
+
ctx.set(
|
|
102
|
+
'x-vona-test-stop-proof',
|
|
103
|
+
createHmac('sha256', key).update(`vona-test-stop-ack-v1\n${payload}`).digest('hex'),
|
|
104
|
+
);
|
|
105
|
+
ctx.body = { status: 'stopping' };
|
|
106
|
+
// closeApp may close this listener, so flush the authenticated acknowledgment
|
|
107
|
+
// first. A disconnected response is not permission to start closing.
|
|
108
|
+
ctx.res.once('finish', () => void closeProcessWorker());
|
|
109
|
+
}
|
|
39
110
|
}
|