@selvajs/server 1.0.0-beta.9 → 1.0.1
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/README.md +18 -20
- package/dist/access/project-access-input.d.ts +13 -16
- package/dist/access/project-access-input.d.ts.map +1 -1
- package/dist/access/project-access-input.js +2 -2
- package/dist/access/project-access-input.js.map +1 -1
- package/dist/compute/idempotency.d.ts +18 -22
- package/dist/compute/idempotency.d.ts.map +1 -1
- package/dist/compute/idempotency.js +10 -14
- package/dist/compute/idempotency.js.map +1 -1
- package/dist/compute/index.d.ts.map +1 -1
- package/dist/compute/index.js +3 -5
- package/dist/compute/index.js.map +1 -1
- package/dist/compute/limits.d.ts +70 -86
- package/dist/compute/limits.d.ts.map +1 -1
- package/dist/compute/limits.js +27 -56
- package/dist/compute/limits.js.map +1 -1
- package/dist/compute/rate-limit.d.ts +41 -53
- package/dist/compute/rate-limit.d.ts.map +1 -1
- package/dist/compute/rate-limit.js +27 -35
- package/dist/compute/rate-limit.js.map +1 -1
- package/dist/compute/remote-definition.d.ts +8 -22
- package/dist/compute/remote-definition.d.ts.map +1 -1
- package/dist/compute/remote-definition.js +9 -19
- package/dist/compute/remote-definition.js.map +1 -1
- package/dist/compute/safe-url.d.ts +7 -5
- package/dist/compute/safe-url.d.ts.map +1 -1
- package/dist/compute/safe-url.js +26 -28
- package/dist/compute/safe-url.js.map +1 -1
- package/dist/definitions/definition-service.d.ts +13 -38
- package/dist/definitions/definition-service.d.ts.map +1 -1
- package/dist/definitions/definition-service.js +25 -53
- package/dist/definitions/definition-service.js.map +1 -1
- package/dist/definitions/index.d.ts +2 -2
- package/dist/definitions/index.d.ts.map +1 -1
- package/dist/definitions/index.js +1 -5
- package/dist/definitions/index.js.map +1 -1
- package/dist/definitions/load-for-render.d.ts +23 -20
- package/dist/definitions/load-for-render.d.ts.map +1 -1
- package/dist/definitions/load-for-render.js +55 -41
- package/dist/definitions/load-for-render.js.map +1 -1
- package/dist/definitions/schema-extraction.d.ts +36 -16
- package/dist/definitions/schema-extraction.d.ts.map +1 -1
- package/dist/definitions/schema-extraction.js +65 -28
- package/dist/definitions/schema-extraction.js.map +1 -1
- package/dist/errors/SentryErrorReporter.d.ts +2 -3
- package/dist/errors/SentryErrorReporter.d.ts.map +1 -1
- package/dist/errors/SentryErrorReporter.js +10 -14
- package/dist/errors/SentryErrorReporter.js.map +1 -1
- package/dist/http/body-size.d.ts +8 -11
- package/dist/http/body-size.d.ts.map +1 -1
- package/dist/http/body-size.js +8 -11
- package/dist/http/body-size.js.map +1 -1
- package/dist/http/redirect.d.ts +3 -7
- package/dist/http/redirect.d.ts.map +1 -1
- package/dist/http/redirect.js +8 -8
- package/dist/http/redirect.js.map +1 -1
- package/dist/http/route-classifier.d.ts +20 -24
- package/dist/http/route-classifier.d.ts.map +1 -1
- package/dist/http/route-classifier.js +6 -7
- package/dist/http/route-classifier.js.map +1 -1
- package/dist/http/security-headers.d.ts +13 -15
- package/dist/http/security-headers.d.ts.map +1 -1
- package/dist/http/security-headers.js +10 -12
- package/dist/http/security-headers.js.map +1 -1
- package/dist/logging/PinoLogger.d.ts +8 -14
- package/dist/logging/PinoLogger.d.ts.map +1 -1
- package/dist/logging/PinoLogger.js +29 -35
- package/dist/logging/PinoLogger.js.map +1 -1
- package/dist/logging/requestId.d.ts +20 -28
- package/dist/logging/requestId.d.ts.map +1 -1
- package/dist/logging/requestId.js +29 -37
- package/dist/logging/requestId.js.map +1 -1
- package/dist/ops/index.d.ts.map +1 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/index.js.map +1 -1
- package/dist/ops/semver.d.ts +22 -24
- package/dist/ops/semver.d.ts.map +1 -1
- package/dist/ops/semver.js +22 -25
- package/dist/ops/semver.js.map +1 -1
- package/dist/providers/create-selva-providers.d.ts +19 -27
- package/dist/providers/create-selva-providers.d.ts.map +1 -1
- package/dist/providers/create-selva-providers.js +28 -34
- package/dist/providers/create-selva-providers.js.map +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +2 -4
- package/dist/providers/index.js.map +1 -1
- package/dist/tokens/token-codec.d.ts +24 -27
- package/dist/tokens/token-codec.d.ts.map +1 -1
- package/dist/tokens/token-codec.js +18 -21
- package/dist/tokens/token-codec.js.map +1 -1
- package/package.json +5 -5
package/dist/http/redirect.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Validate a user-supplied post-login redirect target
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* and `/\evil.com` (back-slash variants some browsers normalize) are rejected.
|
|
6
|
-
*
|
|
7
|
-
* Always returns a safe path: the validated target on success, the fallback
|
|
8
|
-
* otherwise. Routes call this with `redirectTo` from form data or query string.
|
|
2
|
+
* Validate a user-supplied post-login redirect target (`redirectTo` from form
|
|
3
|
+
* data or a query string) against open redirects. Returns the target when it is
|
|
4
|
+
* a same-origin relative path, the fallback otherwise — never a raw target.
|
|
9
5
|
*/
|
|
10
6
|
export function safeRedirectTarget(raw, fallback) {
|
|
7
|
+
// A valid target is `/` plus at least one more character, so the two guards
|
|
8
|
+
// below always have a real `raw[1]` to inspect.
|
|
11
9
|
if (typeof raw !== 'string' || raw.length < 2)
|
|
12
10
|
return fallback;
|
|
11
|
+
// Absolute URLs (`https://evil.com`) leave this origin.
|
|
13
12
|
if (raw[0] !== '/')
|
|
14
13
|
return fallback;
|
|
15
|
-
//
|
|
14
|
+
// `//evil.com` is protocol-relative — a browser treats it as cross-origin —
|
|
15
|
+
// and some browsers normalize `/\evil.com` into the same thing.
|
|
16
16
|
if (raw[1] === '/' || raw[1] === '\\')
|
|
17
17
|
return fallback;
|
|
18
18
|
return raw;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../src/http/redirect.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../src/http/redirect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAA8B,EAAE,QAAgB;IAClF,4EAA4E;IAC5E,gDAAgD;IAChD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC/D,wDAAwD;IACxD,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG;QAAE,OAAO,QAAQ,CAAC;IACpC,4EAA4E;IAC5E,gEAAgE;IAChE,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACvD,OAAO,GAAG,CAAC;AACZ,CAAC"}
|
|
@@ -1,46 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deny-by-default route classification.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* loud 401/redirect), not silently public.
|
|
2
|
+
* Deny-by-default route classification. A request is gated unless its path
|
|
3
|
+
* matches one of the allowlists supplied here, so forgetting to list a new
|
|
4
|
+
* route makes it gated — a loud 401/redirect, not silently public.
|
|
6
5
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The pattern is reusable; the values are app policy, supplied by the consuming
|
|
7
|
+
* app. Matching is plain exact/prefix string comparison: cheap enough for the
|
|
8
|
+
* hooks hot path, and no regex to get wrong.
|
|
10
9
|
*/
|
|
11
10
|
export interface RouteClassifierConfig {
|
|
12
|
-
/** Exact-match
|
|
11
|
+
/** Exact-match pages needing no session (e.g. `/`, `/login`). */
|
|
13
12
|
publicPages?: Iterable<string>;
|
|
14
|
-
/**
|
|
15
|
-
* Public path prefixes (e.g. `/auth/` for OAuth start + callback — the
|
|
16
|
-
* user has no session yet by definition).
|
|
17
|
-
*/
|
|
13
|
+
/** Public path prefixes (e.g. `/auth/` — OAuth start and callback precede any session). */
|
|
18
14
|
publicPrefixes?: readonly string[];
|
|
19
15
|
/**
|
|
20
|
-
* Exact-match API endpoints that
|
|
21
|
-
* load-balancer health probe). Anything
|
|
22
|
-
*
|
|
16
|
+
* Exact-match API endpoints that answer without a session (e.g. a
|
|
17
|
+
* load-balancer health probe). Anything listed here is exposed to anonymous
|
|
18
|
+
* callers.
|
|
23
19
|
*/
|
|
24
20
|
publicApis?: Iterable<string>;
|
|
25
21
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* the
|
|
30
|
-
*
|
|
22
|
+
* One prefix whose routes authorize each request themselves (e.g. a blob
|
|
23
|
+
* proxy classifying per asset path). These classify as public so the hook
|
|
24
|
+
* attaches a session best-effort and does not deny up front; the route makes
|
|
25
|
+
* the real decision. Keep it to ONE prefix — every other API route stays
|
|
26
|
+
* deny-by-default.
|
|
31
27
|
*/
|
|
32
28
|
selfGatingPrefix?: string;
|
|
33
|
-
/** Static-asset
|
|
29
|
+
/** Static-asset prefixes the adapter serves directly (e.g. `/_app/`). */
|
|
34
30
|
staticPrefixes?: readonly string[];
|
|
35
31
|
/** Exact static-asset paths (e.g. `/favicon.svg`, `/robots.txt`). */
|
|
36
32
|
staticPaths?: Iterable<string>;
|
|
37
33
|
}
|
|
38
34
|
export interface RouteClassifier {
|
|
39
|
-
/**
|
|
35
|
+
/** Matches `staticPaths`/`staticPrefixes`. These bypass auth gating entirely. */
|
|
40
36
|
isStaticAsset(pathname: string): boolean;
|
|
41
|
-
/**
|
|
37
|
+
/** Matches `selfGatingPrefix`. The hook must not deny these — the route gates itself. */
|
|
42
38
|
isSelfGatingApiRoute(pathname: string): boolean;
|
|
43
|
-
/**
|
|
39
|
+
/** Needs no session: a public page, API, prefix, or a self-gating route. */
|
|
44
40
|
isPublicRoute(pathname: string): boolean;
|
|
45
41
|
}
|
|
46
42
|
export declare function createRouteClassifier(config: RouteClassifierConfig): RouteClassifier;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-classifier.d.ts","sourceRoot":"","sources":["../../src/http/route-classifier.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"route-classifier.d.ts","sourceRoot":"","sources":["../../src/http/route-classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,qBAAqB;IACrC,iEAAiE;IACjE,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,2FAA2F;IAC3F,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,qEAAqE;IACrE,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC/B,iFAAiF;IACjF,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACzC,yFAAyF;IACzF,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD,4EAA4E;IAC5E,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CACzC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CAyBpF"}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deny-by-default route classification.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* loud 401/redirect), not silently public.
|
|
2
|
+
* Deny-by-default route classification. A request is gated unless its path
|
|
3
|
+
* matches one of the allowlists supplied here, so forgetting to list a new
|
|
4
|
+
* route makes it gated — a loud 401/redirect, not silently public.
|
|
6
5
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The pattern is reusable; the values are app policy, supplied by the consuming
|
|
7
|
+
* app. Matching is plain exact/prefix string comparison: cheap enough for the
|
|
8
|
+
* hooks hot path, and no regex to get wrong.
|
|
10
9
|
*/
|
|
11
10
|
export function createRouteClassifier(config) {
|
|
12
11
|
const publicPages = new Set(config.publicPages ?? []);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-classifier.js","sourceRoot":"","sources":["../../src/http/route-classifier.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"route-classifier.js","sourceRoot":"","sources":["../../src/http/route-classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoCH,MAAM,UAAU,qBAAqB,CAAC,MAA6B;IAClE,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;IACnD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACjD,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,IAAI,EAAE,CAAC;IACnD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAEtD,SAAS,aAAa,CAAC,QAAgB;QACtC,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3C,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,SAAS,oBAAoB,CAAC,QAAgB;QAC7C,OAAO,gBAAgB,KAAK,SAAS,IAAI,QAAQ,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAChF,CAAC;IAED,SAAS,aAAa,CAAC,QAAgB;QACtC,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3C,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,oBAAoB,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAChD,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,aAAa,EAAE,CAAC;AAC/D,CAAC"}
|
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
export interface SecurityHeaderOptions {
|
|
2
2
|
/**
|
|
3
|
-
* Set `Strict-Transport-Security
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Set `Strict-Transport-Security`. Production only — pinning
|
|
4
|
+
* http://localhost to HTTPS bricks local development. The caller decides
|
|
5
|
+
* what "production" means (e.g. `NODE_ENV`).
|
|
6
6
|
*/
|
|
7
7
|
hsts: boolean;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Apply baseline browser-hardening headers to a response
|
|
11
|
-
*
|
|
10
|
+
* Apply baseline browser-hardening headers to a response and return it for
|
|
11
|
+
* chaining. Each is a cheap win that needs no UI verification:
|
|
12
12
|
*
|
|
13
13
|
* - X-Content-Type-Options: nosniff — disables MIME sniffing.
|
|
14
|
-
* - Referrer-Policy: strict-origin-when-cross-origin — strips
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - Permissions-Policy:
|
|
18
|
-
*
|
|
19
|
-
* - HSTS when `opts.hsts
|
|
14
|
+
* - Referrer-Policy: strict-origin-when-cross-origin — strips path and query
|
|
15
|
+
* from cross-origin Referer headers. Matches modern browser defaults, but
|
|
16
|
+
* pins the behavior in case a default drifts.
|
|
17
|
+
* - Permissions-Policy: opts out of browser APIs Selva-engine apps don't use,
|
|
18
|
+
* so XSS can't enable them.
|
|
19
|
+
* - HSTS when `opts.hsts`.
|
|
20
20
|
*
|
|
21
21
|
* **Intentionally NOT set here:**
|
|
22
22
|
* - Content-Security-Policy + frame-ancestors. Selva-engine apps are built
|
|
23
|
-
* for iframe embedding, so a strict CSP needs
|
|
24
|
-
*
|
|
23
|
+
* for iframe embedding, so a strict CSP needs validating against real
|
|
24
|
+
* consumer sites before it can ship.
|
|
25
25
|
* - X-Frame-Options. Same iframe-embedding constraint.
|
|
26
26
|
* - Cache-Control. Asset-path layout is app policy; set it in the caller.
|
|
27
|
-
*
|
|
28
|
-
* Returns the same response for chaining.
|
|
29
27
|
*/
|
|
30
28
|
export declare function applySecurityHeaders(response: Response, opts: SecurityHeaderOptions): Response;
|
|
31
29
|
//# sourceMappingURL=security-headers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../../src/http/security-headers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,IAAI,EAAE,OAAO,CAAC;CACd;AAED
|
|
1
|
+
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../../src/http/security-headers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,IAAI,EAAE,OAAO,CAAC;CACd;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,qBAAqB,GAAG,QAAQ,CAW9F"}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Apply baseline browser-hardening headers to a response
|
|
3
|
-
*
|
|
2
|
+
* Apply baseline browser-hardening headers to a response and return it for
|
|
3
|
+
* chaining. Each is a cheap win that needs no UI verification:
|
|
4
4
|
*
|
|
5
5
|
* - X-Content-Type-Options: nosniff — disables MIME sniffing.
|
|
6
|
-
* - Referrer-Policy: strict-origin-when-cross-origin — strips
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - Permissions-Policy:
|
|
10
|
-
*
|
|
11
|
-
* - HSTS when `opts.hsts
|
|
6
|
+
* - Referrer-Policy: strict-origin-when-cross-origin — strips path and query
|
|
7
|
+
* from cross-origin Referer headers. Matches modern browser defaults, but
|
|
8
|
+
* pins the behavior in case a default drifts.
|
|
9
|
+
* - Permissions-Policy: opts out of browser APIs Selva-engine apps don't use,
|
|
10
|
+
* so XSS can't enable them.
|
|
11
|
+
* - HSTS when `opts.hsts`.
|
|
12
12
|
*
|
|
13
13
|
* **Intentionally NOT set here:**
|
|
14
14
|
* - Content-Security-Policy + frame-ancestors. Selva-engine apps are built
|
|
15
|
-
* for iframe embedding, so a strict CSP needs
|
|
16
|
-
*
|
|
15
|
+
* for iframe embedding, so a strict CSP needs validating against real
|
|
16
|
+
* consumer sites before it can ship.
|
|
17
17
|
* - X-Frame-Options. Same iframe-embedding constraint.
|
|
18
18
|
* - Cache-Control. Asset-path layout is app policy; set it in the caller.
|
|
19
|
-
*
|
|
20
|
-
* Returns the same response for chaining.
|
|
21
19
|
*/
|
|
22
20
|
export function applySecurityHeaders(response, opts) {
|
|
23
21
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../../src/http/security-headers.ts"],"names":[],"mappings":"AASA
|
|
1
|
+
{"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../../src/http/security-headers.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAkB,EAAE,IAA2B;IACnF,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;IAC1D,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,iCAAiC,CAAC,CAAC;IAC3E,QAAQ,CAAC,OAAO,CAAC,GAAG,CACnB,oBAAoB,EACpB,+GAA+G,CAC/G,CAAC;IACF,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,qCAAqC,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,QAAQ,CAAC;AACjB,CAAC"}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { ILogger, LogFields, LogLevel } from '@selvajs/platform';
|
|
2
2
|
/**
|
|
3
|
-
* Minimal `ILogger` over `console.*`, used when pino isn't installed.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* honest about not being machine-parseable. `child` fields are merged eagerly
|
|
7
|
-
* so correlation still works without pino.
|
|
3
|
+
* Minimal `ILogger` over `console.*`, used when pino isn't installed. Renders
|
|
4
|
+
* as `LEVEL message {fields}` — greppable, and honest about not being
|
|
5
|
+
* machine-parseable. `child` fields merge eagerly so correlation still works.
|
|
8
6
|
*/
|
|
9
7
|
export declare class ConsoleLogger implements ILogger {
|
|
10
8
|
private readonly bound;
|
|
@@ -19,21 +17,17 @@ export declare class ConsoleLogger implements ILogger {
|
|
|
19
17
|
child(fields: LogFields): ILogger;
|
|
20
18
|
}
|
|
21
19
|
export interface CreateLoggerOptions {
|
|
22
|
-
/** Minimum level written
|
|
20
|
+
/** Minimum level written; records below it are dropped. Defaults to `info`. */
|
|
23
21
|
level?: LogLevel;
|
|
24
22
|
/**
|
|
25
|
-
* Pretty-print via `pino-pretty`
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* Pretty-print via `pino-pretty` instead of JSON — for local development;
|
|
24
|
+
* production wants JSON for the collector. Asking for this without
|
|
25
|
+
* `pino-pretty` installed drops the whole logger to {@link ConsoleLogger}.
|
|
28
26
|
*/
|
|
29
27
|
pretty?: boolean;
|
|
30
28
|
/** Fields stamped on every record (e.g. service name, release). */
|
|
31
29
|
base?: LogFields;
|
|
32
30
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Build the app's root logger. Never throws and never returns `null`: a
|
|
35
|
-
* deployment with no pino installed gets {@link ConsoleLogger}, because losing
|
|
36
|
-
* warnings and errors is a worse outcome than losing JSON formatting.
|
|
37
|
-
*/
|
|
31
|
+
/** Build the app's root logger. Never throws, never returns `null`. */
|
|
38
32
|
export declare function createLogger(opts?: CreateLoggerOptions): Promise<ILogger>;
|
|
39
33
|
//# sourceMappingURL=PinoLogger.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PinoLogger.d.ts","sourceRoot":"","sources":["../../src/logging/PinoLogger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"PinoLogger.d.ts","sourceRoot":"","sources":["../../src/logging/PinoLogger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AA2DtE;;;;GAIG;AACH,qBAAa,aAAc,YAAW,OAAO;IAE3C,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,KAAK,GAAE,SAAc,EACrB,QAAQ,GAAE,QAAiB;IAG7C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAK3B;IAEF,OAAO,CAAC,KAAK;IAYb,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI;IAGhD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI;IAG/C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI;IAG/C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI;IAGhD,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO;CAGjC;AAqDD,MAAM,WAAW,mBAAmB;IACnC,+EAA+E;IAC/E,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,IAAI,CAAC,EAAE,SAAS,CAAC;CACjB;AAED,uEAAuE;AACvE,wBAAsB,YAAY,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC,CAuBnF"}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Field paths scrubbed before a record is written.
|
|
3
|
-
* sites are required not to log secrets in
|
|
4
|
-
* This catches the
|
|
5
|
-
*
|
|
2
|
+
* Field paths scrubbed before a record is written. A backstop for accidents,
|
|
3
|
+
* NOT a licence to log objects: call sites are required not to log secrets in
|
|
4
|
+
* the first place (see `LogFields`). This catches the case where a whole object
|
|
5
|
+
* is spread into fields and happens to carry a credential.
|
|
6
|
+
*
|
|
7
|
+
* It matches by FIELD NAME, and only at the top level or one level deep (the
|
|
8
|
+
* `*.` entries). It will not catch personal data — an email nested in a payload
|
|
9
|
+
* sails straight through, and a log line reaches a collector that erasure can
|
|
10
|
+
* never reach. Note the nested `*.` set covers only five of the eight names.
|
|
6
11
|
*/
|
|
7
12
|
const REDACTED_PATHS = [
|
|
8
13
|
'token',
|
|
@@ -20,11 +25,9 @@ const REDACTED_PATHS = [
|
|
|
20
25
|
'*.password'
|
|
21
26
|
];
|
|
22
27
|
/**
|
|
23
|
-
* Minimal `ILogger` over `console.*`, used when pino isn't installed.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* honest about not being machine-parseable. `child` fields are merged eagerly
|
|
27
|
-
* so correlation still works without pino.
|
|
28
|
+
* Minimal `ILogger` over `console.*`, used when pino isn't installed. Renders
|
|
29
|
+
* as `LEVEL message {fields}` — greppable, and honest about not being
|
|
30
|
+
* machine-parseable. `child` fields merge eagerly so correlation still works.
|
|
28
31
|
*/
|
|
29
32
|
export class ConsoleLogger {
|
|
30
33
|
bound;
|
|
@@ -45,9 +48,8 @@ export class ConsoleLogger {
|
|
|
45
48
|
const merged = { ...this.bound, ...fields };
|
|
46
49
|
const suffix = Object.keys(merged).length > 0 ? ` ${safeStringify(merged)}` : '';
|
|
47
50
|
const line = `${level.toUpperCase()} ${message}${suffix}`;
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// is already spelled out in the line's own prefix.
|
|
51
|
+
// Split stderr/stdout the way an operator expects. `info` carries debug
|
|
52
|
+
// too — both land on stdout, and the line already spells out its level.
|
|
51
53
|
if (level === 'error')
|
|
52
54
|
console.error(line);
|
|
53
55
|
else if (level === 'warn')
|
|
@@ -85,8 +87,8 @@ class PinoLogger {
|
|
|
85
87
|
constructor(pino) {
|
|
86
88
|
this.pino = pino;
|
|
87
89
|
}
|
|
88
|
-
// pino takes (fields, message); ILogger takes (message, fields)
|
|
89
|
-
// deliberate
|
|
90
|
+
// pino takes (fields, message); ILogger takes (message, fields). The flip is
|
|
91
|
+
// deliberate — the message is what a human scans for first.
|
|
90
92
|
debug(message, fields) {
|
|
91
93
|
try {
|
|
92
94
|
this.pino.debug(fields ?? {}, message);
|
|
@@ -128,11 +130,7 @@ class PinoLogger {
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Build the app's root logger. Never throws and never returns `null`: a
|
|
133
|
-
* deployment with no pino installed gets {@link ConsoleLogger}, because losing
|
|
134
|
-
* warnings and errors is a worse outcome than losing JSON formatting.
|
|
135
|
-
*/
|
|
133
|
+
/** Build the app's root logger. Never throws, never returns `null`. */
|
|
136
134
|
export async function createLogger(opts = {}) {
|
|
137
135
|
const level = opts.level ?? 'info';
|
|
138
136
|
try {
|
|
@@ -150,10 +148,9 @@ export async function createLogger(opts = {}) {
|
|
|
150
148
|
return new PinoLogger(pino);
|
|
151
149
|
}
|
|
152
150
|
catch (err) {
|
|
153
|
-
// pino resolved but
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// something they didn't get. Fall back rather than go quiet.
|
|
151
|
+
// pino resolved but wouldn't construct — a real misconfiguration (e.g.
|
|
152
|
+
// `pretty: true` with pino-pretty absent). Worth complaining about, unlike
|
|
153
|
+
// a merely absent package: the operator asked for something they didn't get.
|
|
157
154
|
console.warn(`[Logger] pino was found but could not be initialized; falling back to console logging. ${String(err)}`);
|
|
158
155
|
return new ConsoleLogger(opts.base ?? {}, level);
|
|
159
156
|
}
|
|
@@ -161,16 +158,13 @@ export async function createLogger(opts = {}) {
|
|
|
161
158
|
/**
|
|
162
159
|
* Load pino's factory, or `null` when it isn't installed.
|
|
163
160
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* `@vite-ignore` alone was NOT sufficient — it suppressed the warning while the
|
|
173
|
-
* rewrite happened anyway.
|
|
161
|
+
* Don't inline the specifier. A literal `import('pino')` is statically
|
|
162
|
+
* analyzable, so Vite applied browser-field mapping to this server build and
|
|
163
|
+
* resolved it to pino's `server_false` browser shim — a stub whose factory
|
|
164
|
+
* throws, turning "use pino" into a silent fall back to the console. Computing
|
|
165
|
+
* the specifier defeats that and leaves the import to Node at runtime.
|
|
166
|
+
* `@vite-ignore` alone was NOT enough: it silenced the warning and the rewrite
|
|
167
|
+
* still happened.
|
|
174
168
|
*/
|
|
175
169
|
async function loadPinoFactory() {
|
|
176
170
|
const specifier = 'pino';
|
|
@@ -181,8 +175,8 @@ async function loadPinoFactory() {
|
|
|
181
175
|
}
|
|
182
176
|
catch {
|
|
183
177
|
// Not installed — the expected path on a base install, where the console
|
|
184
|
-
// fallback is
|
|
185
|
-
//
|
|
178
|
+
// fallback is intended. Quiet on purpose: complaining at boot about an
|
|
179
|
+
// optional package nobody asked for is noise.
|
|
186
180
|
return null;
|
|
187
181
|
}
|
|
188
182
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PinoLogger.js","sourceRoot":"","sources":["../../src/logging/PinoLogger.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PinoLogger.js","sourceRoot":"","sources":["../../src/logging/PinoLogger.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,45 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request-id resolution for log correlation.
|
|
2
|
+
* Request-id resolution for log correlation. Every record on a request path
|
|
3
|
+
* carries the same `requestId`, so an operator handed one id can reconstruct
|
|
4
|
+
* that request's whole story.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Inbound ids are TRUSTED when present. Selva runs behind a reverse proxy
|
|
7
|
+
* (Caddy/nginx) that mints a request id for its own access logs; minting a
|
|
8
|
+
* second one here would split one request across two ids and kill cross-tier
|
|
9
|
+
* correlation. The trade — a direct caller picks their own id — is fine, because
|
|
10
|
+
* the id is only ever a log field and an echoed response header, never an
|
|
11
|
+
* authorization or uniqueness input.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* two ids and make cross-tier correlation impossible. The trade is that a direct
|
|
12
|
-
* caller can choose their own id — acceptable, because a request id is a
|
|
13
|
-
* correlation hint, never an authorization or uniqueness input. It is used only
|
|
14
|
-
* as a log field and an echoed response header.
|
|
15
|
-
*
|
|
16
|
-
* That trust is exactly why {@link sanitizeRequestId} exists: an attacker-chosen
|
|
17
|
-
* id is untrusted *content*. Unbounded or newline-carrying values would let a
|
|
18
|
-
* caller forge log lines (log injection) or bloat every record on the request.
|
|
13
|
+
* That trust is why {@link sanitizeRequestId} exists: an attacker-chosen id is
|
|
14
|
+
* untrusted content. Unbounded or newline-carrying values would let a caller
|
|
15
|
+
* forge log lines (log injection) or bloat every record on the request.
|
|
19
16
|
*/
|
|
20
17
|
/** Header carrying a caller/proxy-supplied id. The de-facto standard spelling. */
|
|
21
18
|
export declare const REQUEST_ID_HEADER = "x-request-id";
|
|
22
|
-
/**
|
|
23
|
-
* Reduce an untrusted inbound id to something safe to stamp on every record.
|
|
24
|
-
* Returns `null` when nothing usable survives, so the caller generates instead.
|
|
25
|
-
*/
|
|
19
|
+
/** Returns `null` when nothing usable survives, so the caller generates instead. */
|
|
26
20
|
export declare function sanitizeRequestId(raw: string | null | undefined): string | null;
|
|
27
21
|
/**
|
|
28
|
-
*
|
|
29
|
-
* sanitization, otherwise mint a fresh UUID.
|
|
22
|
+
* Reuse the proxy's id when it survives sanitization, otherwise mint a UUID.
|
|
30
23
|
*
|
|
31
|
-
* @param
|
|
32
|
-
* @param generate id source; injectable so tests get deterministic ids
|
|
24
|
+
* @param generate injectable so tests get deterministic ids
|
|
33
25
|
*/
|
|
34
26
|
export declare function resolveRequestId(headers: Headers, generate?: () => string): string;
|
|
35
27
|
/**
|
|
36
28
|
* Render a thrown value into a log field.
|
|
37
29
|
*
|
|
38
|
-
* Not every rejection is an `Error` — provider adapters
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
30
|
+
* Not every rejection is an `Error` — provider adapters sometimes reject with a
|
|
31
|
+
* plain `{ message, status }`, which would stringify to a useless
|
|
32
|
+
* `[object Object]` and destroy the only record of the failure. Errors keep
|
|
33
|
+
* their stack; anything else is JSON-serialized, with `String()` as the last
|
|
34
|
+
* resort for cycles and exotic values.
|
|
43
35
|
*/
|
|
44
36
|
export declare function renderThrown(error: unknown): string;
|
|
45
37
|
//# sourceMappingURL=requestId.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"requestId.d.ts","sourceRoot":"","sources":["../../src/logging/requestId.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"requestId.d.ts","sourceRoot":"","sources":["../../src/logging/requestId.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAahD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAI/E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,GAAE,MAAM,MAAiB,GAAG,MAAM,CAE5F;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOnD"}
|
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request-id resolution for log correlation.
|
|
2
|
+
* Request-id resolution for log correlation. Every record on a request path
|
|
3
|
+
* carries the same `requestId`, so an operator handed one id can reconstruct
|
|
4
|
+
* that request's whole story.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Inbound ids are TRUSTED when present. Selva runs behind a reverse proxy
|
|
7
|
+
* (Caddy/nginx) that mints a request id for its own access logs; minting a
|
|
8
|
+
* second one here would split one request across two ids and kill cross-tier
|
|
9
|
+
* correlation. The trade — a direct caller picks their own id — is fine, because
|
|
10
|
+
* the id is only ever a log field and an echoed response header, never an
|
|
11
|
+
* authorization or uniqueness input.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* two ids and make cross-tier correlation impossible. The trade is that a direct
|
|
12
|
-
* caller can choose their own id — acceptable, because a request id is a
|
|
13
|
-
* correlation hint, never an authorization or uniqueness input. It is used only
|
|
14
|
-
* as a log field and an echoed response header.
|
|
15
|
-
*
|
|
16
|
-
* That trust is exactly why {@link sanitizeRequestId} exists: an attacker-chosen
|
|
17
|
-
* id is untrusted *content*. Unbounded or newline-carrying values would let a
|
|
18
|
-
* caller forge log lines (log injection) or bloat every record on the request.
|
|
13
|
+
* That trust is why {@link sanitizeRequestId} exists: an attacker-chosen id is
|
|
14
|
+
* untrusted content. Unbounded or newline-carrying values would let a caller
|
|
15
|
+
* forge log lines (log injection) or bloat every record on the request.
|
|
19
16
|
*/
|
|
20
17
|
/** Header carrying a caller/proxy-supplied id. The de-facto standard spelling. */
|
|
21
18
|
export const REQUEST_ID_HEADER = 'x-request-id';
|
|
22
|
-
/**
|
|
19
|
+
/** Long enough for a UUID or a W3C trace-id, short enough to bound a record. */
|
|
23
20
|
const MAX_REQUEST_ID_LENGTH = 128;
|
|
24
21
|
/**
|
|
25
|
-
* Characters allowed through verbatim.
|
|
26
|
-
* trace-ids and nginx's `$request_id` all fit.
|
|
27
|
-
* control chars, quotes, CR/LF
|
|
28
|
-
* legitimate id
|
|
22
|
+
* Characters allowed through verbatim. Strict on purpose: UUIDs, ULIDs, hex
|
|
23
|
+
* trace-ids and nginx's `$request_id` all fit. Everything else — whitespace,
|
|
24
|
+
* control chars, quotes, CR/LF — is dropped rather than escaped, since no
|
|
25
|
+
* legitimate id needs them and dropping can't be un-escaped downstream.
|
|
29
26
|
*/
|
|
30
27
|
const UNSAFE_REQUEST_ID_CHARS = /[^A-Za-z0-9._:-]/g;
|
|
31
|
-
/**
|
|
32
|
-
* Reduce an untrusted inbound id to something safe to stamp on every record.
|
|
33
|
-
* Returns `null` when nothing usable survives, so the caller generates instead.
|
|
34
|
-
*/
|
|
28
|
+
/** Returns `null` when nothing usable survives, so the caller generates instead. */
|
|
35
29
|
export function sanitizeRequestId(raw) {
|
|
36
30
|
if (!raw)
|
|
37
31
|
return null;
|
|
@@ -39,11 +33,9 @@ export function sanitizeRequestId(raw) {
|
|
|
39
33
|
return cleaned.length > 0 ? cleaned : null;
|
|
40
34
|
}
|
|
41
35
|
/**
|
|
42
|
-
*
|
|
43
|
-
* sanitization, otherwise mint a fresh UUID.
|
|
36
|
+
* Reuse the proxy's id when it survives sanitization, otherwise mint a UUID.
|
|
44
37
|
*
|
|
45
|
-
* @param
|
|
46
|
-
* @param generate id source; injectable so tests get deterministic ids
|
|
38
|
+
* @param generate injectable so tests get deterministic ids
|
|
47
39
|
*/
|
|
48
40
|
export function resolveRequestId(headers, generate = randomId) {
|
|
49
41
|
return sanitizeRequestId(headers.get(REQUEST_ID_HEADER)) ?? generate();
|
|
@@ -51,11 +43,11 @@ export function resolveRequestId(headers, generate = randomId) {
|
|
|
51
43
|
/**
|
|
52
44
|
* Render a thrown value into a log field.
|
|
53
45
|
*
|
|
54
|
-
* Not every rejection is an `Error` — provider adapters
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
46
|
+
* Not every rejection is an `Error` — provider adapters sometimes reject with a
|
|
47
|
+
* plain `{ message, status }`, which would stringify to a useless
|
|
48
|
+
* `[object Object]` and destroy the only record of the failure. Errors keep
|
|
49
|
+
* their stack; anything else is JSON-serialized, with `String()` as the last
|
|
50
|
+
* resort for cycles and exotic values.
|
|
59
51
|
*/
|
|
60
52
|
export function renderThrown(error) {
|
|
61
53
|
if (error instanceof Error)
|
|
@@ -68,10 +60,10 @@ export function renderThrown(error) {
|
|
|
68
60
|
}
|
|
69
61
|
}
|
|
70
62
|
/**
|
|
71
|
-
* `crypto.randomUUID`
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* correlate logs, never unguessable.
|
|
63
|
+
* `crypto.randomUUID` exists in every runtime Selva targets (Node 22+, workers,
|
|
64
|
+
* modern browsers); the `Math.random` branch only covers non-secure contexts
|
|
65
|
+
* where it is missing. Not a weakness: these ids need to be collision-free
|
|
66
|
+
* enough to correlate logs, never unguessable.
|
|
75
67
|
*/
|
|
76
68
|
function randomId() {
|
|
77
69
|
const c = globalThis.crypto;
|