cabloy 5.1.216 → 5.1.218
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/create-cabloy.yml +137 -0
- package/.github/workflows/playwright-e2e.yml +163 -13
- package/.github/workflows/vona-test-sqlite3.yml +77 -7
- package/CHANGELOG.md +22 -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 +42 -181
- 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/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +4 -4
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-utils/zova-vite/package.json +2 -2
- 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/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +2 -2
- package/zova/pnpm-lock.yaml +324 -63
- 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
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
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
|
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHmac } from 'node:crypto';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { registerHooks } from 'node:module';
|
|
6
|
+
import { describe, test } from 'node:test';
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
|
|
9
|
+
const healthUrl = new URL('../src/bean/middlewareSystem.health.ts?fixture=health', import.meta.url)
|
|
10
|
+
.href;
|
|
11
|
+
const identityUrl = new URL(
|
|
12
|
+
'../../../../../../packages-vona/vona-core/src/lib/core/testIsolation.ts',
|
|
13
|
+
import.meta.url,
|
|
14
|
+
).href;
|
|
15
|
+
let closes = 0;
|
|
16
|
+
const closeKey = Symbol.for('vona.health.test.close');
|
|
17
|
+
(globalThis as Record<symbol, unknown>)[closeKey] = () => closes++;
|
|
18
|
+
const hooks = registerHooks({
|
|
19
|
+
resolve(specifier, context, nextResolve) {
|
|
20
|
+
if (context.parentURL === healthUrl) {
|
|
21
|
+
const source = {
|
|
22
|
+
'vona': `export class BeanBase {} export { assertTestAppIdentity } from ${JSON.stringify(identityUrl)}; export function closeProcessWorker() { globalThis[Symbol.for('vona.health.test.close')](); }`,
|
|
23
|
+
'vona-module-a-aspect': 'export function MiddlewareSystem() { return () => {}; }',
|
|
24
|
+
}[specifier];
|
|
25
|
+
if (source)
|
|
26
|
+
return { url: `data:text/javascript,${encodeURIComponent(source)}`, shortCircuit: true };
|
|
27
|
+
}
|
|
28
|
+
return nextResolve(specifier, context);
|
|
29
|
+
},
|
|
30
|
+
load(url, context, nextLoad) {
|
|
31
|
+
if (url === healthUrl) {
|
|
32
|
+
return {
|
|
33
|
+
format: 'module',
|
|
34
|
+
source: ts.transpileModule(readFileSync(new URL(url), 'utf8'), {
|
|
35
|
+
compilerOptions: {
|
|
36
|
+
experimentalDecorators: true,
|
|
37
|
+
target: ts.ScriptTarget.ESNext,
|
|
38
|
+
module: ts.ModuleKind.ESNext,
|
|
39
|
+
},
|
|
40
|
+
}).outputText,
|
|
41
|
+
shortCircuit: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return nextLoad(url, context);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const { MiddlewareSystemHealth } = await import(healthUrl);
|
|
48
|
+
hooks.deregister();
|
|
49
|
+
|
|
50
|
+
const runId = '20261014010203-a1b2c3d4';
|
|
51
|
+
const key = 'a'.repeat(64);
|
|
52
|
+
const nonce = 'b'.repeat(32);
|
|
53
|
+
const payload = `${runId}\nPOST\n/health/test-stop\n${nonce}`;
|
|
54
|
+
const sign = (value: string) => createHmac('sha256', key).update(value).digest('hex');
|
|
55
|
+
|
|
56
|
+
function fixture() {
|
|
57
|
+
const req = { socket: { remoteAddress: '127.0.0.1' }, url: '/health/test-stop' };
|
|
58
|
+
const headers: Record<string, string> = {
|
|
59
|
+
'x-vona-test-stop-nonce': nonce,
|
|
60
|
+
'x-vona-test-stop-authorization': sign(`vona-test-stop-request-v1\n${payload}`),
|
|
61
|
+
};
|
|
62
|
+
const responseHeaders: Record<string, string> = {};
|
|
63
|
+
const ctx = {
|
|
64
|
+
path: '/health/test-stop',
|
|
65
|
+
method: 'POST',
|
|
66
|
+
req,
|
|
67
|
+
res: new EventEmitter(),
|
|
68
|
+
status: 0,
|
|
69
|
+
type: '',
|
|
70
|
+
body: undefined as unknown,
|
|
71
|
+
get: (name: string) => headers[name] ?? '',
|
|
72
|
+
set: (name: string, value: string) => (responseHeaders[name] = value),
|
|
73
|
+
};
|
|
74
|
+
const app = {
|
|
75
|
+
name: `cabtest${runId}`,
|
|
76
|
+
meta: { env: { META_MODE: 'test', VONA_TEST_RUN_ID: runId } },
|
|
77
|
+
config: { server: { workers: 1 } },
|
|
78
|
+
};
|
|
79
|
+
let nexts = 0;
|
|
80
|
+
const healthCalls: string[] = [];
|
|
81
|
+
const middleware = Object.create(MiddlewareSystemHealth.prototype);
|
|
82
|
+
Object.assign(middleware, {
|
|
83
|
+
app,
|
|
84
|
+
ctx,
|
|
85
|
+
$scope: {
|
|
86
|
+
metrics: {
|
|
87
|
+
service: {
|
|
88
|
+
health: {
|
|
89
|
+
async status(path: string) {
|
|
90
|
+
healthCalls.push(path);
|
|
91
|
+
return { code: 200, status: 'ok' };
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
app,
|
|
100
|
+
ctx,
|
|
101
|
+
headers,
|
|
102
|
+
responseHeaders,
|
|
103
|
+
healthCalls,
|
|
104
|
+
execute: () =>
|
|
105
|
+
middleware.execute({}, async () => {
|
|
106
|
+
nexts++;
|
|
107
|
+
}),
|
|
108
|
+
nexts: () => nexts,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function withServerEnv(callback: () => Promise<void>) {
|
|
113
|
+
const names = ['META_MODE', 'VONA_TEST_RUN_ID', 'VONA_TEST_READY_KEY'] as const;
|
|
114
|
+
const old = names.map(name => process.env[name]);
|
|
115
|
+
process.env.META_MODE = 'test';
|
|
116
|
+
process.env.VONA_TEST_RUN_ID = runId;
|
|
117
|
+
process.env.VONA_TEST_READY_KEY = key;
|
|
118
|
+
return callback().finally(() => {
|
|
119
|
+
for (const [index, name] of names.entries()) {
|
|
120
|
+
if (old[index] === undefined) delete process.env[name];
|
|
121
|
+
else process.env[name] = old[index];
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
describe('authenticated managed shutdown', { concurrency: false }, () => {
|
|
127
|
+
test('shutdown acknowledges its distinct domain before close and rejects replay across middleware instances', async () => {
|
|
128
|
+
await withServerEnv(async () => {
|
|
129
|
+
const f = fixture();
|
|
130
|
+
const before = closes;
|
|
131
|
+
await f.execute();
|
|
132
|
+
assert.equal(f.ctx.status, 200);
|
|
133
|
+
assert.deepEqual(f.ctx.body, { status: 'stopping' });
|
|
134
|
+
assert.equal(
|
|
135
|
+
f.responseHeaders['x-vona-test-stop-proof'],
|
|
136
|
+
sign(`vona-test-stop-ack-v1\n${payload}`),
|
|
137
|
+
);
|
|
138
|
+
assert.equal(closes, before);
|
|
139
|
+
const replay = fixture();
|
|
140
|
+
replay.app = f.app;
|
|
141
|
+
// A separate request-scoped middleware with the same application must reject.
|
|
142
|
+
const middleware = Object.create(MiddlewareSystemHealth.prototype);
|
|
143
|
+
Object.assign(middleware, { app: f.app, ctx: replay.ctx });
|
|
144
|
+
await middleware.execute({}, async () => assert.fail('not next'));
|
|
145
|
+
assert.equal(replay.ctx.status, 409);
|
|
146
|
+
f.ctx.res.emit('finish');
|
|
147
|
+
f.ctx.res.emit('finish');
|
|
148
|
+
assert.equal(closes, before + 1);
|
|
149
|
+
assert.deepEqual(f.healthCalls, []);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const invalidCases: [string, (f: ReturnType<typeof fixture>) => void, number][] = [
|
|
154
|
+
[
|
|
155
|
+
'ordinary mode',
|
|
156
|
+
f => {
|
|
157
|
+
f.app.meta.env.META_MODE = 'dev';
|
|
158
|
+
},
|
|
159
|
+
404,
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
'missing identity',
|
|
163
|
+
f => {
|
|
164
|
+
f.app.meta.env.VONA_TEST_RUN_ID = '';
|
|
165
|
+
},
|
|
166
|
+
404,
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
'invalid date',
|
|
170
|
+
f => {
|
|
171
|
+
f.app.meta.env.VONA_TEST_RUN_ID = '20269999010203-a1b2c3d4';
|
|
172
|
+
},
|
|
173
|
+
404,
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
'wrong app identity',
|
|
177
|
+
f => {
|
|
178
|
+
f.app.name = 'ordinary-app';
|
|
179
|
+
},
|
|
180
|
+
404,
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
'multiple workers',
|
|
184
|
+
f => {
|
|
185
|
+
f.app.config.server.workers = 2;
|
|
186
|
+
},
|
|
187
|
+
404,
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
'non-loopback socket ignoring forwarded IP',
|
|
191
|
+
f => {
|
|
192
|
+
f.ctx.req.socket.remoteAddress = '192.0.2.1';
|
|
193
|
+
f.headers['x-forwarded-for'] = '127.0.0.1';
|
|
194
|
+
},
|
|
195
|
+
404,
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
'wrong method',
|
|
199
|
+
f => {
|
|
200
|
+
f.ctx.method = 'GET';
|
|
201
|
+
},
|
|
202
|
+
403,
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
'query path',
|
|
206
|
+
f => {
|
|
207
|
+
f.ctx.req.url += '?ignored=yes';
|
|
208
|
+
},
|
|
209
|
+
403,
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
'missing nonce',
|
|
213
|
+
f => {
|
|
214
|
+
delete f.headers['x-vona-test-stop-nonce'];
|
|
215
|
+
},
|
|
216
|
+
403,
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
'malformed authorization',
|
|
220
|
+
f => {
|
|
221
|
+
f.headers['x-vona-test-stop-authorization'] = 'short';
|
|
222
|
+
},
|
|
223
|
+
403,
|
|
224
|
+
],
|
|
225
|
+
[
|
|
226
|
+
'bad signature',
|
|
227
|
+
f => {
|
|
228
|
+
f.headers['x-vona-test-stop-authorization'] = 'f'.repeat(64);
|
|
229
|
+
},
|
|
230
|
+
403,
|
|
231
|
+
],
|
|
232
|
+
[
|
|
233
|
+
'readiness signature cannot stop',
|
|
234
|
+
f => {
|
|
235
|
+
f.headers['x-vona-test-stop-authorization'] = sign(nonce);
|
|
236
|
+
},
|
|
237
|
+
403,
|
|
238
|
+
],
|
|
239
|
+
[
|
|
240
|
+
'ack signature cannot stop',
|
|
241
|
+
f => {
|
|
242
|
+
f.headers['x-vona-test-stop-authorization'] = sign(`vona-test-stop-ack-v1\n${payload}`);
|
|
243
|
+
},
|
|
244
|
+
403,
|
|
245
|
+
],
|
|
246
|
+
[
|
|
247
|
+
'wrong run signature',
|
|
248
|
+
f => {
|
|
249
|
+
f.headers['x-vona-test-stop-authorization'] = sign(
|
|
250
|
+
`vona-test-stop-request-v1\n${payload.replace(runId, '20261014010203-d4e5f6a7')}`,
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
403,
|
|
254
|
+
],
|
|
255
|
+
[
|
|
256
|
+
'different nonce',
|
|
257
|
+
f => {
|
|
258
|
+
f.headers['x-vona-test-stop-nonce'] = 'c'.repeat(32);
|
|
259
|
+
},
|
|
260
|
+
403,
|
|
261
|
+
],
|
|
262
|
+
];
|
|
263
|
+
for (const [name, change, status] of invalidCases) {
|
|
264
|
+
test(`shutdown rejects ${name} without initiating close`, async () => {
|
|
265
|
+
await withServerEnv(async () => {
|
|
266
|
+
const f = fixture();
|
|
267
|
+
const before = closes;
|
|
268
|
+
change(f);
|
|
269
|
+
await f.execute();
|
|
270
|
+
assert.equal(f.ctx.status, status);
|
|
271
|
+
assert.equal(f.ctx.res.listenerCount('finish'), 0);
|
|
272
|
+
f.ctx.res.emit('finish');
|
|
273
|
+
assert.equal(closes, before);
|
|
274
|
+
assert.deepEqual(f.responseHeaders, {});
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const [name, value] of [
|
|
280
|
+
['META_MODE', 'dev'],
|
|
281
|
+
['VONA_TEST_RUN_ID', '20261014010203-d4e5f6a7'],
|
|
282
|
+
['VONA_TEST_READY_KEY', 'not-a-key'],
|
|
283
|
+
] as const) {
|
|
284
|
+
test(`shutdown requires matching server-only ${name}`, async () => {
|
|
285
|
+
await withServerEnv(async () => {
|
|
286
|
+
const f = fixture();
|
|
287
|
+
process.env[name] = value;
|
|
288
|
+
await f.execute();
|
|
289
|
+
assert.equal(f.ctx.status, 404);
|
|
290
|
+
assert.equal(f.ctx.res.listenerCount('finish'), 0);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
test('existing readiness signing and unrelated health routes remain unchanged', async () => {
|
|
296
|
+
await withServerEnv(async () => {
|
|
297
|
+
const f = fixture();
|
|
298
|
+
f.ctx.path = '/health/ready';
|
|
299
|
+
f.headers['x-vona-test-challenge'] = nonce;
|
|
300
|
+
await f.execute();
|
|
301
|
+
assert.equal(f.responseHeaders['x-vona-test-proof'], sign(nonce));
|
|
302
|
+
assert.deepEqual(f.healthCalls, ['/health/ready']);
|
|
303
|
+
assert.equal(f.ctx.res.listenerCount('finish'), 0);
|
|
304
|
+
f.ctx.path = '/unrelated';
|
|
305
|
+
await f.execute();
|
|
306
|
+
assert.equal(f.nexts(), 1);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transactionAsyncLocalStorage_.ts
CHANGED
|
@@ -18,9 +18,9 @@ export class ServiceTransactionAsyncLocalStorage extends BeanBase {
|
|
|
18
18
|
return this.transactionStorage.getStore()!;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
async run<RESULT>(fn: FunctionAsync<RESULT
|
|
22
|
-
//
|
|
23
|
-
if (this.transactionState) {
|
|
21
|
+
async run<RESULT>(fn: FunctionAsync<RESULT>, isolate = false): Promise<RESULT> {
|
|
22
|
+
// Delegated calls and datasource switches share state; independent contexts do not.
|
|
23
|
+
if (!isolate && this.transactionState) {
|
|
24
24
|
return fn();
|
|
25
25
|
}
|
|
26
26
|
const transactionState = this.bean._newBean(ServiceTransactionState);
|
|
@@ -161,7 +161,7 @@ async function _getFullPathInner(
|
|
|
161
161
|
): Promise<string | undefined> {
|
|
162
162
|
// trim prefix
|
|
163
163
|
if (options.apiStaticPrefix !== '/') {
|
|
164
|
-
const filePrefix = path.normalize(options.apiStaticPrefix.replace(/^\//, ''));
|
|
164
|
+
const filePrefix = path.posix.normalize(options.apiStaticPrefix.replace(/^\//, ''));
|
|
165
165
|
if (filename.indexOf(filePrefix) !== 0) {
|
|
166
166
|
return;
|
|
167
167
|
// cannot throw error
|
|
@@ -170,7 +170,7 @@ async function _getFullPathInner(
|
|
|
170
170
|
filename = filename.slice(filePrefix.length);
|
|
171
171
|
}
|
|
172
172
|
// check
|
|
173
|
-
const parts = filename.split(
|
|
173
|
+
const parts = filename.split('/');
|
|
174
174
|
const wordFirst = parts.shift();
|
|
175
175
|
// public
|
|
176
176
|
if (wordFirst === 'public') {
|