cabloy 5.1.219 → 5.1.220
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 +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/repo-docs/fullstack/quickstart.md +3 -3
- package/repo-docs/reference/repo-scripts.md +12 -0
- package/scripts/generateOpenapiIsolated.test.ts +25 -0
- package/scripts/generateOpenapiIsolated.ts +28 -11
- package/scripts/runIsolatedTest.test.ts +157 -45
- package/scripts/runIsolatedTest.ts +30 -6
- package/scripts/strictGenerationStartup.test.ts +212 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- 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/bean/beanBase.ts +4 -0
- package/vona/packages-vona/vona-core/src/lib/core/asyncLocalStorage.ts +84 -12
- package/vona/packages-vona/vona-core/src/lib/core/index.ts +14 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +11 -3
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +146 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.ts +138 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/utils.ts +25 -5
- package/vona/packages-vona/vona-core/src/lib/core/requestBoundary.ts +89 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.test.ts +379 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +238 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +4 -3
- package/vona/packages-vona/vona-core/src/lib/framework/start.ts +2 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +154 -125
- 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 +111 -60
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/service/executor.ts +6 -6
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +13 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/lib/jwtClientLogging.test.ts +182 -0
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +2 -8
- 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/aopMethod.log.ts +4 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/bean.logger.ts +9 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +27 -14
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.test.ts +207 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.ts +30 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +33 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +6 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/bean.telemetry.ts +8 -1
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/middlewareSystem.trace.ts +9 -14
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/lib/spanDiagnostics.ts +70 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/service/telemetry.ts +37 -23
- package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +31 -4
- package/vona/src/suite-vendor/a-vona/package.json +2 -2
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +11 -101
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencies.ts +78 -0
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +231 -0
- package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +431 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { FunctionAsync } from 'vona';
|
|
1
|
+
import type { FunctionAsync, VonaContext } from 'vona';
|
|
2
2
|
import type { IApiPathRecordMethodMap } from 'vona-module-a-web';
|
|
3
3
|
|
|
4
4
|
import { isNil } from '@cabloy/utils';
|
|
5
|
-
import { BeanBase } from 'vona';
|
|
5
|
+
import { BeanBase, captureRequestBoundary } from 'vona';
|
|
6
6
|
import { Bean } from 'vona-module-a-bean';
|
|
7
7
|
import { ServiceTransactionAsyncLocalStorage } from 'vona-module-a-orm';
|
|
8
8
|
|
|
9
|
-
import type { INewCtxOptions, IPerformActionOptions } from '../types/executor.ts';
|
|
9
|
+
import type { INewCtxLifecycle, INewCtxOptions, IPerformActionOptions } from '../types/executor.ts';
|
|
10
10
|
|
|
11
11
|
import { __delegateProperties } from '../lib/utils.ts';
|
|
12
12
|
|
|
@@ -30,6 +30,7 @@ export class BeanExecutor extends BeanBase {
|
|
|
30
30
|
async newCtxIsolate<RESULT>(
|
|
31
31
|
fn: FunctionAsync<RESULT>,
|
|
32
32
|
options?: INewCtxOptions,
|
|
33
|
+
lifecycle?: INewCtxLifecycle,
|
|
33
34
|
): Promise<RESULT> {
|
|
34
35
|
options = Object.assign({}, options);
|
|
35
36
|
// instanceName !== undefined means isolate
|
|
@@ -39,83 +40,133 @@ export class BeanExecutor extends BeanBase {
|
|
|
39
40
|
options.locale = options.locale === undefined ? this.ctx?.locale : options.locale;
|
|
40
41
|
options.tz = options.tz === undefined ? this.ctx?.tz : options.tz;
|
|
41
42
|
}
|
|
42
|
-
return this.newCtx(fn, options);
|
|
43
|
+
return this.newCtx(fn, options, lifecycle);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
async mockCtx<RESULT>(
|
|
46
|
+
async mockCtx<RESULT>(
|
|
47
|
+
fn: FunctionAsync<RESULT>,
|
|
48
|
+
options?: INewCtxOptions,
|
|
49
|
+
lifecycle?: INewCtxLifecycle,
|
|
50
|
+
): Promise<RESULT> {
|
|
46
51
|
options = Object.assign({}, options);
|
|
47
52
|
options.dbInfo = isNil(options.dbInfo) ? { level: 1 } : options.dbInfo;
|
|
48
53
|
options.instanceName =
|
|
49
54
|
options.instanceName === undefined ? (this.ctx?.instanceName ?? '') : options.instanceName;
|
|
50
55
|
options.instance = !isNil(options.instanceName);
|
|
51
|
-
return this.newCtx(fn, options);
|
|
56
|
+
return this.newCtx(fn, options, lifecycle);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
async newCtx<RESULT>(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
async newCtx<RESULT>(
|
|
60
|
+
fn: FunctionAsync<RESULT>,
|
|
61
|
+
options?: INewCtxOptions,
|
|
62
|
+
lifecycle?: INewCtxLifecycle,
|
|
63
|
+
): Promise<RESULT> {
|
|
64
|
+
return this._newCtxInnerStep1(
|
|
65
|
+
() => {
|
|
66
|
+
if (isNil(options?.dbInfo)) return this._newCtxInnerStep2(fn, options);
|
|
67
|
+
return this.bean.database.switchDb(() => {
|
|
68
|
+
return this._newCtxInnerStep2(fn, options);
|
|
69
|
+
}, options?.dbInfo);
|
|
70
|
+
},
|
|
71
|
+
options,
|
|
72
|
+
lifecycle,
|
|
73
|
+
);
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
private async _newCtxInnerStep1<RESULT>(
|
|
64
77
|
fn: FunctionAsync<RESULT>,
|
|
65
|
-
options
|
|
78
|
+
options: INewCtxOptions = {},
|
|
79
|
+
lifecycle?: INewCtxLifecycle,
|
|
66
80
|
): Promise<RESULT> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
!this.ctx ||
|
|
73
|
-
options.instanceName !== undefined ||
|
|
74
|
-
(!innerAccess && this.ctx?.instanceName === null);
|
|
75
|
-
const ctxCaller = !isolate && this.ctx ? this.ctx : undefined;
|
|
76
|
-
// locale/tz/instanceName
|
|
77
|
-
if (!isolate && this.ctx) {
|
|
78
|
-
options.locale = options.locale === undefined ? this.ctx.locale : options.locale;
|
|
79
|
-
options.tz = options.tz === undefined ? this.ctx.tz : options.tz;
|
|
80
|
-
options.instanceName =
|
|
81
|
-
options.instanceName === undefined
|
|
82
|
-
? !innerAccess && this.ctx.instanceName === null
|
|
83
|
-
? undefined
|
|
84
|
-
: this.ctx.instanceName
|
|
85
|
-
: options.instanceName;
|
|
86
|
-
}
|
|
87
|
-
// run
|
|
88
|
-
const ctx = this.app.createAnonymousContext(options.req, options.res);
|
|
89
|
-
return await this.app.ctxStorage.run(ctx, async () => {
|
|
81
|
+
const capture = captureRequestBoundary();
|
|
82
|
+
const onScopeOutcome = lifecycle?.onScopeOutcome;
|
|
83
|
+
let ctx: VonaContext | undefined;
|
|
84
|
+
try {
|
|
85
|
+
options = Object.assign({}, options);
|
|
90
86
|
// innerAccess
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
ctx
|
|
87
|
+
const innerAccess = options.innerAccess !== false;
|
|
88
|
+
// isolate
|
|
89
|
+
const isolate =
|
|
90
|
+
!this.ctx ||
|
|
91
|
+
options.instanceName !== undefined ||
|
|
92
|
+
(!innerAccess && this.ctx?.instanceName === null);
|
|
93
|
+
const ctxCaller = !isolate && this.ctx ? this.ctx : undefined;
|
|
94
|
+
// locale/tz/instanceName
|
|
95
|
+
if (!isolate && this.ctx) {
|
|
96
|
+
options.locale = options.locale === undefined ? this.ctx.locale : options.locale;
|
|
97
|
+
options.tz = options.tz === undefined ? this.ctx.tz : options.tz;
|
|
98
|
+
options.instanceName =
|
|
99
|
+
options.instanceName === undefined
|
|
100
|
+
? !innerAccess && this.ctx.instanceName === null
|
|
101
|
+
? undefined
|
|
102
|
+
: this.ctx.instanceName
|
|
103
|
+
: options.instanceName;
|
|
95
104
|
}
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// run
|
|
106
|
+
ctx = this.app.createAnonymousContext(options.req, options.res);
|
|
107
|
+
const context = ctx;
|
|
108
|
+
const execute = async () => {
|
|
109
|
+
const initialized: unknown = lifecycle?.initializeContext?.(context);
|
|
110
|
+
if (initialized && (typeof initialized === 'object' || typeof initialized === 'function')) {
|
|
111
|
+
const then = (initialized as PromiseLike<unknown>).then;
|
|
112
|
+
if (typeof then === 'function') {
|
|
113
|
+
await new Promise<unknown>((resolve, reject) => {
|
|
114
|
+
then.call(initialized, resolve, reject);
|
|
115
|
+
}).catch(() => undefined);
|
|
116
|
+
throw new Error('Context initializer must be synchronous');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// innerAccess
|
|
120
|
+
context.innerAccess = innerAccess;
|
|
121
|
+
// locale
|
|
122
|
+
if (options.locale !== undefined) {
|
|
123
|
+
context.locale = options.locale;
|
|
124
|
+
}
|
|
125
|
+
// tz
|
|
126
|
+
if (options.tz !== undefined) {
|
|
127
|
+
context.tz = options.tz;
|
|
128
|
+
}
|
|
129
|
+
// instanceName: undefined/null is different
|
|
130
|
+
if (options.instanceName !== undefined) {
|
|
131
|
+
context.instanceName = options.instanceName;
|
|
132
|
+
}
|
|
108
133
|
// ctxCaller
|
|
109
|
-
|
|
110
|
-
} else {
|
|
111
|
-
// isolate: extraData
|
|
112
|
-
if (options.extraData) {
|
|
134
|
+
if (ctxCaller) {
|
|
113
135
|
// delegateProperties
|
|
114
|
-
__delegateProperties(
|
|
136
|
+
__delegateProperties(context, ctxCaller);
|
|
137
|
+
// ctxCaller
|
|
138
|
+
context.ctxCaller = ctxCaller;
|
|
139
|
+
} else {
|
|
140
|
+
// isolate: extraData
|
|
141
|
+
if (options.extraData) {
|
|
142
|
+
// delegateProperties
|
|
143
|
+
__delegateProperties(context, options.extraData);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return await this.bean._getBean(ServiceTransactionAsyncLocalStorage).run(fn, isolate);
|
|
147
|
+
};
|
|
148
|
+
return onScopeOutcome
|
|
149
|
+
? await this.app.ctxStorage.runObserved(context, execute, onScopeOutcome)
|
|
150
|
+
: await this.app.ctxStorage.run(context, execute);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (!ctx && onScopeOutcome) {
|
|
153
|
+
try {
|
|
154
|
+
const result: unknown = onScopeOutcome('not_created');
|
|
155
|
+
if (result && (typeof result === 'object' || typeof result === 'function')) {
|
|
156
|
+
const then = (result as PromiseLike<unknown>).then;
|
|
157
|
+
if (typeof then === 'function') {
|
|
158
|
+
capture?.reportFailure('observer_failed');
|
|
159
|
+
void new Promise<unknown>((resolve, reject) => {
|
|
160
|
+
then.call(result, resolve, reject);
|
|
161
|
+
}).catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
capture?.reportFailure('observer_failed');
|
|
115
166
|
}
|
|
116
167
|
}
|
|
117
|
-
|
|
118
|
-
}
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
119
170
|
}
|
|
120
171
|
|
|
121
172
|
private async _newCtxInnerStep2<RESULT>(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IApiPathRecordMethodMap } from 'vona-module-a-web';
|
|
2
2
|
|
|
3
3
|
import { combineParamsAndQuery } from '@cabloy/utils';
|
|
4
|
-
import { BeanBase, cast, deepExtend } from 'vona';
|
|
4
|
+
import { BeanBase, cast, deepExtend, getRequestBoundary } from 'vona';
|
|
5
5
|
import { Aspect } from 'vona-module-a-aspect';
|
|
6
6
|
import { Service } from 'vona-module-a-bean';
|
|
7
7
|
|
|
@@ -19,8 +19,8 @@ export class ServiceExecutor extends BeanBase {
|
|
|
19
19
|
): Promise<any> {
|
|
20
20
|
// app
|
|
21
21
|
const app = this.app;
|
|
22
|
-
const telemetry = this.$scope.telemetry.service.telemetry;
|
|
23
|
-
const span = telemetry
|
|
22
|
+
const telemetry = getRequestBoundary() ? undefined : this.$scope.telemetry.service.telemetry;
|
|
23
|
+
const span = telemetry?.enabled
|
|
24
24
|
? telemetry.startSpan(`vona.action ${method.toUpperCase()}`, {
|
|
25
25
|
attributes: { 'vona.action.internal': true, 'http.request.method': method.toUpperCase() },
|
|
26
26
|
})
|
|
@@ -83,16 +83,16 @@ export class ServiceExecutor extends BeanBase {
|
|
|
83
83
|
state: {
|
|
84
84
|
...options?.extraData?.state,
|
|
85
85
|
telemetry: span
|
|
86
|
-
? { context: telemetry
|
|
86
|
+
? { context: telemetry!.createContext(span), span, internalAction: true }
|
|
87
87
|
: undefined,
|
|
88
88
|
},
|
|
89
89
|
},
|
|
90
90
|
},
|
|
91
91
|
);
|
|
92
92
|
try {
|
|
93
|
-
return await (span ? telemetry
|
|
93
|
+
return await (span ? telemetry!.withSpan(span, execute) : execute());
|
|
94
94
|
} catch (err) {
|
|
95
|
-
if (span) telemetry
|
|
95
|
+
if (span) telemetry!.recordException(span, err);
|
|
96
96
|
throw err;
|
|
97
97
|
} finally {
|
|
98
98
|
span?.end();
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ContextState,
|
|
3
|
+
IInstanceRecord,
|
|
4
|
+
ILocaleRecord,
|
|
5
|
+
PowerPartial,
|
|
6
|
+
VonaContext,
|
|
7
|
+
VonaScopeOutcomeObserver,
|
|
8
|
+
} from 'vona';
|
|
2
9
|
import type { ConfigOnions } from 'vona-module-a-onion';
|
|
3
10
|
import type { IDbInfo, ITransactionOptions } from 'vona-module-a-orm';
|
|
4
11
|
|
|
@@ -15,6 +22,11 @@ export interface INewCtxOptions extends INewCtxBaseOptions {
|
|
|
15
22
|
dbInfo?: Partial<IDbInfo>;
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
export interface INewCtxLifecycle {
|
|
26
|
+
initializeContext?: (ctx: VonaContext) => void;
|
|
27
|
+
onScopeOutcome?: VonaScopeOutcomeObserver;
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export interface INewCtxBaseOptions {
|
|
19
31
|
locale?: keyof ILocaleRecord;
|
|
20
32
|
tz?: string;
|
|
@@ -0,0 +1,182 @@
|
|
|
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 previousApp = Object.getOwnPropertyDescriptor(globalThis, '__app__');
|
|
8
|
+
const vonaUrl = new URL('../../../../../../../', import.meta.url);
|
|
9
|
+
const clientUrl = new URL('../service/jwtClient.ts?fixture=jwtLogging', import.meta.url).href;
|
|
10
|
+
const formatterUrl = new URL('packages-vona/vona-core/src/lib/core/logger/utils.ts', vonaUrl).href;
|
|
11
|
+
const checkUrl = new URL('packages-utils/utils/src/check.ts', vonaUrl).href;
|
|
12
|
+
const utilsUrl = new URL('packages-utils/utils/src/utils.ts', vonaUrl).href;
|
|
13
|
+
const castUrl = new URL('packages-vona/vona-core/src/types/utils/cast.ts', vonaUrl).href;
|
|
14
|
+
const fixture = (source: string) => `data:text/javascript,${encodeURIComponent(source)}`;
|
|
15
|
+
const frameworkFixture = fixture(`
|
|
16
|
+
export class BeanBase {}
|
|
17
|
+
export { cast } from ${JSON.stringify(castUrl)};
|
|
18
|
+
export function deepExtend() { throw new Error('unexpected client initialization'); }
|
|
19
|
+
`);
|
|
20
|
+
const utilityFixture = fixture(`
|
|
21
|
+
export { isNil } from ${JSON.stringify(checkUrl)};
|
|
22
|
+
export { catchError } from ${JSON.stringify(utilsUrl)};
|
|
23
|
+
`);
|
|
24
|
+
const hooks = registerHooks({
|
|
25
|
+
resolve(specifier, context, nextResolve) {
|
|
26
|
+
if (context.parentURL === clientUrl) {
|
|
27
|
+
if (specifier === 'vona') return { url: frameworkFixture, shortCircuit: true };
|
|
28
|
+
if (specifier === '@cabloy/utils') return { url: utilityFixture, shortCircuit: true };
|
|
29
|
+
if (specifier === 'vona-module-a-bean') {
|
|
30
|
+
return {
|
|
31
|
+
url: fixture('export function Service() { return () => {}; }'),
|
|
32
|
+
shortCircuit: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (context.parentURL === formatterUrl) {
|
|
37
|
+
if (specifier === '@cabloy/utils') return { url: checkUrl, shortCircuit: true };
|
|
38
|
+
if (specifier === '../../framework/useApp.ts') {
|
|
39
|
+
return {
|
|
40
|
+
url: fixture(
|
|
41
|
+
'export function useApp() { throw new Error("unexpected application access"); }',
|
|
42
|
+
),
|
|
43
|
+
shortCircuit: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return nextResolve(specifier, context);
|
|
48
|
+
},
|
|
49
|
+
load(url, context, nextLoad) {
|
|
50
|
+
if (url === clientUrl) {
|
|
51
|
+
return {
|
|
52
|
+
format: 'module',
|
|
53
|
+
source: ts.transpileModule(readFileSync(new URL(url), 'utf8'), {
|
|
54
|
+
compilerOptions: {
|
|
55
|
+
experimentalDecorators: true,
|
|
56
|
+
target: ts.ScriptTarget.ESNext,
|
|
57
|
+
module: ts.ModuleKind.ESNext,
|
|
58
|
+
},
|
|
59
|
+
}).outputText,
|
|
60
|
+
shortCircuit: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return nextLoad(url, context);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const { ServiceJwtClient, formatLoggerFilter } = await (async () => {
|
|
67
|
+
try {
|
|
68
|
+
return { ...(await import(clientUrl)), ...(await import(formatterUrl)) };
|
|
69
|
+
} finally {
|
|
70
|
+
hooks.deregister();
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
const secret = 'synthetic-jwt-secret-sentinel';
|
|
75
|
+
|
|
76
|
+
for (const operation of ['sign', 'verify'] as const) {
|
|
77
|
+
for (const failure of [false, true]) {
|
|
78
|
+
test(`actual JWT ${operation} debug log, ${failure ? 'error' : 'success'}`, async () => {
|
|
79
|
+
const output: string[] = [];
|
|
80
|
+
const filter = formatLoggerFilter({ level: 'debug', child: undefined });
|
|
81
|
+
const error = new Error(secret);
|
|
82
|
+
const payloadData = Object.freeze({ value: secret });
|
|
83
|
+
const options = Object.freeze({ path: '/callback', expiresIn: 60 });
|
|
84
|
+
const signOptions = Object.freeze({ issuer: 'synthetic-test' });
|
|
85
|
+
const verifyOptions = Object.freeze({ issuer: 'synthetic-test' });
|
|
86
|
+
let calls = 0;
|
|
87
|
+
const fake: any = {
|
|
88
|
+
_clientName: 'access',
|
|
89
|
+
_clientOptions: { secret, signOptions, verifyOptions },
|
|
90
|
+
fieldClient: 'client',
|
|
91
|
+
fieldPath: 'path',
|
|
92
|
+
fieldData: 'data',
|
|
93
|
+
_signInner: ServiceJwtClient.prototype['_signInner'],
|
|
94
|
+
_verifyInner: ServiceJwtClient.prototype['_verifyInner'],
|
|
95
|
+
_checkVerifyPath: ServiceJwtClient.prototype._checkVerifyPath,
|
|
96
|
+
scope: { service: { jwtExtract: { fromAllWays: () => secret } } },
|
|
97
|
+
app: {
|
|
98
|
+
throw: () => {
|
|
99
|
+
throw new Error('unexpected authentication change');
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
$loggerChild: (name: string) => {
|
|
103
|
+
assert.equal(name, 'jwt');
|
|
104
|
+
return {
|
|
105
|
+
debug: (message: any) => {
|
|
106
|
+
const info = filter.transform({ level: 'debug', message }, filter.options);
|
|
107
|
+
assert.ok(info);
|
|
108
|
+
output.push(JSON.stringify(info));
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
_jwtInstance: {
|
|
113
|
+
sign: (payload: any, key: string, passedOptions: any, callback: any) => {
|
|
114
|
+
calls++;
|
|
115
|
+
assert.equal(key, secret);
|
|
116
|
+
assert.equal(payload.data, payloadData);
|
|
117
|
+
assert.equal(payload.client, 'access');
|
|
118
|
+
assert.equal(payload.path, '/callback');
|
|
119
|
+
assert.deepEqual(passedOptions, { ...signOptions, expiresIn: 60 });
|
|
120
|
+
callback(failure ? error : null, failure ? undefined : secret);
|
|
121
|
+
},
|
|
122
|
+
verify: (token: string, key: string, passedOptions: any, callback: any) => {
|
|
123
|
+
calls++;
|
|
124
|
+
assert.equal(token, secret);
|
|
125
|
+
assert.equal(key, secret);
|
|
126
|
+
assert.equal(passedOptions, verifyOptions);
|
|
127
|
+
callback(
|
|
128
|
+
failure ? error : null,
|
|
129
|
+
failure
|
|
130
|
+
? undefined
|
|
131
|
+
: {
|
|
132
|
+
client: 'access',
|
|
133
|
+
path: '/callback',
|
|
134
|
+
data: payloadData,
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const execution =
|
|
141
|
+
operation === 'sign'
|
|
142
|
+
? ServiceJwtClient.prototype.sign.call(fake, payloadData, options)
|
|
143
|
+
: ServiceJwtClient.prototype.verify.call(fake, secret, options);
|
|
144
|
+
if (failure) {
|
|
145
|
+
await assert.rejects(execution, value => value === error);
|
|
146
|
+
} else {
|
|
147
|
+
assert.equal(await execution, operation === 'sign' ? secret : payloadData);
|
|
148
|
+
}
|
|
149
|
+
assert.equal(calls, 1);
|
|
150
|
+
assert.equal(output.length, 1);
|
|
151
|
+
assert.equal(output.join('\n').includes(secret), false);
|
|
152
|
+
assert.equal(
|
|
153
|
+
JSON.parse(output[0]!).message,
|
|
154
|
+
`jwt.${operation}: ${failure ? 'failed' : 'completed'}`,
|
|
155
|
+
);
|
|
156
|
+
assert.deepEqual(Object.getOwnPropertyDescriptor(globalThis, '__app__'), previousApp);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
test('JWT extraction and missing-token behavior retain safe debug diagnostics', async () => {
|
|
162
|
+
const output: string[] = [];
|
|
163
|
+
let extracted = 0;
|
|
164
|
+
const fake = {
|
|
165
|
+
_clientName: 'access',
|
|
166
|
+
scope: {
|
|
167
|
+
service: {
|
|
168
|
+
jwtExtract: {
|
|
169
|
+
fromAllWays: () => {
|
|
170
|
+
extracted++;
|
|
171
|
+
return undefined;
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
_verifyInner: ServiceJwtClient.prototype['_verifyInner'],
|
|
177
|
+
$loggerChild: () => ({ debug: (message: () => string) => output.push(message()) }),
|
|
178
|
+
};
|
|
179
|
+
assert.equal(await ServiceJwtClient.prototype.verify.call(fake as any), undefined);
|
|
180
|
+
assert.equal(extracted, 1);
|
|
181
|
+
assert.deepEqual(output, ['jwt.verify: completed']);
|
|
182
|
+
});
|
|
@@ -53,10 +53,7 @@ export class ServiceJwtClient extends BeanBase {
|
|
|
53
53
|
const [res, error] = await catchError(() => {
|
|
54
54
|
return this._signInner(payloadData, options);
|
|
55
55
|
});
|
|
56
|
-
this.$loggerChild('jwt').debug(
|
|
57
|
-
() =>
|
|
58
|
-
`jwt.sign: client:${this._clientName}, token:${res}${error ? `, error: ${error.message}` : ''}`,
|
|
59
|
-
);
|
|
56
|
+
this.$loggerChild('jwt').debug(() => `jwt.sign: ${error ? 'failed' : 'completed'}`);
|
|
60
57
|
if (error) throw error;
|
|
61
58
|
return res;
|
|
62
59
|
}
|
|
@@ -99,10 +96,7 @@ export class ServiceJwtClient extends BeanBase {
|
|
|
99
96
|
const [res, error] = await catchError(() => {
|
|
100
97
|
return this._verifyInner(token, options);
|
|
101
98
|
});
|
|
102
|
-
this.$loggerChild('jwt').debug(
|
|
103
|
-
() =>
|
|
104
|
-
`jwt.verify: client:${this._clientName}, token:${token}${error ? `, error: ${error.message}` : ''}`,
|
|
105
|
-
);
|
|
99
|
+
this.$loggerChild('jwt').debug(() => `jwt.verify: ${error ? 'failed' : 'completed'}`);
|
|
106
100
|
if (error) throw error;
|
|
107
101
|
return res;
|
|
108
102
|
}
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
import type winston from 'winston';
|
|
9
9
|
|
|
10
10
|
import { evaluateExpressions } from '@cabloy/utils';
|
|
11
|
-
import { BeanAopMethodBase, SymbolBeanFullName } from 'vona';
|
|
11
|
+
import { BeanAopMethodBase, getRequestBoundary, SymbolBeanFullName } from 'vona';
|
|
12
12
|
import { AopMethod } from 'vona-module-a-aspect';
|
|
13
13
|
|
|
14
14
|
export interface IAopMethodOptionsLog extends IDecoratorAopMethodOptions {
|
|
@@ -28,6 +28,7 @@ export class AopMethodLog
|
|
|
28
28
|
implements IAopMethodGet, IAopMethodSet, IAopMethodExecute
|
|
29
29
|
{
|
|
30
30
|
get(options: IAopMethodOptionsLog, next: NextSync, receiver: any, prop: string): any {
|
|
31
|
+
if (getRequestBoundary()) return next();
|
|
31
32
|
const context = this._getContext(options, receiver);
|
|
32
33
|
const message = `${receiver[SymbolBeanFullName]}#${prop}(get)`;
|
|
33
34
|
const logger = this.app.meta.logger.child(options.childName, options.clientName);
|
|
@@ -51,6 +52,7 @@ export class AopMethodLog
|
|
|
51
52
|
receiver: any,
|
|
52
53
|
prop: string,
|
|
53
54
|
): boolean {
|
|
55
|
+
if (getRequestBoundary()) return next();
|
|
54
56
|
const context = this._getContext(options, receiver);
|
|
55
57
|
const message = `${receiver[SymbolBeanFullName]}#${prop}(set)`;
|
|
56
58
|
const logger = this.app.meta.logger.child(options.childName, options.clientName);
|
|
@@ -74,6 +76,7 @@ export class AopMethodLog
|
|
|
74
76
|
receiver: any,
|
|
75
77
|
prop: string,
|
|
76
78
|
): Promise<any> | any {
|
|
79
|
+
if (getRequestBoundary()) return next();
|
|
77
80
|
const context = this._getContext(options, receiver);
|
|
78
81
|
const message = `${receiver[SymbolBeanFullName]}#${prop}`;
|
|
79
82
|
const logger = this.app.meta.logger.child(options.childName, options.clientName);
|
|
@@ -9,11 +9,14 @@ import type * as Transport from 'winston-transport';
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
BeanBase,
|
|
12
|
+
formatLoggerBoundary,
|
|
12
13
|
formatLoggerConsole,
|
|
13
14
|
formatLoggerCtx,
|
|
14
15
|
formatLoggerDummy,
|
|
15
16
|
formatLoggerErrors,
|
|
16
17
|
formatLoggerFilter,
|
|
18
|
+
getRequestBoundary,
|
|
19
|
+
getSuppressedDiagnosticLogger,
|
|
17
20
|
} from 'vona';
|
|
18
21
|
import { Bean } from 'vona-module-a-bean';
|
|
19
22
|
import * as Winston from 'winston';
|
|
@@ -39,10 +42,12 @@ export class BeanLogger extends BeanBase {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
public get default(): Winston.Logger {
|
|
45
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
42
46
|
return this.app.meta.logger.get();
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
public get(clientName?: keyof ILoggerClientRecord): Winston.Logger {
|
|
50
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
46
51
|
return this.app.meta.logger.get(clientName);
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -50,11 +55,13 @@ export class BeanLogger extends BeanBase {
|
|
|
50
55
|
childName: keyof ILoggerChildRecord,
|
|
51
56
|
clientName?: keyof ILoggerClientRecord,
|
|
52
57
|
): Winston.Logger {
|
|
58
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
53
59
|
return this.app.meta.logger.get(clientName).child({ name: childName });
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
public makeFormatBase(opts: ILoggerFormatOpts) {
|
|
57
63
|
return Winston.format.combine(
|
|
64
|
+
formatLoggerBoundary(),
|
|
58
65
|
formatLoggerErrors(opts),
|
|
59
66
|
formatLoggerCtx(),
|
|
60
67
|
Winston.format.splat(),
|
|
@@ -71,6 +78,7 @@ export class BeanLogger extends BeanBase {
|
|
|
71
78
|
return this.app.meta.logger.createTransportFile(fileName, clientInfo, {
|
|
72
79
|
level: levelStrict ?? 'silly',
|
|
73
80
|
format: Winston.format.combine(
|
|
81
|
+
formatLoggerBoundary(),
|
|
74
82
|
formatLoggerFilter({
|
|
75
83
|
child: childStrict ?? clientInfo.child,
|
|
76
84
|
level: levelStrict ?? clientInfo.level,
|
|
@@ -85,6 +93,7 @@ export class BeanLogger extends BeanBase {
|
|
|
85
93
|
return new Winston.transports.Console({
|
|
86
94
|
level: 'silly',
|
|
87
95
|
format: Winston.format.combine(
|
|
96
|
+
formatLoggerBoundary(),
|
|
88
97
|
formatLoggerDummy(),
|
|
89
98
|
formatLoggerFilter({ child: clientInfo.child, level: clientInfo.level, silly: true }),
|
|
90
99
|
Winston.format.colorize(),
|
|
@@ -5,9 +5,10 @@ import type {
|
|
|
5
5
|
} from 'vona-module-a-aspect';
|
|
6
6
|
|
|
7
7
|
import StdSerializers from 'pino-std-serializers';
|
|
8
|
-
import { BeanBase } from 'vona';
|
|
8
|
+
import { BeanBase, getRequestBoundary } from 'vona';
|
|
9
9
|
import { MiddlewareSystem } from 'vona-module-a-aspect';
|
|
10
10
|
|
|
11
|
+
import { redactHttpLogHeaders, redactHttpLogUrl } from '../lib/redactHttpLog.ts';
|
|
11
12
|
import { redactIsolatedReadyHeader } from '../lib/redactIsolatedReadyHeader.ts';
|
|
12
13
|
|
|
13
14
|
export interface IMiddlewareSystemOptionsHttpLog extends IDecoratorMiddlewareSystemOptions {}
|
|
@@ -15,28 +16,40 @@ export interface IMiddlewareSystemOptionsHttpLog extends IDecoratorMiddlewareSys
|
|
|
15
16
|
@MiddlewareSystem<IMiddlewareSystemOptionsHttpLog>()
|
|
16
17
|
export class MiddlewareSystemHttpLog extends BeanBase implements IMiddlewareSystemExecute {
|
|
17
18
|
async execute(_options: IMiddlewareSystemOptionsHttpLog, next: Next) {
|
|
19
|
+
if (getRequestBoundary()) return next();
|
|
18
20
|
const ctx = this.ctx;
|
|
19
21
|
// start
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const serialized = StdSerializers.req(ctx.req);
|
|
23
|
+
// Do not retain serializer query/params/raw extensions or request bodies in logs.
|
|
24
|
+
const req = {
|
|
25
|
+
method: serialized.method,
|
|
26
|
+
url: redactHttpLogUrl(serialized.url),
|
|
27
|
+
headers: redactHttpLogHeaders(
|
|
28
|
+
redactIsolatedReadyHeader(
|
|
29
|
+
serialized.headers,
|
|
30
|
+
ctx.path,
|
|
31
|
+
this.app.meta.env.VONA_TEST_RUN_ID,
|
|
32
|
+
['x-vona-test-challenge', 'x-vona-test-stop-nonce', 'x-vona-test-stop-authorization'],
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
remoteAddress: serialized.remoteAddress,
|
|
36
|
+
remotePort: serialized.remotePort,
|
|
37
|
+
};
|
|
27
38
|
this.$loggerChild('req').http(() => JSON.stringify(req));
|
|
28
39
|
const profiler = this.$loggerChild('res').startTimer();
|
|
29
40
|
// next
|
|
30
41
|
await next();
|
|
31
42
|
// end
|
|
32
|
-
const headers =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
const headers = redactHttpLogHeaders(
|
|
44
|
+
redactIsolatedReadyHeader(
|
|
45
|
+
ctx.res.getHeaders(),
|
|
46
|
+
ctx.path,
|
|
47
|
+
this.app.meta.env.VONA_TEST_RUN_ID,
|
|
48
|
+
['x-vona-test-proof', 'x-vona-test-stop-proof'],
|
|
49
|
+
),
|
|
37
50
|
);
|
|
38
51
|
const res = {
|
|
39
|
-
url: ctx.req.url,
|
|
52
|
+
url: redactHttpLogUrl(ctx.req.url),
|
|
40
53
|
statusCode: ctx.res.statusCode,
|
|
41
54
|
headers,
|
|
42
55
|
};
|