@adonis-agora/telescope 0.4.0 → 0.5.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/CHANGELOG.md +10 -0
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +6 -0
- package/dist/configure.js.map +1 -1
- package/dist/providers/telescope_ai_provider.d.ts +9 -1
- package/dist/providers/telescope_ai_provider.d.ts.map +1 -1
- package/dist/providers/telescope_ai_provider.js +22 -2
- package/dist/providers/telescope_ai_provider.js.map +1 -1
- package/dist/providers/telescope_alerts_provider.d.ts +1 -0
- package/dist/providers/telescope_alerts_provider.d.ts.map +1 -1
- package/dist/providers/telescope_alerts_provider.js +37 -1
- package/dist/providers/telescope_alerts_provider.js.map +1 -1
- package/dist/providers/telescope_mcp_provider.d.ts +26 -0
- package/dist/providers/telescope_mcp_provider.d.ts.map +1 -0
- package/dist/providers/telescope_mcp_provider.js +114 -0
- package/dist/providers/telescope_mcp_provider.js.map +1 -0
- package/dist/providers/telescope_provider.d.ts +8 -0
- package/dist/providers/telescope_provider.d.ts.map +1 -1
- package/dist/providers/telescope_provider.js +56 -1
- package/dist/providers/telescope_provider.js.map +1 -1
- package/dist/providers/telescope_ui_provider.d.ts +2 -0
- package/dist/providers/telescope_ui_provider.d.ts.map +1 -1
- package/dist/providers/telescope_ui_provider.js +106 -22
- package/dist/providers/telescope_ui_provider.js.map +1 -1
- package/dist/providers/telescope_watchers_provider.d.ts +13 -2
- package/dist/providers/telescope_watchers_provider.d.ts.map +1 -1
- package/dist/providers/telescope_watchers_provider.js +57 -7
- package/dist/providers/telescope_watchers_provider.js.map +1 -1
- package/dist/src/ai/define_config.d.ts +39 -7
- package/dist/src/ai/define_config.d.ts.map +1 -1
- package/dist/src/ai/define_config.js +14 -4
- package/dist/src/ai/define_config.js.map +1 -1
- package/dist/src/ai/diagnosis_coordinator.d.ts +106 -0
- package/dist/src/ai/diagnosis_coordinator.d.ts.map +1 -0
- package/dist/src/ai/diagnosis_coordinator.js +143 -0
- package/dist/src/ai/diagnosis_coordinator.js.map +1 -0
- package/dist/src/ai/factory.d.ts +8 -5
- package/dist/src/ai/factory.d.ts.map +1 -1
- package/dist/src/ai/factory.js +20 -8
- package/dist/src/ai/factory.js.map +1 -1
- package/dist/src/ai/index.d.ts +3 -1
- package/dist/src/ai/index.d.ts.map +1 -1
- package/dist/src/ai/index.js +3 -1
- package/dist/src/ai/index.js.map +1 -1
- package/dist/src/alerts/alert_rule.d.ts +79 -11
- package/dist/src/alerts/alert_rule.d.ts.map +1 -1
- package/dist/src/alerts/alerter.d.ts +11 -1
- package/dist/src/alerts/alerter.d.ts.map +1 -1
- package/dist/src/alerts/alerter.js +21 -1
- package/dist/src/alerts/alerter.js.map +1 -1
- package/dist/src/alerts/alerter_service.d.ts +103 -0
- package/dist/src/alerts/alerter_service.d.ts.map +1 -0
- package/dist/src/alerts/alerter_service.js +215 -0
- package/dist/src/alerts/alerter_service.js.map +1 -0
- package/dist/src/alerts/index.d.ts +4 -2
- package/dist/src/alerts/index.d.ts.map +1 -1
- package/dist/src/alerts/index.js +4 -2
- package/dist/src/alerts/index.js.map +1 -1
- package/dist/src/alerts/slack_format.d.ts.map +1 -1
- package/dist/src/alerts/slack_format.js +35 -2
- package/dist/src/alerts/slack_format.js.map +1 -1
- package/dist/src/client_errors/config.d.ts +66 -0
- package/dist/src/client_errors/config.d.ts.map +1 -0
- package/dist/src/client_errors/config.js +17 -0
- package/dist/src/client_errors/config.js.map +1 -0
- package/dist/src/client_errors/index.d.ts +5 -0
- package/dist/src/client_errors/index.d.ts.map +1 -0
- package/dist/src/client_errors/index.js +5 -0
- package/dist/src/client_errors/index.js.map +1 -0
- package/dist/src/client_errors/ingestor.d.ts +87 -0
- package/dist/src/client_errors/ingestor.d.ts.map +1 -0
- package/dist/src/client_errors/ingestor.js +135 -0
- package/dist/src/client_errors/ingestor.js.map +1 -0
- package/dist/src/client_errors/rate_limiter.d.ts +53 -0
- package/dist/src/client_errors/rate_limiter.d.ts.map +1 -0
- package/dist/src/client_errors/rate_limiter.js +83 -0
- package/dist/src/client_errors/rate_limiter.js.map +1 -0
- package/dist/src/client_errors/validation.d.ts +62 -0
- package/dist/src/client_errors/validation.d.ts.map +1 -0
- package/dist/src/client_errors/validation.js +109 -0
- package/dist/src/client_errors/validation.js.map +1 -0
- package/dist/src/define_config.d.ts +120 -0
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +41 -0
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/entry.d.ts +20 -0
- package/dist/src/entry.d.ts.map +1 -1
- package/dist/src/entry.js +20 -0
- package/dist/src/entry.js.map +1 -1
- package/dist/src/index.d.ts +16 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +15 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/define_config.d.ts +57 -0
- package/dist/src/mcp/define_config.d.ts.map +1 -0
- package/dist/src/mcp/define_config.js +35 -0
- package/dist/src/mcp/define_config.js.map +1 -0
- package/dist/src/mcp/index.d.ts +7 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/mcp/index.js +7 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/server.d.ts +89 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +279 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/metrics/pulse.d.ts +158 -0
- package/dist/src/metrics/pulse.d.ts.map +1 -0
- package/dist/src/metrics/pulse.js +414 -0
- package/dist/src/metrics/pulse.js.map +1 -0
- package/dist/src/overload_guard.d.ts +77 -0
- package/dist/src/overload_guard.d.ts.map +1 -0
- package/dist/src/overload_guard.js +134 -0
- package/dist/src/overload_guard.js.map +1 -0
- package/dist/src/pruner.d.ts +93 -0
- package/dist/src/pruner.d.ts.map +1 -0
- package/dist/src/pruner.js +120 -0
- package/dist/src/pruner.js.map +1 -0
- package/dist/src/registry.d.ts +26 -0
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +19 -0
- package/dist/src/registry.js.map +1 -1
- package/dist/src/service.d.ts +16 -1
- package/dist/src/service.d.ts.map +1 -1
- package/dist/src/service.js +19 -1
- package/dist/src/service.js.map +1 -1
- package/dist/src/stores/factory.d.ts.map +1 -1
- package/dist/src/stores/factory.js +8 -2
- package/dist/src/stores/factory.js.map +1 -1
- package/dist/src/telescope_middleware.d.ts.map +1 -1
- package/dist/src/telescope_middleware.js +3 -1
- package/dist/src/telescope_middleware.js.map +1 -1
- package/dist/src/ui/api.d.ts +9 -1
- package/dist/src/ui/api.d.ts.map +1 -1
- package/dist/src/ui/api.js +23 -1
- package/dist/src/ui/api.js.map +1 -1
- package/dist/src/ui/auth.d.ts +112 -0
- package/dist/src/ui/auth.d.ts.map +1 -0
- package/dist/src/ui/auth.js +130 -0
- package/dist/src/ui/auth.js.map +1 -0
- package/dist/src/ui/dashboard_auth.d.ts +34 -0
- package/dist/src/ui/dashboard_auth.d.ts.map +1 -0
- package/dist/src/ui/dashboard_auth.js +57 -0
- package/dist/src/ui/dashboard_auth.js.map +1 -0
- package/dist/src/ui/define_config.d.ts +15 -0
- package/dist/src/ui/define_config.d.ts.map +1 -1
- package/dist/src/ui/define_config.js +4 -0
- package/dist/src/ui/define_config.js.map +1 -1
- package/dist/src/ui/index.d.ts +7 -1
- package/dist/src/ui/index.d.ts.map +1 -1
- package/dist/src/ui/index.js +6 -1
- package/dist/src/ui/index.js.map +1 -1
- package/dist/src/ui/login_page.d.ts +23 -0
- package/dist/src/ui/login_page.d.ts.map +1 -0
- package/dist/src/ui/login_page.js +170 -0
- package/dist/src/ui/login_page.js.map +1 -0
- package/dist/src/ui/session_cookie.d.ts +42 -0
- package/dist/src/ui/session_cookie.d.ts.map +1 -0
- package/dist/src/ui/session_cookie.js +97 -0
- package/dist/src/ui/session_cookie.js.map +1 -0
- package/dist/src/watchers/define_config.d.ts +115 -12
- package/dist/src/watchers/define_config.d.ts.map +1 -1
- package/dist/src/watchers/define_config.js +48 -11
- package/dist/src/watchers/define_config.js.map +1 -1
- package/dist/src/watchers/http_client_watcher.d.ts +91 -31
- package/dist/src/watchers/http_client_watcher.d.ts.map +1 -1
- package/dist/src/watchers/http_client_watcher.js +161 -53
- package/dist/src/watchers/http_client_watcher.js.map +1 -1
- package/dist/src/watchers/index.d.ts +9 -5
- package/dist/src/watchers/index.d.ts.map +1 -1
- package/dist/src/watchers/index.js +6 -2
- package/dist/src/watchers/index.js.map +1 -1
- package/dist/src/watchers/lucid_query_watcher.d.ts +20 -2
- package/dist/src/watchers/lucid_query_watcher.d.ts.map +1 -1
- package/dist/src/watchers/lucid_query_watcher.js +44 -10
- package/dist/src/watchers/lucid_query_watcher.js.map +1 -1
- package/dist/src/watchers/profiling_watcher.d.ts +144 -0
- package/dist/src/watchers/profiling_watcher.d.ts.map +1 -0
- package/dist/src/watchers/profiling_watcher.js +231 -0
- package/dist/src/watchers/profiling_watcher.js.map +1 -0
- package/dist/src/watchers/query_family_hash.d.ts +7 -2
- package/dist/src/watchers/query_family_hash.d.ts.map +1 -1
- package/dist/src/watchers/query_family_hash.js +8 -3
- package/dist/src/watchers/query_family_hash.js.map +1 -1
- package/dist/src/watchers/record.d.ts.map +1 -1
- package/dist/src/watchers/record.js +4 -0
- package/dist/src/watchers/record.js.map +1 -1
- package/dist/src/watchers/schedule_watcher.d.ts +143 -0
- package/dist/src/watchers/schedule_watcher.d.ts.map +1 -0
- package/dist/src/watchers/schedule_watcher.js +195 -0
- package/dist/src/watchers/schedule_watcher.js.map +1 -0
- package/dist/stubs/config/telescope.stub +0 -65
- package/dist/stubs/config/telescope_ai.stub +0 -36
- package/dist/stubs/config/telescope_alerts.stub +0 -47
- package/dist/stubs/config/telescope_mcp.stub +0 -0
- package/dist/stubs/config/telescope_ui.stub +0 -53
- package/dist/stubs/config/telescope_watchers.stub +0 -41
- package/dist/stubs/database/migrations/create_telescope_entries_table.stub +2 -2
- package/package.json +12 -2
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DEFAULT_CLIENT_ERRORS_PATH, DEFAULT_MAX_BODY_BYTES, DEFAULT_RATE_LIMIT_PER_MINUTE, resolveClientErrors, } from './config.js';
|
|
2
|
+
export { ClientErrorIngestor, storeRecorder, } from './ingestor.js';
|
|
3
|
+
export { ClientErrorRateLimiter, DEFAULT_MAX_TRACKED_IPS } from './rate_limiter.js';
|
|
4
|
+
export { userIdentityTag, validateClientErrorBody, } from './validation.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client_errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,0BAA0B,EAC1B,sBAAsB,EACtB,6BAA6B,EAC7B,mBAAmB,GAEpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,EAKnB,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAGL,eAAe,EACf,uBAAuB,GACxB,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type RecordInput } from '../entry.js';
|
|
2
|
+
import type { TelescopeStore } from '../store.js';
|
|
3
|
+
import type { ResolvedClientErrorsConfig } from './config.js';
|
|
4
|
+
import { type ClientExceptionContent } from './validation.js';
|
|
5
|
+
/**
|
|
6
|
+
* The minimal HTTP surface the ingestor reads/writes, kept deliberately
|
|
7
|
+
* framework-light so the handler is unit-testable with a plain object (no
|
|
8
|
+
* running AdonisJS server). An Adonis `HttpContext` satisfies this structurally
|
|
9
|
+
* — `request.body()`, `request.header()`, `request.ip()`, and a `response` that
|
|
10
|
+
* can set a status and send a body — so the provider passes the real `ctx`
|
|
11
|
+
* straight through.
|
|
12
|
+
*/
|
|
13
|
+
export interface ClientErrorRequest {
|
|
14
|
+
/** The parsed request body (Adonis `request.body()`). */
|
|
15
|
+
body(): unknown;
|
|
16
|
+
/** A single request header by (case-insensitive) name, or `undefined`. */
|
|
17
|
+
header(name: string): string | undefined;
|
|
18
|
+
/** The resolving client IP (Adonis `request.ip()`, which honours trust-proxy). */
|
|
19
|
+
ip(): string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** The slice of an outbound response the ingestor writes. */
|
|
22
|
+
export interface ClientErrorResponse {
|
|
23
|
+
/** Set the HTTP status code; returns `this` for chaining (Adonis-compatible). */
|
|
24
|
+
status(code: number): ClientErrorResponse;
|
|
25
|
+
/** Send a body (object → JSON, string → as-is). Terminal. */
|
|
26
|
+
send(body: unknown): unknown;
|
|
27
|
+
}
|
|
28
|
+
/** A framework-light HTTP context: just the request + response slices above. */
|
|
29
|
+
export interface ClientErrorHttpContext {
|
|
30
|
+
request: ClientErrorRequest;
|
|
31
|
+
response: ClientErrorResponse;
|
|
32
|
+
}
|
|
33
|
+
/** Injectable seams so the ingestor is testable without the global runtime. */
|
|
34
|
+
export interface ClientErrorIngestorDeps {
|
|
35
|
+
config: ResolvedClientErrorsConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Fire-and-forget recorder. Wired by the provider to
|
|
38
|
+
* `store.record(input)` with rejections swallowed. Injected (rather than the
|
|
39
|
+
* store) so a test can assert exactly what would be recorded.
|
|
40
|
+
*/
|
|
41
|
+
record: (input: RecordInput<ClientExceptionContent>) => void;
|
|
42
|
+
/** Overload-shed seam. Defaults to the global runtime `paused` flag. */
|
|
43
|
+
isPaused?: () => boolean;
|
|
44
|
+
/** Wall-clock seam (ms) for the rate limiter. Defaults to `Date.now`. */
|
|
45
|
+
now?: () => number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Records the active telescope store's `record` as a fire-and-forget sink,
|
|
49
|
+
* swallowing every failure — mirroring the watcher `safeRecord` contract so an
|
|
50
|
+
* ingest request can never be broken (or blocked) by a failing store.
|
|
51
|
+
*/
|
|
52
|
+
export declare function storeRecorder(store: TelescopeStore): (input: RecordInput<ClientExceptionContent>) => void;
|
|
53
|
+
/**
|
|
54
|
+
* The framework-agnostic client-error ingestion handler. Mirrors the NestJS
|
|
55
|
+
* `ClientErrorController.ingest` pipeline in idiomatic AdonisJS form — the
|
|
56
|
+
* provider registers `POST <path>` and delegates to {@link handle}.
|
|
57
|
+
*
|
|
58
|
+
* Pipeline (each stage short-circuits): authorize hook → body byte cap (413) →
|
|
59
|
+
* per-IP rate limit (429) → structural validation (400) → overload-shed check →
|
|
60
|
+
* record as a `client_exception` entry and answer `204 No Content`.
|
|
61
|
+
*/
|
|
62
|
+
export declare class ClientErrorIngestor {
|
|
63
|
+
private readonly config;
|
|
64
|
+
private readonly record;
|
|
65
|
+
private readonly isPaused;
|
|
66
|
+
private readonly limiter;
|
|
67
|
+
/** One warn for an authorize-hook throw, so a flaky hook can't spam logs. */
|
|
68
|
+
private warnedAuthorize;
|
|
69
|
+
constructor(deps: ClientErrorIngestorDeps);
|
|
70
|
+
handle(ctx: ClientErrorHttpContext): Promise<unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* Run the host's authorize hook defensively: a throw is a DENIAL (fail closed)
|
|
73
|
+
* and warn-logged once, so a buggy hook never 500s the public endpoint nor
|
|
74
|
+
* floods the logs.
|
|
75
|
+
*/
|
|
76
|
+
private runAuthorize;
|
|
77
|
+
/**
|
|
78
|
+
* Best-effort serialized byte size of the parsed body. The framework already
|
|
79
|
+
* parsed JSON by the time we get here, so we re-serialize to measure bytes
|
|
80
|
+
* (UTF-8) — a tight enough proxy for the wire size to reject oversized
|
|
81
|
+
* payloads. A non-serializable body counts as 0 (it'll fail validation anyway).
|
|
82
|
+
*/
|
|
83
|
+
private bodyByteSize;
|
|
84
|
+
/** The reporting client's IP via Adonis `request.ip()`, or `'unknown'`. */
|
|
85
|
+
private clientIp;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=ingestor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingestor.d.ts","sourceRoot":"","sources":["../../../src/client_errors/ingestor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,IAAI,IAAI,OAAO,CAAC;IAChB,0EAA0E;IAC1E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACzC,kFAAkF;IAClF,EAAE,IAAI,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED,6DAA6D;AAC7D,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAAC;IAC1C,6DAA6D;IAC7D,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;CAC9B;AAED,gFAAgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,+EAA+E;AAC/E,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,0BAA0B,CAAC;IACnC;;;;OAIG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,sBAAsB,CAAC,KAAK,IAAI,CAAC;IAC7D,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC;IACzB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,cAAc,GACpB,CAAC,KAAK,EAAE,WAAW,CAAC,sBAAsB,CAAC,KAAK,IAAI,CAQtD;AAOD;;;;;;;;GAQG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuD;IAC9E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,6EAA6E;IAC7E,OAAO,CAAC,eAAe,CAAS;gBAEpB,IAAI,EAAE,uBAAuB;IAUnC,MAAM,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC;IAwD3D;;;;OAIG;YACW,YAAY;IAmB1B;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAQpB,2EAA2E;IAC3E,OAAO,CAAC,QAAQ;CAIjB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { EntryType } from '../entry.js';
|
|
2
|
+
import { exceptionFamilyHash } from '../exception_family_hash.js';
|
|
3
|
+
import { getTelescopeRuntime } from '../registry.js';
|
|
4
|
+
import { ClientErrorRateLimiter } from './rate_limiter.js';
|
|
5
|
+
import { userIdentityTag, validateClientErrorBody, } from './validation.js';
|
|
6
|
+
/**
|
|
7
|
+
* Records the active telescope store's `record` as a fire-and-forget sink,
|
|
8
|
+
* swallowing every failure — mirroring the watcher `safeRecord` contract so an
|
|
9
|
+
* ingest request can never be broken (or blocked) by a failing store.
|
|
10
|
+
*/
|
|
11
|
+
export function storeRecorder(store) {
|
|
12
|
+
return (input) => {
|
|
13
|
+
try {
|
|
14
|
+
void Promise.resolve(store.record(input)).catch((err) => reportError(err));
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
reportError(err);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function reportError(err) {
|
|
22
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23
|
+
console.error(`telescope client-errors: failed to record entry: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The framework-agnostic client-error ingestion handler. Mirrors the NestJS
|
|
27
|
+
* `ClientErrorController.ingest` pipeline in idiomatic AdonisJS form — the
|
|
28
|
+
* provider registers `POST <path>` and delegates to {@link handle}.
|
|
29
|
+
*
|
|
30
|
+
* Pipeline (each stage short-circuits): authorize hook → body byte cap (413) →
|
|
31
|
+
* per-IP rate limit (429) → structural validation (400) → overload-shed check →
|
|
32
|
+
* record as a `client_exception` entry and answer `204 No Content`.
|
|
33
|
+
*/
|
|
34
|
+
export class ClientErrorIngestor {
|
|
35
|
+
config;
|
|
36
|
+
record;
|
|
37
|
+
isPaused;
|
|
38
|
+
limiter;
|
|
39
|
+
/** One warn for an authorize-hook throw, so a flaky hook can't spam logs. */
|
|
40
|
+
warnedAuthorize = false;
|
|
41
|
+
constructor(deps) {
|
|
42
|
+
this.config = deps.config;
|
|
43
|
+
this.record = deps.record;
|
|
44
|
+
this.isPaused = deps.isPaused ?? (() => getTelescopeRuntime().paused);
|
|
45
|
+
this.limiter = new ClientErrorRateLimiter({
|
|
46
|
+
perMinute: this.config.rateLimitPerMinute,
|
|
47
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async handle(ctx) {
|
|
51
|
+
const config = this.config;
|
|
52
|
+
// 1) authorize hook runs FIRST (before any work): a session/header gate.
|
|
53
|
+
if (config.authorize !== undefined) {
|
|
54
|
+
if (!(await this.runAuthorize(config.authorize, ctx))) {
|
|
55
|
+
return ctx.response.status(403).send({ error: 'Forbidden' });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// 2) body byte cap, BEFORE validation, so a huge payload is cheap to reject.
|
|
59
|
+
const body = ctx.request.body();
|
|
60
|
+
if (this.bodyByteSize(body) > config.maxBodyBytes) {
|
|
61
|
+
return ctx.response.status(413).send({ error: 'Payload too large' });
|
|
62
|
+
}
|
|
63
|
+
// 3) per-IP rate limit (per-process, best-effort abuse dampening).
|
|
64
|
+
const ip = this.clientIp(ctx);
|
|
65
|
+
if (!this.limiter.tryConsume(ip)) {
|
|
66
|
+
return ctx.response.status(429).send({ error: 'Rate limit exceeded' });
|
|
67
|
+
}
|
|
68
|
+
// 4) structural validation — never trust the body; no echo on failure.
|
|
69
|
+
const validation = validateClientErrorBody(body);
|
|
70
|
+
if (!validation.ok) {
|
|
71
|
+
return ctx.response.status(400).send({ error: validation.reason });
|
|
72
|
+
}
|
|
73
|
+
// 5) overload shed: while the guard has paused ingestion, DROP the entry so
|
|
74
|
+
// a lagging event loop is never made worse — but still answer 204 so the
|
|
75
|
+
// browser treats it as accepted and does not retry-storm.
|
|
76
|
+
if (this.isPaused()) {
|
|
77
|
+
return ctx.response.status(204).send(null);
|
|
78
|
+
}
|
|
79
|
+
// 6) record through the normal pipeline: family-hash from name+message+top
|
|
80
|
+
// frame (mirrors server exceptions), and the composing tags.
|
|
81
|
+
const content = validation.value;
|
|
82
|
+
const tags = ['failed', 'client'];
|
|
83
|
+
const userTag = userIdentityTag(content.user);
|
|
84
|
+
if (userTag !== null)
|
|
85
|
+
tags.push(userTag);
|
|
86
|
+
this.record({
|
|
87
|
+
type: EntryType.ClientException,
|
|
88
|
+
familyHash: exceptionFamilyHash({
|
|
89
|
+
name: content.name ?? '',
|
|
90
|
+
message: content.message,
|
|
91
|
+
stack: content.stack,
|
|
92
|
+
}),
|
|
93
|
+
tags,
|
|
94
|
+
content: { ...content, clientIp: ip === 'unknown' ? null : ip },
|
|
95
|
+
});
|
|
96
|
+
return ctx.response.status(204).send(null);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run the host's authorize hook defensively: a throw is a DENIAL (fail closed)
|
|
100
|
+
* and warn-logged once, so a buggy hook never 500s the public endpoint nor
|
|
101
|
+
* floods the logs.
|
|
102
|
+
*/
|
|
103
|
+
async runAuthorize(authorize, ctx) {
|
|
104
|
+
try {
|
|
105
|
+
return (await authorize(ctx)) === true;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (!this.warnedAuthorize) {
|
|
109
|
+
this.warnedAuthorize = true;
|
|
110
|
+
console.warn(`telescope client-errors authorize hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Best-effort serialized byte size of the parsed body. The framework already
|
|
117
|
+
* parsed JSON by the time we get here, so we re-serialize to measure bytes
|
|
118
|
+
* (UTF-8) — a tight enough proxy for the wire size to reject oversized
|
|
119
|
+
* payloads. A non-serializable body counts as 0 (it'll fail validation anyway).
|
|
120
|
+
*/
|
|
121
|
+
bodyByteSize(body) {
|
|
122
|
+
try {
|
|
123
|
+
return Buffer.byteLength(JSON.stringify(body) ?? '', 'utf8');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** The reporting client's IP via Adonis `request.ip()`, or `'unknown'`. */
|
|
130
|
+
clientIp(ctx) {
|
|
131
|
+
const ip = ctx.request.ip();
|
|
132
|
+
return typeof ip === 'string' && ip.length > 0 ? ip : 'unknown';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=ingestor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingestor.js","sourceRoot":"","sources":["../../../src/client_errors/ingestor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default ceiling on the number of distinct IPs tracked at once. The limiter is
|
|
3
|
+
* the only state the public client-error endpoint keeps, so it MUST stay bounded
|
|
4
|
+
* — an attacker rotating source IPs (or sitting behind a large CGNAT/proxy pool)
|
|
5
|
+
* could otherwise grow the map without limit. At the cap we evict the OLDEST
|
|
6
|
+
* inserted IP, the one least likely to still be mid-burst, which is the right
|
|
7
|
+
* bias for a short-window rate limiter.
|
|
8
|
+
*/
|
|
9
|
+
declare const DEFAULT_MAX_TRACKED_IPS = 10000;
|
|
10
|
+
/**
|
|
11
|
+
* A bounded, in-memory, per-IP token-bucket rate limiter for the PUBLIC
|
|
12
|
+
* client-error endpoint. Per-process by design (no cross-process coordination):
|
|
13
|
+
* in a multi-instance deployment the same client may get up to `perMinute`
|
|
14
|
+
* requests PER INSTANCE. The intent is cheap abuse-dampening on a public
|
|
15
|
+
* surface, not a hard global quota (a shared limiter would need a cross-process
|
|
16
|
+
* store, out of scope for this in-process slice).
|
|
17
|
+
*
|
|
18
|
+
* Each IP gets a bucket holding at most `perMinute` tokens that refills linearly
|
|
19
|
+
* at `perMinute / 60_000` tokens per ms. A request consumes one token; an empty
|
|
20
|
+
* bucket means "over the limit". The bucket map is capped (insertion-order
|
|
21
|
+
* eviction of the oldest IP) so memory can't grow without bound.
|
|
22
|
+
*
|
|
23
|
+
* Ported from the NestJS `nestjs-telescope` core client-error rate limiter.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ClientErrorRateLimiter {
|
|
26
|
+
private readonly buckets;
|
|
27
|
+
private readonly perMinute;
|
|
28
|
+
private readonly maxTrackedIps;
|
|
29
|
+
private readonly refillPerMs;
|
|
30
|
+
private readonly now;
|
|
31
|
+
constructor(options: {
|
|
32
|
+
perMinute: number;
|
|
33
|
+
maxTrackedIps?: number;
|
|
34
|
+
/** Wall-clock seam (ms). Defaults to `Date.now`. */
|
|
35
|
+
now?: () => number;
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Try to spend one token for `ip`. Returns `true` when the request is allowed
|
|
39
|
+
* (a token was available and consumed), `false` when the bucket is empty (over
|
|
40
|
+
* the limit). Refills the bucket continuously based on elapsed time so the
|
|
41
|
+
* effective rate is exactly `perMinute` without a coarse fixed window.
|
|
42
|
+
*/
|
|
43
|
+
tryConsume(ip: string): boolean;
|
|
44
|
+
/** Number of tracked IPs (test/observability seam). */
|
|
45
|
+
get size(): number;
|
|
46
|
+
/**
|
|
47
|
+
* Insert a fresh bucket, evicting the oldest tracked IP first when at the cap.
|
|
48
|
+
* `Map` iterates in insertion order, so the first key is the oldest inserted.
|
|
49
|
+
*/
|
|
50
|
+
private insert;
|
|
51
|
+
}
|
|
52
|
+
export { DEFAULT_MAX_TRACKED_IPS };
|
|
53
|
+
//# sourceMappingURL=rate_limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate_limiter.d.ts","sourceRoot":"","sources":["../../../src/client_errors/rate_limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,QAAA,MAAM,uBAAuB,QAAS,CAAC;AASvC;;;;;;;;;;;;;;GAcG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,EAAE;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,oDAAoD;QACpD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;KACpB;IAUD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAmB/B,uDAAuD;IACvD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;OAGG;IACH,OAAO,CAAC,MAAM;CAQf;AAED,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default ceiling on the number of distinct IPs tracked at once. The limiter is
|
|
3
|
+
* the only state the public client-error endpoint keeps, so it MUST stay bounded
|
|
4
|
+
* — an attacker rotating source IPs (or sitting behind a large CGNAT/proxy pool)
|
|
5
|
+
* could otherwise grow the map without limit. At the cap we evict the OLDEST
|
|
6
|
+
* inserted IP, the one least likely to still be mid-burst, which is the right
|
|
7
|
+
* bias for a short-window rate limiter.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_MAX_TRACKED_IPS = 10_000;
|
|
10
|
+
/**
|
|
11
|
+
* A bounded, in-memory, per-IP token-bucket rate limiter for the PUBLIC
|
|
12
|
+
* client-error endpoint. Per-process by design (no cross-process coordination):
|
|
13
|
+
* in a multi-instance deployment the same client may get up to `perMinute`
|
|
14
|
+
* requests PER INSTANCE. The intent is cheap abuse-dampening on a public
|
|
15
|
+
* surface, not a hard global quota (a shared limiter would need a cross-process
|
|
16
|
+
* store, out of scope for this in-process slice).
|
|
17
|
+
*
|
|
18
|
+
* Each IP gets a bucket holding at most `perMinute` tokens that refills linearly
|
|
19
|
+
* at `perMinute / 60_000` tokens per ms. A request consumes one token; an empty
|
|
20
|
+
* bucket means "over the limit". The bucket map is capped (insertion-order
|
|
21
|
+
* eviction of the oldest IP) so memory can't grow without bound.
|
|
22
|
+
*
|
|
23
|
+
* Ported from the NestJS `nestjs-telescope` core client-error rate limiter.
|
|
24
|
+
*/
|
|
25
|
+
export class ClientErrorRateLimiter {
|
|
26
|
+
buckets = new Map();
|
|
27
|
+
perMinute;
|
|
28
|
+
maxTrackedIps;
|
|
29
|
+
refillPerMs;
|
|
30
|
+
now;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
// Guard against a misconfigured zero/negative rate turning into a divide
|
|
33
|
+
// that never refills; treat anything <= 0 as "allow 1/min" so the endpoint
|
|
34
|
+
// still functions rather than locking out every caller forever.
|
|
35
|
+
this.perMinute = options.perMinute > 0 ? options.perMinute : 1;
|
|
36
|
+
this.maxTrackedIps = options.maxTrackedIps ?? DEFAULT_MAX_TRACKED_IPS;
|
|
37
|
+
this.refillPerMs = this.perMinute / 60_000;
|
|
38
|
+
this.now = options.now ?? Date.now;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Try to spend one token for `ip`. Returns `true` when the request is allowed
|
|
42
|
+
* (a token was available and consumed), `false` when the bucket is empty (over
|
|
43
|
+
* the limit). Refills the bucket continuously based on elapsed time so the
|
|
44
|
+
* effective rate is exactly `perMinute` without a coarse fixed window.
|
|
45
|
+
*/
|
|
46
|
+
tryConsume(ip) {
|
|
47
|
+
const nowMs = this.now();
|
|
48
|
+
const existing = this.buckets.get(ip);
|
|
49
|
+
if (existing === undefined) {
|
|
50
|
+
// First request from this IP: start full, immediately spend one token.
|
|
51
|
+
this.insert(ip, { tokens: this.perMinute - 1, lastRefillMs: nowMs });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const elapsedMs = nowMs - existing.lastRefillMs;
|
|
55
|
+
const refilled = Math.min(this.perMinute, existing.tokens + elapsedMs * this.refillPerMs);
|
|
56
|
+
existing.lastRefillMs = nowMs;
|
|
57
|
+
if (refilled < 1) {
|
|
58
|
+
existing.tokens = refilled;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
existing.tokens = refilled - 1;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
/** Number of tracked IPs (test/observability seam). */
|
|
65
|
+
get size() {
|
|
66
|
+
return this.buckets.size;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Insert a fresh bucket, evicting the oldest tracked IP first when at the cap.
|
|
70
|
+
* `Map` iterates in insertion order, so the first key is the oldest inserted.
|
|
71
|
+
*/
|
|
72
|
+
insert(ip, bucket) {
|
|
73
|
+
while (this.buckets.size >= this.maxTrackedIps) {
|
|
74
|
+
const oldest = this.buckets.keys().next().value;
|
|
75
|
+
if (oldest === undefined)
|
|
76
|
+
break;
|
|
77
|
+
this.buckets.delete(oldest);
|
|
78
|
+
}
|
|
79
|
+
this.buckets.set(ip, bucket);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export { DEFAULT_MAX_TRACKED_IPS };
|
|
83
|
+
//# sourceMappingURL=rate_limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate_limiter.js","sourceRoot":"","sources":["../../../src/client_errors/rate_limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,uBAAuB,GAAG,MAAM,CAAC;AASvC;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,sBAAsB;IAChB,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IACpC,SAAS,CAAS;IAClB,aAAa,CAAS;IACtB,WAAW,CAAS;IACpB,GAAG,CAAe;IAEnC,YAAY,OAKX;QACC,yEAAyE;QACzE,2EAA2E;QAC3E,gEAAgE;QAChE,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,uBAAuB,CAAC;QACtE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;QAC3C,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACrC,CAAC;IAED;;;;;OAKG;IACH,UAAU,CAAC,EAAU;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,uEAAuE;YACvE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,GAAG,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC;YACrE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;QAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;QAC1F,QAAQ,CAAC,YAAY,GAAG,KAAK,CAAC;QAC9B,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACjB,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC;YAC3B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,QAAQ,CAAC,MAAM,GAAG,QAAQ,GAAG,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,uDAAuD;IACvD,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACK,MAAM,CAAC,EAAU,EAAE,MAAc;QACvC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAChD,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM;YAChC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC;CACF;AAED,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A browser-reported error ingested via the public client-error endpoint (see
|
|
3
|
+
* {@link EntryType.ClientException}). Everything beyond `message` is optional
|
|
4
|
+
* because the browser is UNTRUSTED — {@link validateClientErrorBody} validates
|
|
5
|
+
* the structure and length-caps the strings before recording, and `extra` (an
|
|
6
|
+
* arbitrary, host-defined bag of debugging context) is bounded by the normal
|
|
7
|
+
* redaction budget at record time exactly like every other content payload.
|
|
8
|
+
*
|
|
9
|
+
* Field intent mirrors the NestJS `nestjs-telescope` core:
|
|
10
|
+
* - `message` — the error message (required; the one field we insist on).
|
|
11
|
+
* - `name` — the error class/name (e.g. `TypeError`); feeds the family hash.
|
|
12
|
+
* - `stack` — the JS stack string; its TOP frame also feeds the family hash.
|
|
13
|
+
* - `componentStack` — React's component stack (from an error boundary).
|
|
14
|
+
* - `url` — the page URL where the error happened.
|
|
15
|
+
* - `userAgent` — the reporting browser's UA string.
|
|
16
|
+
* - `user` — host-supplied user identity (pivoted into a `user:<id>` tag).
|
|
17
|
+
* - `release` — an app version / build id, so errors group by deploy.
|
|
18
|
+
* - `extra` — free-form debugging context; redacted/bounded like any content.
|
|
19
|
+
* - `clientIp` — filled in BY THE SERVER from `request.ip()`; never from the body.
|
|
20
|
+
*/
|
|
21
|
+
export interface ClientExceptionContent {
|
|
22
|
+
message: string;
|
|
23
|
+
name: string | null;
|
|
24
|
+
stack: string | null;
|
|
25
|
+
componentStack: string | null;
|
|
26
|
+
url: string | null;
|
|
27
|
+
userAgent: string | null;
|
|
28
|
+
user: unknown;
|
|
29
|
+
release: string | null;
|
|
30
|
+
extra: Record<string, unknown> | null;
|
|
31
|
+
clientIp: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The successful parse result. We DELIBERATELY do not echo the raw payload back
|
|
35
|
+
* to the client on failure (see {@link validateClientErrorBody}) — the caller
|
|
36
|
+
* gets a generic 400 with a fixed reason, never a reflection of what it sent.
|
|
37
|
+
*/
|
|
38
|
+
export type ClientErrorValidation = {
|
|
39
|
+
ok: true;
|
|
40
|
+
value: Omit<ClientExceptionContent, 'clientIp'>;
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
reason: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Structurally validate an UNTRUSTED client-error body WITHOUT adding a schema
|
|
47
|
+
* dependency. Rules: `message` is a required non-empty string; every other field
|
|
48
|
+
* is optional with a type check and a length cap; `user` is passed through as-is
|
|
49
|
+
* (redacted at record time) and `extra` must be a plain object if present (its
|
|
50
|
+
* contents are bounded by redaction, not here). On any violation returns a
|
|
51
|
+
* generic reason and NO echo of the payload, so the endpoint never reflects
|
|
52
|
+
* attacker input.
|
|
53
|
+
*/
|
|
54
|
+
export declare function validateClientErrorBody(body: unknown): ClientErrorValidation;
|
|
55
|
+
/**
|
|
56
|
+
* The `user:<id>` tag for a raw user value, or `null` when no usable identity is
|
|
57
|
+
* present. Prefers `id`, then `_id`, then `email` — so an error reported from a
|
|
58
|
+
* browser tags identically to a server request. Cheap property reads; never throws.
|
|
59
|
+
* Ported from the `nestjs-telescope` tagger's `userIdentityTag`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function userIdentityTag(user: unknown): string | null;
|
|
62
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/client_errors/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAgBD;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,IAAI,CAAC,sBAAsB,EAAE,UAAU,CAAC,CAAA;CAAE,GAC7D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAqBlC;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,qBAAqB,CA+C5E;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK5D"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Length caps for the validated string fields. The browser is UNTRUSTED, so we
|
|
3
|
+
* bound every string before recording — a stack can be large but not unbounded,
|
|
4
|
+
* and short fields (message/url/userAgent) get a tight cap. Arrays/objects under
|
|
5
|
+
* `extra` are NOT capped here; they pass through the redaction budget
|
|
6
|
+
* (depth/string/array/node bounds) at record time exactly like all other content.
|
|
7
|
+
*/
|
|
8
|
+
const STACK_MAX = 16 * 1024;
|
|
9
|
+
const COMPONENT_STACK_MAX = 16 * 1024;
|
|
10
|
+
const MESSAGE_MAX = 2 * 1024;
|
|
11
|
+
const SHORT_FIELD_MAX = 2 * 1024;
|
|
12
|
+
const NAME_MAX = 256;
|
|
13
|
+
const RELEASE_MAX = 256;
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read an OPTIONAL string field: missing/`null`/`undefined` yields `null`; a
|
|
19
|
+
* present non-string is a hard validation failure (we don't silently coerce
|
|
20
|
+
* untrusted input); a present string is length-capped.
|
|
21
|
+
*/
|
|
22
|
+
function optionalString(value, field, max) {
|
|
23
|
+
if (value === undefined || value === null)
|
|
24
|
+
return { ok: true, value: null };
|
|
25
|
+
if (typeof value !== 'string')
|
|
26
|
+
return { ok: false, reason: `\`${field}\` must be a string` };
|
|
27
|
+
return { ok: true, value: value.length > max ? value.slice(0, max) : value };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Structurally validate an UNTRUSTED client-error body WITHOUT adding a schema
|
|
31
|
+
* dependency. Rules: `message` is a required non-empty string; every other field
|
|
32
|
+
* is optional with a type check and a length cap; `user` is passed through as-is
|
|
33
|
+
* (redacted at record time) and `extra` must be a plain object if present (its
|
|
34
|
+
* contents are bounded by redaction, not here). On any violation returns a
|
|
35
|
+
* generic reason and NO echo of the payload, so the endpoint never reflects
|
|
36
|
+
* attacker input.
|
|
37
|
+
*/
|
|
38
|
+
export function validateClientErrorBody(body) {
|
|
39
|
+
if (!isRecord(body)) {
|
|
40
|
+
return { ok: false, reason: 'Body must be a JSON object' };
|
|
41
|
+
}
|
|
42
|
+
const message = body.message;
|
|
43
|
+
if (typeof message !== 'string' || message.length === 0) {
|
|
44
|
+
return { ok: false, reason: '`message` is required and must be a non-empty string' };
|
|
45
|
+
}
|
|
46
|
+
const cappedMessage = message.length > MESSAGE_MAX ? message.slice(0, MESSAGE_MAX) : message;
|
|
47
|
+
const name = optionalString(body.name, 'name', NAME_MAX);
|
|
48
|
+
if (!name.ok)
|
|
49
|
+
return name;
|
|
50
|
+
const stack = optionalString(body.stack, 'stack', STACK_MAX);
|
|
51
|
+
if (!stack.ok)
|
|
52
|
+
return stack;
|
|
53
|
+
const componentStack = optionalString(body.componentStack, 'componentStack', COMPONENT_STACK_MAX);
|
|
54
|
+
if (!componentStack.ok)
|
|
55
|
+
return componentStack;
|
|
56
|
+
const url = optionalString(body.url, 'url', SHORT_FIELD_MAX);
|
|
57
|
+
if (!url.ok)
|
|
58
|
+
return url;
|
|
59
|
+
const userAgent = optionalString(body.userAgent, 'userAgent', SHORT_FIELD_MAX);
|
|
60
|
+
if (!userAgent.ok)
|
|
61
|
+
return userAgent;
|
|
62
|
+
const release = optionalString(body.release, 'release', RELEASE_MAX);
|
|
63
|
+
if (!release.ok)
|
|
64
|
+
return release;
|
|
65
|
+
let extra = null;
|
|
66
|
+
if (body.extra !== undefined && body.extra !== null) {
|
|
67
|
+
if (!isRecord(body.extra)) {
|
|
68
|
+
return { ok: false, reason: '`extra` must be an object' };
|
|
69
|
+
}
|
|
70
|
+
extra = body.extra;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
value: {
|
|
75
|
+
message: cappedMessage,
|
|
76
|
+
name: name.value,
|
|
77
|
+
stack: stack.value,
|
|
78
|
+
componentStack: componentStack.value,
|
|
79
|
+
url: url.value,
|
|
80
|
+
userAgent: userAgent.value,
|
|
81
|
+
// `user` is intentionally untyped passthrough: redacted at record time and
|
|
82
|
+
// pivoted into a `user:<id>` tag via userIdentityTag.
|
|
83
|
+
user: 'user' in body ? body.user : null,
|
|
84
|
+
release: release.value,
|
|
85
|
+
extra,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The `user:<id>` tag for a raw user value, or `null` when no usable identity is
|
|
91
|
+
* present. Prefers `id`, then `_id`, then `email` — so an error reported from a
|
|
92
|
+
* browser tags identically to a server request. Cheap property reads; never throws.
|
|
93
|
+
* Ported from the `nestjs-telescope` tagger's `userIdentityTag`.
|
|
94
|
+
*/
|
|
95
|
+
export function userIdentityTag(user) {
|
|
96
|
+
if (typeof user !== 'object' || user === null)
|
|
97
|
+
return null;
|
|
98
|
+
const record = user;
|
|
99
|
+
const id = identityToken(record.id) ?? identityToken(record._id) ?? identityToken(record.email);
|
|
100
|
+
return id === null ? null : `user:${id}`;
|
|
101
|
+
}
|
|
102
|
+
function identityToken(value) {
|
|
103
|
+
if (typeof value === 'string')
|
|
104
|
+
return value.length > 0 ? value : null;
|
|
105
|
+
if (typeof value === 'number')
|
|
106
|
+
return Number.isFinite(value) ? String(value) : null;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../../src/client_errors/validation.ts"],"names":[],"mappings":"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"}
|