@nage-api/core 1.0.0-beta.2
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/LICENSE +202 -0
- package/README.md +141 -0
- package/dist/bootstrap/bootstrap.d.ts +48 -0
- package/dist/bootstrap/bootstrap.js +255 -0
- package/dist/bootstrap/drain.d.ts +48 -0
- package/dist/bootstrap/drain.js +113 -0
- package/dist/bootstrap/lifecycle.d.ts +30 -0
- package/dist/bootstrap/lifecycle.js +64 -0
- package/dist/bootstrap/process-guards.d.ts +42 -0
- package/dist/bootstrap/process-guards.js +103 -0
- package/dist/bootstrap/query-parser.d.ts +34 -0
- package/dist/bootstrap/query-parser.js +37 -0
- package/dist/bootstrap/shutdown.d.ts +55 -0
- package/dist/bootstrap/shutdown.js +182 -0
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +48 -0
- package/dist/context/active-context.d.ts +23 -0
- package/dist/context/active-context.js +34 -0
- package/dist/context/request-context.middleware.d.ts +31 -0
- package/dist/context/request-context.middleware.js +95 -0
- package/dist/context/request-context.service.d.ts +29 -0
- package/dist/context/request-context.service.js +67 -0
- package/dist/decorators/owner.decorator.d.ts +18 -0
- package/dist/decorators/owner.decorator.js +31 -0
- package/dist/decorators/public.decorator.d.ts +13 -0
- package/dist/decorators/public.decorator.js +23 -0
- package/dist/decorators/version.decorators.d.ts +34 -0
- package/dist/decorators/version.decorators.js +40 -0
- package/dist/errors/catalog.d.ts +149 -0
- package/dist/errors/catalog.js +289 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.js +22 -0
- package/dist/errors/nage.error.d.ts +43 -0
- package/dist/errors/nage.error.js +45 -0
- package/dist/guards/api-version.guard.d.ts +20 -0
- package/dist/guards/api-version.guard.js +73 -0
- package/dist/http/all-exceptions.filter.d.ts +25 -0
- package/dist/http/all-exceptions.filter.js +256 -0
- package/dist/http/envelope.d.ts +25 -0
- package/dist/http/envelope.js +44 -0
- package/dist/http/no-envelope.decorator.d.ts +11 -0
- package/dist/http/no-envelope.decorator.js +16 -0
- package/dist/http/request-timeout.decorators.d.ts +23 -0
- package/dist/http/request-timeout.decorators.js +29 -0
- package/dist/http/request-timeout.interceptor.d.ts +28 -0
- package/dist/http/request-timeout.interceptor.js +75 -0
- package/dist/http/response.interceptor.d.ts +19 -0
- package/dist/http/response.interceptor.js +73 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +135 -0
- package/dist/job/job.factory.d.ts +29 -0
- package/dist/job/job.factory.js +50 -0
- package/dist/logging/json.logger.d.ts +23 -0
- package/dist/logging/json.logger.js +136 -0
- package/dist/logging/nest-logger.adapter.d.ts +20 -0
- package/dist/logging/nest-logger.adapter.js +46 -0
- package/dist/module/core.module.d.ts +40 -0
- package/dist/module/core.module.js +112 -0
- package/dist/security/audit.d.ts +42 -0
- package/dist/security/audit.js +399 -0
- package/dist/security/index.d.ts +15 -0
- package/dist/security/index.js +50 -0
- package/dist/security/legacy-scan.d.ts +24 -0
- package/dist/security/legacy-scan.js +98 -0
- package/dist/security/random.d.ts +40 -0
- package/dist/security/random.js +87 -0
- package/dist/security/rate-limit.decorators.d.ts +24 -0
- package/dist/security/rate-limit.decorators.js +25 -0
- package/dist/security/rate-limit.guard.d.ts +44 -0
- package/dist/security/rate-limit.guard.js +130 -0
- package/dist/security/rate-limit.store.d.ts +30 -0
- package/dist/security/rate-limit.store.js +63 -0
- package/dist/security/redaction.d.ts +54 -0
- package/dist/security/redaction.js +146 -0
- package/dist/security/tls.d.ts +29 -0
- package/dist/security/tls.js +48 -0
- package/dist/tokens.d.ts +60 -0
- package/dist/tokens.js +89 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +8 -0
- package/package.json +77 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Graceful shutdown, in the order the stages actually have to happen
|
|
4
|
+
* (PLAN.md §21).
|
|
5
|
+
*
|
|
6
|
+
* `app.enableShutdownHooks()` is not this. Nest 11's signal handler runs
|
|
7
|
+
* `onModuleDestroy` → `beforeApplicationShutdown` → close the HTTP server →
|
|
8
|
+
* `onApplicationShutdown`, so **modules destroy their pools while requests are
|
|
9
|
+
* still running**. Measured against a 600ms handler signalled at 200ms: the
|
|
10
|
+
* handler's query came back `pool is closed` and the client got that as a 200.
|
|
11
|
+
* A second measurement: one handler that never returns kept the process alive
|
|
12
|
+
* past 8 seconds, because nothing in that sequence has a deadline — the
|
|
13
|
+
* container survives until the orchestrator's `SIGKILL`.
|
|
14
|
+
*
|
|
15
|
+
* This runs the stages in the order that makes each one meaningful:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Fail readiness.** The load balancer needs to stop choosing this
|
|
18
|
+
* instance, and endpoint propagation is not instant (`readinessDelayMs`).
|
|
19
|
+
* 2. **Stop accepting connections**, and release keep-alive sockets that have
|
|
20
|
+
* no request on them. Requests already in flight keep their sockets.
|
|
21
|
+
* 3. **Wait for those requests**, bounded by `drainTimeoutMs`. Past the
|
|
22
|
+
* deadline the remaining sockets are cut, because a client that has hung is
|
|
23
|
+
* not a reason to abandon the pools.
|
|
24
|
+
* 4. **`app.close()`** — only now do modules close pools, queues and Redis.
|
|
25
|
+
* 5. **Exit.** Zero if the sequence finished, non-zero if `forceExitAfterMs`
|
|
26
|
+
* expired, so a deploy can tell a clean drain from a broken one.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.installShutdown = installShutdown;
|
|
30
|
+
exports.registerShutdownHandle = registerShutdownHandle;
|
|
31
|
+
exports.shutdownHandleFor = shutdownHandleFor;
|
|
32
|
+
const json_logger_js_1 = require("../logging/json.logger.js");
|
|
33
|
+
const lifecycle_js_1 = require("./lifecycle.js");
|
|
34
|
+
const drain_js_1 = require("./drain.js");
|
|
35
|
+
const DEFAULT_SIGNALS = ['SIGTERM', 'SIGINT'];
|
|
36
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 10_000;
|
|
37
|
+
const DEFAULT_FORCE_EXIT_MS = 30_000;
|
|
38
|
+
function installShutdown(app, options = {}) {
|
|
39
|
+
const config = options.config;
|
|
40
|
+
const logger = options.logger ?? new json_logger_js_1.JsonLogger(undefined, config);
|
|
41
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
42
|
+
const signals = options.signals ?? DEFAULT_SIGNALS;
|
|
43
|
+
const lifecycle = options.lifecycle ?? (0, lifecycle_js_1.processLifecycle)();
|
|
44
|
+
const drain = options.drain ?? (0, drain_js_1.requestDrainFor)(app) ?? new drain_js_1.RequestDrain();
|
|
45
|
+
const readinessDelayMs = config?.shutdown?.readinessDelayMs ?? 0;
|
|
46
|
+
const drainTimeoutMs = config?.shutdown?.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
47
|
+
const forceExitAfterMs = config?.shutdown?.forceExitAfterMs ?? DEFAULT_FORCE_EXIT_MS;
|
|
48
|
+
let running;
|
|
49
|
+
const run = async (reason) => {
|
|
50
|
+
running ??= sequence(reason);
|
|
51
|
+
return running;
|
|
52
|
+
};
|
|
53
|
+
const sequence = async (reason) => {
|
|
54
|
+
lifecycle.markDraining();
|
|
55
|
+
logger.info('Shutdown started', { reason, inFlight: drain.active, drainTimeoutMs });
|
|
56
|
+
// Deliberately not `unref`ed. An `await` does not hold the event loop open,
|
|
57
|
+
// so a hook that never settles would otherwise let the process exit 0 with
|
|
58
|
+
// its pools unclosed — a silent failure dressed as a clean shutdown.
|
|
59
|
+
// A holder rather than a `let`: the flag is set inside a timer callback, and
|
|
60
|
+
// the type system narrows a local boolean to `false` at every later read
|
|
61
|
+
// because it cannot order the closure.
|
|
62
|
+
const state = { expired: false };
|
|
63
|
+
const deadline = setTimeout(() => {
|
|
64
|
+
state.expired = true;
|
|
65
|
+
logger.fatal('Shutdown deadline expired; exiting without finishing', {
|
|
66
|
+
reason,
|
|
67
|
+
forceExitAfterMs,
|
|
68
|
+
inFlight: drain.active,
|
|
69
|
+
});
|
|
70
|
+
exit(1);
|
|
71
|
+
}, forceExitAfterMs);
|
|
72
|
+
try {
|
|
73
|
+
// 1. Readiness fails from `markDraining()` above; this is the window the
|
|
74
|
+
// load balancer needs to act on it.
|
|
75
|
+
if (readinessDelayMs > 0)
|
|
76
|
+
await delay(readinessDelayMs);
|
|
77
|
+
// 2. No new connections. Keep-alive sockets with nothing on them are
|
|
78
|
+
// released here, so they do not hold the server open for their idle
|
|
79
|
+
// timeout.
|
|
80
|
+
const server = httpServerOf(app);
|
|
81
|
+
if (server?.listening === true) {
|
|
82
|
+
server.close();
|
|
83
|
+
server.closeIdleConnections?.();
|
|
84
|
+
}
|
|
85
|
+
// 3. The requests we already accepted.
|
|
86
|
+
const drained = await drain.whenIdle(drainTimeoutMs);
|
|
87
|
+
if (drained) {
|
|
88
|
+
// Again, and deliberately: a socket that was mid-request in step 2 is
|
|
89
|
+
// idle now. `app.close()` waits on `httpServer.close()`, which waits on
|
|
90
|
+
// every open socket, so a keep-alive connection whose request has
|
|
91
|
+
// finished would otherwise hold the shutdown for its idle timeout.
|
|
92
|
+
server?.closeIdleConnections?.();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
logger.warn('Drain deadline expired; cutting remaining connections', {
|
|
96
|
+
inFlight: drain.active,
|
|
97
|
+
drainTimeoutMs,
|
|
98
|
+
});
|
|
99
|
+
server?.closeAllConnections?.();
|
|
100
|
+
}
|
|
101
|
+
// 4. Now the pools. Nothing is using them.
|
|
102
|
+
await app.close();
|
|
103
|
+
clearTimeout(deadline);
|
|
104
|
+
// The deadline already called `exit(1)`; in a process where `exit` is
|
|
105
|
+
// injected (a test) the sequence keeps running, and it must not then report
|
|
106
|
+
// success.
|
|
107
|
+
if (state.expired)
|
|
108
|
+
return 1;
|
|
109
|
+
logger.info('Shutdown complete', { reason });
|
|
110
|
+
exit(0);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
clearTimeout(deadline);
|
|
115
|
+
logger.fatal('Shutdown failed', {
|
|
116
|
+
reason,
|
|
117
|
+
error: error instanceof Error ? error.message : String(error),
|
|
118
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
119
|
+
});
|
|
120
|
+
exit(1);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const onSignal = (signal) => {
|
|
125
|
+
if (running !== undefined) {
|
|
126
|
+
// An operator pressing ctrl-C twice, or a supervisor escalating, is asking
|
|
127
|
+
// for the drain to stop being polite.
|
|
128
|
+
logger.warn('Second shutdown signal; exiting immediately', {
|
|
129
|
+
signal,
|
|
130
|
+
inFlight: drain.active,
|
|
131
|
+
});
|
|
132
|
+
exit(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
void run(signal);
|
|
136
|
+
};
|
|
137
|
+
const listeners = new Map();
|
|
138
|
+
for (const signal of signals) {
|
|
139
|
+
const listener = () => {
|
|
140
|
+
onSignal(signal);
|
|
141
|
+
};
|
|
142
|
+
listeners.set(signal, listener);
|
|
143
|
+
process.on(signal, listener);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
get draining() {
|
|
147
|
+
return lifecycle.draining;
|
|
148
|
+
},
|
|
149
|
+
run,
|
|
150
|
+
dispose() {
|
|
151
|
+
for (const [signal, listener] of listeners)
|
|
152
|
+
process.removeListener(signal, listener);
|
|
153
|
+
listeners.clear();
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The handle `bootstrap` installed for an application.
|
|
159
|
+
*
|
|
160
|
+
* Exported for two callers with the same need: a test that boots an application
|
|
161
|
+
* and must take its signal listeners off the process again, and an application
|
|
162
|
+
* embedded in something else that owns its own signal handling.
|
|
163
|
+
*/
|
|
164
|
+
const handles = new WeakMap();
|
|
165
|
+
function registerShutdownHandle(app, handle) {
|
|
166
|
+
handles.set(app, handle);
|
|
167
|
+
}
|
|
168
|
+
function shutdownHandleFor(app) {
|
|
169
|
+
return handles.get(app);
|
|
170
|
+
}
|
|
171
|
+
function httpServerOf(app) {
|
|
172
|
+
const server = app.getHttpServer?.();
|
|
173
|
+
if (server === null || typeof server !== 'object')
|
|
174
|
+
return undefined;
|
|
175
|
+
return typeof server.close === 'function' ? server : undefined;
|
|
176
|
+
}
|
|
177
|
+
async function delay(ms) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
setTimeout(resolve, ms);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=shutdown.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Header and metadata names shared across the framework (PLAN.md §16.2). */
|
|
2
|
+
/** Correlation id accepted from the caller and echoed on every response. */
|
|
3
|
+
export declare const REQUEST_ID_HEADER = "x-request-id";
|
|
4
|
+
/** Upstream trace id, when the caller is another service. */
|
|
5
|
+
export declare const CORRELATION_ID_HEADER = "x-correlation-id";
|
|
6
|
+
/** Retained from the legacy framework so existing clients keep working. */
|
|
7
|
+
export declare const API_VERSION_HEADER = "x-application-version";
|
|
8
|
+
export declare const DEFAULT_API_VERSION = 1;
|
|
9
|
+
/**
|
|
10
|
+
* Accepted shape of a caller-supplied request id. Anything else is replaced with
|
|
11
|
+
* a generated one — an id echoed into responses and log lines is untrusted input
|
|
12
|
+
* (log forging, header injection), so it is validated rather than trusted.
|
|
13
|
+
*/
|
|
14
|
+
export declare const REQUEST_ID_PATTERN: RegExp;
|
|
15
|
+
/** Reflector metadata keys. */
|
|
16
|
+
export declare const METADATA_KEYS: {
|
|
17
|
+
readonly isPublic: "nage:is-public";
|
|
18
|
+
readonly noEnvelope: "nage:no-envelope";
|
|
19
|
+
readonly versionRule: "nage:version-rule";
|
|
20
|
+
readonly rateLimit: "nage:rate-limit";
|
|
21
|
+
readonly skipRateLimit: "nage:skip-rate-limit";
|
|
22
|
+
readonly requestTimeout: "nage:request-timeout";
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Ceiling on handler duration (§21). An unbounded handler holds its connection
|
|
26
|
+
* until the client gives up, and a client that has given up is a connection the
|
|
27
|
+
* server is still paying for.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
30
|
+
/** Field names redacted from every log line by default — extend, never shrink (§18). */
|
|
31
|
+
export declare const DEFAULT_REDACTED_FIELDS: readonly string[];
|
|
32
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Header and metadata names shared across the framework (PLAN.md §16.2). */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.DEFAULT_REDACTED_FIELDS = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.METADATA_KEYS = exports.REQUEST_ID_PATTERN = exports.DEFAULT_API_VERSION = exports.API_VERSION_HEADER = exports.CORRELATION_ID_HEADER = exports.REQUEST_ID_HEADER = void 0;
|
|
5
|
+
/** Correlation id accepted from the caller and echoed on every response. */
|
|
6
|
+
exports.REQUEST_ID_HEADER = 'x-request-id';
|
|
7
|
+
/** Upstream trace id, when the caller is another service. */
|
|
8
|
+
exports.CORRELATION_ID_HEADER = 'x-correlation-id';
|
|
9
|
+
/** Retained from the legacy framework so existing clients keep working. */
|
|
10
|
+
exports.API_VERSION_HEADER = 'x-application-version';
|
|
11
|
+
exports.DEFAULT_API_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Accepted shape of a caller-supplied request id. Anything else is replaced with
|
|
14
|
+
* a generated one — an id echoed into responses and log lines is untrusted input
|
|
15
|
+
* (log forging, header injection), so it is validated rather than trusted.
|
|
16
|
+
*/
|
|
17
|
+
exports.REQUEST_ID_PATTERN = /^[A-Za-z0-9._~-]{8,128}$/;
|
|
18
|
+
/** Reflector metadata keys. */
|
|
19
|
+
exports.METADATA_KEYS = {
|
|
20
|
+
isPublic: 'nage:is-public',
|
|
21
|
+
noEnvelope: 'nage:no-envelope',
|
|
22
|
+
versionRule: 'nage:version-rule',
|
|
23
|
+
rateLimit: 'nage:rate-limit',
|
|
24
|
+
skipRateLimit: 'nage:skip-rate-limit',
|
|
25
|
+
requestTimeout: 'nage:request-timeout',
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Ceiling on handler duration (§21). An unbounded handler holds its connection
|
|
29
|
+
* until the client gives up, and a client that has given up is a connection the
|
|
30
|
+
* server is still paying for.
|
|
31
|
+
*/
|
|
32
|
+
exports.DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
33
|
+
/** Field names redacted from every log line by default — extend, never shrink (§18). */
|
|
34
|
+
exports.DEFAULT_REDACTED_FIELDS = [
|
|
35
|
+
'password',
|
|
36
|
+
'passwordConfirmation',
|
|
37
|
+
'token',
|
|
38
|
+
'accessToken',
|
|
39
|
+
'refreshToken',
|
|
40
|
+
'refresh_token',
|
|
41
|
+
'authorization',
|
|
42
|
+
'cookie',
|
|
43
|
+
'secret',
|
|
44
|
+
'apiKey',
|
|
45
|
+
'api_key',
|
|
46
|
+
'otp',
|
|
47
|
+
];
|
|
48
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process-wide `AsyncLocalStorage` backing the request context.
|
|
3
|
+
*
|
|
4
|
+
* It lives at module scope, not inside the injectable, for one reason: param
|
|
5
|
+
* decorators (`@Owner()`), static helpers and non-DI code (a repository base
|
|
6
|
+
* class, a queue publisher) need the same context that services see, and a
|
|
7
|
+
* decorator factory has nothing to inject into.
|
|
8
|
+
*
|
|
9
|
+
* `RequestContextService` is the DI-facing wrapper over these functions — both
|
|
10
|
+
* read and write the same store.
|
|
11
|
+
*/
|
|
12
|
+
import type { RequestContext } from '@nage-api/contracts';
|
|
13
|
+
/** Internal, mutable view. Callers outside this module only see the readonly type. */
|
|
14
|
+
export type MutableRequestContext = {
|
|
15
|
+
-readonly [K in keyof RequestContext]: RequestContext[K];
|
|
16
|
+
};
|
|
17
|
+
/** The active context, or `undefined` outside a request. */
|
|
18
|
+
export declare function getActiveContext(): RequestContext | undefined;
|
|
19
|
+
/** Run `fn` with `context` active for its whole async subtree. */
|
|
20
|
+
export declare function runWithContext<TResult>(context: RequestContext, fn: () => TResult): TResult;
|
|
21
|
+
/** Update one field of the active context; a no-op when none is active. */
|
|
22
|
+
export declare function setContextValue<TKey extends keyof RequestContext>(key: TKey, value: RequestContext[TKey]): void;
|
|
23
|
+
//# sourceMappingURL=active-context.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The process-wide `AsyncLocalStorage` backing the request context.
|
|
4
|
+
*
|
|
5
|
+
* It lives at module scope, not inside the injectable, for one reason: param
|
|
6
|
+
* decorators (`@Owner()`), static helpers and non-DI code (a repository base
|
|
7
|
+
* class, a queue publisher) need the same context that services see, and a
|
|
8
|
+
* decorator factory has nothing to inject into.
|
|
9
|
+
*
|
|
10
|
+
* `RequestContextService` is the DI-facing wrapper over these functions — both
|
|
11
|
+
* read and write the same store.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.getActiveContext = getActiveContext;
|
|
15
|
+
exports.runWithContext = runWithContext;
|
|
16
|
+
exports.setContextValue = setContextValue;
|
|
17
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
18
|
+
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
19
|
+
/** The active context, or `undefined` outside a request. */
|
|
20
|
+
function getActiveContext() {
|
|
21
|
+
return storage.getStore();
|
|
22
|
+
}
|
|
23
|
+
/** Run `fn` with `context` active for its whole async subtree. */
|
|
24
|
+
function runWithContext(context, fn) {
|
|
25
|
+
return storage.run({ ...context }, fn);
|
|
26
|
+
}
|
|
27
|
+
/** Update one field of the active context; a no-op when none is active. */
|
|
28
|
+
function setContextValue(key, value) {
|
|
29
|
+
const context = storage.getStore();
|
|
30
|
+
if (context === undefined)
|
|
31
|
+
return;
|
|
32
|
+
context[key] = value;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=active-context.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opens the request context and settles the correlation id (PLAN.md §16.2, §18).
|
|
3
|
+
*
|
|
4
|
+
* Runs before guards, so everything downstream — guards, interceptors, services,
|
|
5
|
+
* the exception filter — sees the same `requestId`, and the caller gets it back
|
|
6
|
+
* in the `X-Request-Id` header whether the request succeeded or failed.
|
|
7
|
+
*/
|
|
8
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
9
|
+
import { type NestMiddleware } from '@nestjs/common';
|
|
10
|
+
import type { NageCoreConfig } from '@nage-api/contracts';
|
|
11
|
+
import { RequestContextService } from './request-context.service.js';
|
|
12
|
+
/** What core needs from a request object, without binding to Express. */
|
|
13
|
+
interface HttpRequestLike extends IncomingMessage {
|
|
14
|
+
readonly ip?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A caller-supplied id is echoed into responses and log lines, so it is treated
|
|
18
|
+
* as untrusted input: anything not matching the expected shape is replaced
|
|
19
|
+
* rather than sanitized, which closes off log forging and header injection.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveRequestId(candidate: string | undefined): string;
|
|
22
|
+
/** Parse `x-application-version`; a malformed value falls back to the default. */
|
|
23
|
+
export declare function parseApiVersion(candidate: string | undefined): number | undefined;
|
|
24
|
+
export declare class RequestContextMiddleware implements NestMiddleware {
|
|
25
|
+
#private;
|
|
26
|
+
private readonly context;
|
|
27
|
+
constructor(context: RequestContextService, config?: NageCoreConfig);
|
|
28
|
+
use(request: HttpRequestLike, response: ServerResponse, next: () => void): void;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=request-context.middleware.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Opens the request context and settles the correlation id (PLAN.md §16.2, §18).
|
|
4
|
+
*
|
|
5
|
+
* Runs before guards, so everything downstream — guards, interceptors, services,
|
|
6
|
+
* the exception filter — sees the same `requestId`, and the caller gets it back
|
|
7
|
+
* in the `X-Request-Id` header whether the request succeeded or failed.
|
|
8
|
+
*/
|
|
9
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
10
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
11
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
12
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
13
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
14
|
+
};
|
|
15
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
16
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
17
|
+
};
|
|
18
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
19
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.RequestContextMiddleware = void 0;
|
|
23
|
+
exports.resolveRequestId = resolveRequestId;
|
|
24
|
+
exports.parseApiVersion = parseApiVersion;
|
|
25
|
+
const node_crypto_1 = require("node:crypto");
|
|
26
|
+
const common_1 = require("@nestjs/common");
|
|
27
|
+
const constants_js_1 = require("../constants.js");
|
|
28
|
+
const tokens_js_1 = require("../tokens.js");
|
|
29
|
+
const request_context_service_js_1 = require("./request-context.service.js");
|
|
30
|
+
function headerValue(request, name) {
|
|
31
|
+
const raw = request.headers[name];
|
|
32
|
+
if (Array.isArray(raw))
|
|
33
|
+
return raw[0];
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A caller-supplied id is echoed into responses and log lines, so it is treated
|
|
38
|
+
* as untrusted input: anything not matching the expected shape is replaced
|
|
39
|
+
* rather than sanitized, which closes off log forging and header injection.
|
|
40
|
+
*/
|
|
41
|
+
function resolveRequestId(candidate) {
|
|
42
|
+
if (candidate !== undefined && constants_js_1.REQUEST_ID_PATTERN.test(candidate))
|
|
43
|
+
return candidate;
|
|
44
|
+
return (0, node_crypto_1.randomUUID)();
|
|
45
|
+
}
|
|
46
|
+
/** Parse `x-application-version`; a malformed value falls back to the default. */
|
|
47
|
+
function parseApiVersion(candidate) {
|
|
48
|
+
if (candidate === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
const version = Number.parseInt(candidate, 10);
|
|
51
|
+
if (!Number.isInteger(version) || version < 1)
|
|
52
|
+
return undefined;
|
|
53
|
+
return version;
|
|
54
|
+
}
|
|
55
|
+
let RequestContextMiddleware = class RequestContextMiddleware {
|
|
56
|
+
context;
|
|
57
|
+
#versionHeader;
|
|
58
|
+
constructor(context, config) {
|
|
59
|
+
this.context = context;
|
|
60
|
+
this.#versionHeader = (config?.versioning?.header ?? constants_js_1.API_VERSION_HEADER).toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
use(request, response, next) {
|
|
63
|
+
// Idempotent: if a context is already open (nested router, double
|
|
64
|
+
// registration) keep it rather than minting a second correlation id.
|
|
65
|
+
if (this.context.get() !== undefined) {
|
|
66
|
+
next();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const requestId = resolveRequestId(headerValue(request, constants_js_1.REQUEST_ID_HEADER));
|
|
70
|
+
const correlationId = headerValue(request, constants_js_1.CORRELATION_ID_HEADER);
|
|
71
|
+
const apiVersion = parseApiVersion(headerValue(request, this.#versionHeader));
|
|
72
|
+
const userAgent = headerValue(request, 'user-agent');
|
|
73
|
+
const context = {
|
|
74
|
+
requestId,
|
|
75
|
+
startedAt: Date.now(),
|
|
76
|
+
...(correlationId !== undefined && constants_js_1.REQUEST_ID_PATTERN.test(correlationId)
|
|
77
|
+
? { correlationId }
|
|
78
|
+
: {}),
|
|
79
|
+
...(apiVersion !== undefined ? { apiVersion } : {}),
|
|
80
|
+
...(request.ip !== undefined ? { ip: request.ip } : {}),
|
|
81
|
+
...(userAgent !== undefined ? { userAgent } : {}),
|
|
82
|
+
};
|
|
83
|
+
response.setHeader(constants_js_1.REQUEST_ID_HEADER, requestId);
|
|
84
|
+
this.context.run(context, next);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
exports.RequestContextMiddleware = RequestContextMiddleware;
|
|
88
|
+
exports.RequestContextMiddleware = RequestContextMiddleware = __decorate([
|
|
89
|
+
(0, common_1.Injectable)(),
|
|
90
|
+
__param(0, (0, common_1.Inject)(request_context_service_js_1.RequestContextService)),
|
|
91
|
+
__param(1, (0, common_1.Optional)()),
|
|
92
|
+
__param(1, (0, common_1.Inject)(tokens_js_1.NAGE_CONFIG)),
|
|
93
|
+
__metadata("design:paramtypes", [request_context_service_js_1.RequestContextService, Object])
|
|
94
|
+
], RequestContextMiddleware);
|
|
95
|
+
//# sourceMappingURL=request-context.middleware.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient per-request context (PLAN.md §18).
|
|
3
|
+
*
|
|
4
|
+
* The legacy framework declared a `ClsStore` and never populated it, so no log
|
|
5
|
+
* line or response could be correlated to a request. This is the DI-facing
|
|
6
|
+
* wrapper over the process-wide store in `active-context.ts`: any code running
|
|
7
|
+
* inside the request — service, guard, repository, queue publisher — reads the
|
|
8
|
+
* same context, with nothing threaded through call signatures.
|
|
9
|
+
*/
|
|
10
|
+
import type { AuthUser, ContextStore, Id, RequestContext } from '@nage-api/contracts';
|
|
11
|
+
export declare class RequestContextService implements ContextStore {
|
|
12
|
+
/** The active context, or `undefined` outside a request (jobs, boot code). */
|
|
13
|
+
get(): RequestContext | undefined;
|
|
14
|
+
/** The active context, or a thrown `INTERNAL_ERROR` when there is none. */
|
|
15
|
+
require(): RequestContext;
|
|
16
|
+
/** Run `fn` with `context` as the ambient context. */
|
|
17
|
+
run<TResult>(context: RequestContext, fn: () => TResult): TResult;
|
|
18
|
+
/**
|
|
19
|
+
* Update one field of the active context — how the auth guard attaches the
|
|
20
|
+
* resolved user once a token has been verified. A no-op outside a request, so
|
|
21
|
+
* background code never has to guard the call.
|
|
22
|
+
*/
|
|
23
|
+
set<TKey extends keyof RequestContext>(key: TKey, value: RequestContext[TKey]): void;
|
|
24
|
+
/** Convenience accessors for the fields nearly every caller wants. */
|
|
25
|
+
get requestId(): string | undefined;
|
|
26
|
+
get user(): AuthUser | undefined;
|
|
27
|
+
get tenantId(): Id | undefined;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=request-context.service.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Ambient per-request context (PLAN.md §18).
|
|
4
|
+
*
|
|
5
|
+
* The legacy framework declared a `ClsStore` and never populated it, so no log
|
|
6
|
+
* line or response could be correlated to a request. This is the DI-facing
|
|
7
|
+
* wrapper over the process-wide store in `active-context.ts`: any code running
|
|
8
|
+
* inside the request — service, guard, repository, queue publisher — reads the
|
|
9
|
+
* same context, with nothing threaded through call signatures.
|
|
10
|
+
*/
|
|
11
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
12
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
13
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
14
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
15
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
16
|
+
};
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.RequestContextService = void 0;
|
|
19
|
+
const common_1 = require("@nestjs/common");
|
|
20
|
+
const catalog_js_1 = require("../errors/catalog.js");
|
|
21
|
+
const active_context_js_1 = require("./active-context.js");
|
|
22
|
+
let RequestContextService = class RequestContextService {
|
|
23
|
+
/** The active context, or `undefined` outside a request (jobs, boot code). */
|
|
24
|
+
get() {
|
|
25
|
+
return (0, active_context_js_1.getActiveContext)();
|
|
26
|
+
}
|
|
27
|
+
/** The active context, or a thrown `INTERNAL_ERROR` when there is none. */
|
|
28
|
+
require() {
|
|
29
|
+
const context = (0, active_context_js_1.getActiveContext)();
|
|
30
|
+
if (context === undefined) {
|
|
31
|
+
throw new catalog_js_1.InternalError({
|
|
32
|
+
detail: 'No request context is active',
|
|
33
|
+
meta: {
|
|
34
|
+
hint: 'RequestContextMiddleware must run before this code, or wrap the work in context.run().',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return context;
|
|
39
|
+
}
|
|
40
|
+
/** Run `fn` with `context` as the ambient context. */
|
|
41
|
+
run(context, fn) {
|
|
42
|
+
return (0, active_context_js_1.runWithContext)(context, fn);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Update one field of the active context — how the auth guard attaches the
|
|
46
|
+
* resolved user once a token has been verified. A no-op outside a request, so
|
|
47
|
+
* background code never has to guard the call.
|
|
48
|
+
*/
|
|
49
|
+
set(key, value) {
|
|
50
|
+
(0, active_context_js_1.setContextValue)(key, value);
|
|
51
|
+
}
|
|
52
|
+
/** Convenience accessors for the fields nearly every caller wants. */
|
|
53
|
+
get requestId() {
|
|
54
|
+
return (0, active_context_js_1.getActiveContext)()?.requestId;
|
|
55
|
+
}
|
|
56
|
+
get user() {
|
|
57
|
+
return (0, active_context_js_1.getActiveContext)()?.user;
|
|
58
|
+
}
|
|
59
|
+
get tenantId() {
|
|
60
|
+
return (0, active_context_js_1.getActiveContext)()?.tenantId;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
exports.RequestContextService = RequestContextService;
|
|
64
|
+
exports.RequestContextService = RequestContextService = __decorate([
|
|
65
|
+
(0, common_1.Injectable)()
|
|
66
|
+
], RequestContextService);
|
|
67
|
+
//# sourceMappingURL=request-context.service.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@Owner()` — the authenticated principal for the current request
|
|
3
|
+
* (PLAN.md §13, §15.2).
|
|
4
|
+
*
|
|
5
|
+
* Typed as `AuthUser`, not `any`: the legacy `OwnerDto.info: any` is exactly the
|
|
6
|
+
* kind of boundary this framework refuses. Reads from the ambient context rather
|
|
7
|
+
* than `request.user`, so the same value is visible to services, repositories
|
|
8
|
+
* and queue publishers without threading it through call signatures.
|
|
9
|
+
*/
|
|
10
|
+
import type { AuthUser } from '@nage-api/contracts';
|
|
11
|
+
/**
|
|
12
|
+
* @param field optional property to pluck, e.g. `@Owner('id')`
|
|
13
|
+
* @throws AuthenticationError when the route is reached without a principal
|
|
14
|
+
*/
|
|
15
|
+
export declare const Owner: (...dataOrPipes: (keyof AuthUser<string> | import("@nestjs/common").PipeTransform<any, any> | import("@nestjs/common").Type<import("@nestjs/common").PipeTransform<any, any>> | undefined)[]) => ParameterDecorator;
|
|
16
|
+
/** Same as `@Owner()` but yields `undefined` on public routes instead of throwing. */
|
|
17
|
+
export declare const OptionalOwner: (...dataOrPipes: unknown[]) => ParameterDecorator;
|
|
18
|
+
//# sourceMappingURL=owner.decorator.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `@Owner()` — the authenticated principal for the current request
|
|
4
|
+
* (PLAN.md §13, §15.2).
|
|
5
|
+
*
|
|
6
|
+
* Typed as `AuthUser`, not `any`: the legacy `OwnerDto.info: any` is exactly the
|
|
7
|
+
* kind of boundary this framework refuses. Reads from the ambient context rather
|
|
8
|
+
* than `request.user`, so the same value is visible to services, repositories
|
|
9
|
+
* and queue publishers without threading it through call signatures.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.OptionalOwner = exports.Owner = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const catalog_js_1 = require("../errors/catalog.js");
|
|
15
|
+
const active_context_js_1 = require("../context/active-context.js");
|
|
16
|
+
/**
|
|
17
|
+
* @param field optional property to pluck, e.g. `@Owner('id')`
|
|
18
|
+
* @throws AuthenticationError when the route is reached without a principal
|
|
19
|
+
*/
|
|
20
|
+
exports.Owner = (0, common_1.createParamDecorator)((field, _context) => {
|
|
21
|
+
const user = (0, active_context_js_1.getActiveContext)()?.user;
|
|
22
|
+
if (user === undefined) {
|
|
23
|
+
throw new catalog_js_1.AuthenticationError('AUTH_REQUIRED', {
|
|
24
|
+
meta: { hint: '@Owner() used on a route that is not behind an authentication guard' },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return field === undefined ? user : user[field];
|
|
28
|
+
});
|
|
29
|
+
/** Same as `@Owner()` but yields `undefined` on public routes instead of throwing. */
|
|
30
|
+
exports.OptionalOwner = (0, common_1.createParamDecorator)((_data, _context) => (0, active_context_js_1.getActiveContext)()?.user);
|
|
31
|
+
//# sourceMappingURL=owner.decorator.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type CustomDecorator, type ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import type { Reflector } from '@nestjs/core';
|
|
3
|
+
/**
|
|
4
|
+
* Marks a route as reachable without authentication (PLAN.md §15.1).
|
|
5
|
+
*
|
|
6
|
+
* Authentication is global and opt-**out**: forgetting a decorator leaves a
|
|
7
|
+
* route protected rather than open, which is the inverse of the legacy
|
|
8
|
+
* per-controller guard wiring.
|
|
9
|
+
*/
|
|
10
|
+
export declare const Public: () => CustomDecorator;
|
|
11
|
+
/** Read the `@Public()` marker for the handler being executed. */
|
|
12
|
+
export declare function isPublicRoute(reflector: Reflector, context: ExecutionContext): boolean;
|
|
13
|
+
//# sourceMappingURL=public.decorator.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Public = void 0;
|
|
4
|
+
exports.isPublicRoute = isPublicRoute;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const constants_js_1 = require("../constants.js");
|
|
7
|
+
/**
|
|
8
|
+
* Marks a route as reachable without authentication (PLAN.md §15.1).
|
|
9
|
+
*
|
|
10
|
+
* Authentication is global and opt-**out**: forgetting a decorator leaves a
|
|
11
|
+
* route protected rather than open, which is the inverse of the legacy
|
|
12
|
+
* per-controller guard wiring.
|
|
13
|
+
*/
|
|
14
|
+
const Public = () => (0, common_1.SetMetadata)(constants_js_1.METADATA_KEYS.isPublic, true);
|
|
15
|
+
exports.Public = Public;
|
|
16
|
+
/** Read the `@Public()` marker for the handler being executed. */
|
|
17
|
+
function isPublicRoute(reflector, context) {
|
|
18
|
+
return (reflector.getAllAndOverride(constants_js_1.METADATA_KEYS.isPublic, [
|
|
19
|
+
context.getHandler(),
|
|
20
|
+
context.getClass(),
|
|
21
|
+
]) === true);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=public.decorator.js.map
|