@sourceregistry/sveltekit-enhance 1.1.2 → 1.3.0

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 CHANGED
@@ -1,112 +1,396 @@
1
+ <div align="center">
2
+
1
3
  # @sourceregistry/sveltekit-enhance
2
4
 
3
- [![npm version](https://img.shields.io/npm/v/@sourceregistry/sveltekit-enhance?logo=npm)](https://www.npmjs.com/package/@sourceregistry/sveltekit-enhance)
4
- [![License](https://img.shields.io/npm/l/@sourceregistry/sveltekit-enhance)](https://github.com/SourceRegistry/sveltekit-enhance/blob/main/LICENSE)
5
- [![CI](https://github.com/SourceRegistry/sveltekit-enhance/actions/workflows/ci.yml/badge.svg)](https://github.com/SourceRegistry/sveltekit-enhance/actions/workflows/ci.yml)
5
+ **Composable middleware, guards, and form utilities for SvelteKit**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@sourceregistry/sveltekit-enhance?style=flat-square&color=f96743)](https://www.npmjs.com/package/@sourceregistry/sveltekit-enhance)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@sourceregistry/sveltekit-enhance?style=flat-square)](https://www.npmjs.com/package/@sourceregistry/sveltekit-enhance)
9
+ [![license](https://img.shields.io/npm/l/@sourceregistry/sveltekit-enhance?style=flat-square)](./LICENSE)
10
+ [![SvelteKit](https://img.shields.io/badge/SvelteKit-%5E2.58-FF3E00?style=flat-square&logo=svelte&logoColor=white)](https://kit.svelte.dev)
11
+ [![issues](https://img.shields.io/github/issues/SourceRegistry/sveltekit-enhance?style=flat-square)](https://github.com/SourceRegistry/sveltekit-enhance/issues)
12
+
13
+ Wrap actions, loads, methods, and hooks with composable enhancers. Stack auth guards, feature flags, request tracing, and form parsing without touching SvelteKit's internals.
6
14
 
7
- Production-ready utilities for **SvelteKit server flows**.
8
- Use `@sourceregistry/sveltekit-enhance` to build cleaner actions, loads, methods, and hooks with reusable guards for authentication, feature flags, request correlation, and form processing.
15
+ [Docs](https://sourceregistry.github.io/sveltekit-enhance/) · [npm](https://www.npmjs.com/package/@sourceregistry/sveltekit-enhance) · [Issues](https://github.com/SourceRegistry/sveltekit-enhance/issues)
9
16
 
10
- ## Why teams use this
17
+ </div>
11
18
 
12
- - Standardize server-side context logic across routes.
13
- - Reduce repetitive request parsing and validation code.
14
- - Keep middleware-like behavior explicit and composable.
15
- - Improve observability with correlation IDs on every response.
19
+ ---
16
20
 
17
21
  ## Installation
18
22
 
19
- ```bash
23
+ ```sh
20
24
  npm install @sourceregistry/sveltekit-enhance
21
25
  ```
22
26
 
23
- ## Core concepts
27
+ **Peer dependency:** `@sveltejs/kit ^2.58.0`
24
28
 
25
- The package provides an `enhance` wrapper for:
29
+ ---
26
30
 
27
- - `enhance.action(...)`
28
- - `enhance.load(...)`
29
- - `enhance.method(...)`
30
- - `enhance.handle(...)`
31
+ ## Overview
31
32
 
32
- Each wrapper accepts one or more context functions and merges their outputs into a typed `context` object.
33
+ ```ts
34
+ import { enhance, Auth, RequestCorrelation, RequestMonitor, Form } from '@sourceregistry/sveltekit-enhance';
33
35
 
34
- ## Quick start
36
+ // hooks.server.ts
37
+ export const handle = enhance.handle(
38
+ async ({ event, resolve }) => resolve(event),
39
+ RequestCorrelation.attach,
40
+ RequestMonitor.trace({ logger: myLogger, record: metrics.record }),
41
+ );
35
42
 
36
- ```ts
37
- // src/routes/account/+page.server.ts
38
- import {enhance} from '@sourceregistry/sveltekit-enhance';
39
- import {Auth, FeatureFlag, form} from '@sourceregistry/sveltekit-enhance';
43
+ // +server.ts
44
+ export const POST = enhance.method(
45
+ async (event) => new Response(JSON.stringify(event.context)),
46
+ Auth.Bearer,
47
+ FeatureFlag.all('PUBLIC_API_ENABLED'),
48
+ );
40
49
 
50
+ // +page.server.ts
41
51
  export const actions = {
42
- save: enhance.action(
43
- async ({request, context}) => {
44
- const data = await request.formData();
45
- const email = form.string$(data, 'email');
46
-
47
- return {
48
- ok: true,
49
- token: context.token,
50
- email
51
- };
52
- },
52
+ default: enhance.action(
53
+ async (event) => {
54
+ const name = event.context.form.string$('name');
55
+ return success({ name });
56
+ },
57
+ Form.schema(myValidator),
58
+ ),
59
+ };
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Core API
65
+
66
+ Import from `@sourceregistry/sveltekit-enhance`.
67
+
68
+ ### `enhance.handle`
69
+
70
+ Wraps SvelteKit's `handle` hook. Enhancers run left-to-right before the handler; their return values are merged into `context`.
71
+
72
+ ```ts
73
+ import { enhance } from '@sourceregistry/sveltekit-enhance';
74
+
75
+ // src/hooks.server.ts
76
+ export const handle = enhance.handle(
77
+ async ({ event, resolve, context }) => resolve(event),
78
+ enhancerA,
79
+ enhancerB,
80
+ );
81
+ ```
82
+
83
+ ### `enhance.load`
84
+
85
+ Wraps server `load` functions.
86
+
87
+ ```ts
88
+ // +page.server.ts
89
+ export const load = enhance.load(
90
+ async (event) => ({ user: event.context.user }),
53
91
  Auth.Bearer,
54
- FeatureFlag.all('PUBLIC_ACCOUNT_EDIT')
55
- )
92
+ );
93
+ ```
94
+
95
+ ### `enhance.action`
96
+
97
+ Wraps form actions.
98
+
99
+ ```ts
100
+ // +page.server.ts
101
+ export const actions = {
102
+ submit: enhance.action(
103
+ async (event) => success(event.context),
104
+ Auth.Bearer,
105
+ Form.schema(myValidator),
106
+ ),
56
107
  };
57
108
  ```
58
109
 
59
- ## Included helpers
110
+ ### `enhance.method`
60
111
 
61
- - `Auth.Bearer`
62
- Validates `Authorization: Bearer <token>` and returns `{ token }`.
112
+ Wraps `+server.ts` endpoint handlers.
63
113
 
64
- - `FeatureFlag.all(...flags)` / `FeatureFlag.oneOf(...flags)`
65
- Enforces public environment-based feature flags.
114
+ ```ts
115
+ // +server.ts
116
+ export const GET = enhance.method(
117
+ async (event) => new Response(JSON.stringify(event.context)),
118
+ Auth.Bearer,
119
+ );
120
+ ```
121
+
122
+ ### Utilities
123
+
124
+ ```ts
125
+ import { fail, error, success, not_good } from '@sourceregistry/sveltekit-enhance';
126
+
127
+ fail(400, { message: 'bad input' }); // throws ActionFailure — use inside actions
128
+ error(404, { message: 'not found' }); // throws HttpError
129
+ success({ id: 1 }); // typed identity helper
130
+ not_good(input, 403); // delegates to fail or error based on callType
131
+ ```
66
132
 
67
- - `RequestCorrelation.attach`
68
- Reuses incoming `x-correlation-id` / `x-request-id` or generates one, stores it in `locals`, and appends it to response headers.
133
+ ---
69
134
 
70
- - `Devtools.ignore`
71
- Ignores the Chrome DevTools app-specific probe route with a `204` response.
135
+ ## Helpers
72
136
 
73
- - `Form` utilities
74
- Typed helpers for strings, numbers, booleans, dates, files, arrays, JSON, selector helpers, and schema-style validation workflows.
137
+ All helpers are available from `@sourceregistry/sveltekit-enhance` or `@sourceregistry/sveltekit-enhance/helpers`.
75
138
 
76
- ## Example: handle hook with correlation ID
139
+ ---
140
+
141
+ ### `CSRF`
142
+
143
+ Blocks cross-site form submissions on mutating methods (`POST`, `PUT`, `PATCH`, `DELETE`) with form content types. Checks the `Origin` header against the request origin. Absent `Origin` (server-side fetch, curl) is allowed through. Returns `403` — JSON body when `Accept: application/json`, SvelteKit `error()` otherwise.
77
144
 
78
145
  ```ts
79
- // src/hooks.server.ts
80
- import {enhance, RequestCorrelation} from '@sourceregistry/sveltekit-enhance';
146
+ import { CSRF, CSRFChecker } from '@sourceregistry/sveltekit-enhance';
81
147
 
82
148
  export const handle = enhance.handle(
83
- async ({event, resolve}) => resolve(event),
84
- RequestCorrelation.attach
149
+ myHandler,
150
+ CSRF.inspect(
151
+ CSRFChecker.list('/api/webhooks/stripe'), // bypass paths
152
+ myLogger, // optional, defaults to console
153
+ ),
154
+ );
155
+ ```
156
+
157
+ Built-in bypass checkers:
158
+
159
+ | Checker | Description |
160
+ |---------|-------------|
161
+ | `CSRFChecker.list(...paths)` | Exact pathname match |
162
+ | `CSRFChecker.regex(...patterns)` | RegExp match against pathname |
163
+
164
+ Custom checker — any `(input: EnhanceInput) => MaybePromise<boolean>`:
165
+ ```ts
166
+ // true = bypass CSRF check
167
+ CSRF.inspect((input) => input.url.pathname.startsWith('/api/public'))
168
+ ```
169
+
170
+ Returns `{ csrf_valid: true }` on pass. Locals set: none.
171
+
172
+ ---
173
+
174
+ ### `Auth`
175
+
176
+ Extracts and validates `Authorization: Bearer <token>` headers.
177
+
178
+ ```ts
179
+ import { Auth } from '@sourceregistry/sveltekit-enhance';
180
+
181
+ export const GET = enhance.method(
182
+ async (event) => new Response(event.context.token),
183
+ Auth.Bearer,
85
184
  );
86
185
  ```
87
186
 
88
- ## API exports
187
+ Returns `{ token: string }`. Throws `401` if the header is missing or malformed.
188
+
189
+ ---
190
+
191
+ ### `Devtools`
192
+
193
+ Silences Chrome DevTools probe requests (`/.well-known/appspecific/com.chrome.devtools.json`) with a `204 No Content`. Logs in `dev` mode.
89
194
 
90
195
  ```ts
91
- import {
92
- enhance,
93
- action,
94
- load,
95
- method,
96
- handle,
97
- Auth,
98
- FeatureFlag,
99
- RequestCorrelation,
100
- Devtools,
101
- Form
102
- } from '@sourceregistry/sveltekit-enhance';
196
+ import { Devtools } from '@sourceregistry/sveltekit-enhance';
197
+
198
+ export const handle = enhance.handle(myHandler, Devtools.ignore);
103
199
  ```
104
200
 
105
- ## Compatibility
201
+ ---
202
+
203
+ ### `FeatureFlag`
204
+
205
+ Guards routes behind SvelteKit public env vars (`$env/dynamic/public`). Always passes in `dev` mode.
206
+
207
+ ```ts
208
+ import { FeatureFlag } from '@sourceregistry/sveltekit-enhance';
209
+
210
+ // All listed flags must be enabled
211
+ FeatureFlag.all('PUBLIC_FEATURE_A', 'PUBLIC_FEATURE_B')
212
+
213
+ // At least one flag must be enabled
214
+ FeatureFlag.oneOf('PUBLIC_FEATURE_A', 'PUBLIC_FEATURE_B')
215
+ ```
216
+
217
+ Truthy values: `true`, `TRUE`, `on`, `ON`, `1`. Returns `{ flags }` or throws `503 Feature not enabled`.
218
+
219
+ ---
220
+
221
+ ### `RequestCorrelation`
222
+
223
+ Propagates a correlation ID across the request/response cycle.
224
+
225
+ - Reads `x-correlation-id` or `x-request-id` from incoming headers
226
+ - Validates: max 128 chars, pattern `[A-Za-z0-9._:-]+`
227
+ - Generates a UUID v4 if absent
228
+ - Echoes the ID back via `x-correlation-id` response header
229
+
230
+ ```ts
231
+ import { RequestCorrelation } from '@sourceregistry/sveltekit-enhance';
232
+
233
+ export const handle = enhance.handle(myHandler, RequestCorrelation.attach);
234
+ ```
235
+
236
+ | Local | Type | Description |
237
+ |-------|------|-------------|
238
+ | `correlation_id` | `string` | Resolved correlation ID |
239
+ | `request_started_at` | `number` | `Date.now()` at attach time |
240
+
241
+ ---
242
+
243
+ ### `RequestMonitor`
244
+
245
+ Structured HTTP request logging and optional metrics collection. Instruments the full lifecycle: start, completion (log level by status), and unhandled errors — all with elapsed duration.
246
+
247
+ ```ts
248
+ import { RequestMonitor } from '@sourceregistry/sveltekit-enhance';
249
+
250
+ export const handle = enhance.handle(
251
+ myHandler,
252
+ RequestCorrelation.attach,
253
+ RequestMonitor.trace({
254
+ logger: myLogger, // optional, defaults to console
255
+ record: metrics.record, // optional
256
+ }),
257
+ );
258
+ ```
259
+
260
+ #### `TraceOptions`
261
+
262
+ | Option | Type | Default | Description |
263
+ |--------|------|---------|-------------|
264
+ | `logger` | `TraceLogger` | `console` | Must implement `debug`, `info`, `warn`, `error` |
265
+ | `record` | `(entry: RecordTraceMetricEntry) => any` | — | Called after every request |
266
+
267
+ #### Log events
268
+
269
+ | Event | Level | Condition |
270
+ |-------|-------|-----------|
271
+ | `http.request.started` | `debug` | Before resolve |
272
+ | `http.request.completed` | `info` | `status < 400` |
273
+ | `http.request.completed` | `warn` | `status 4xx` |
274
+ | `http.request.completed` | `error` | `status 5xx` |
275
+ | `http.request.failed` | `error` | Unhandled throw |
276
+
277
+ #### Types
278
+
279
+ ```ts
280
+ type RecordTraceMetricEntry = { method: string; path: string; status: number; durationMs: number }
281
+ type TraceLogger = { debug(...args: any[]): any; info(...args: any[]): any; warn(...args: any[]): any; error(...args: any[]): any }
282
+ ```
283
+
284
+ Locals set: `trace: { id: string; started_at: bigint }`.
285
+
286
+ ---
287
+
288
+ ### `Form`
289
+
290
+ Typed, ergonomic FormData extraction. Works standalone or as an enhancer.
291
+
292
+ #### As an enhancer — `Form.schema`
293
+
294
+ Validates and deserializes form data via a `Validator<T>` before the handler runs.
295
+
296
+ ```ts
297
+ import { Form, enhance, success } from '@sourceregistry/sveltekit-enhance';
298
+
299
+ export const actions = {
300
+ default: enhance.action(
301
+ async (event) => success(event.context.form.result),
302
+ Form.schema(myValidator),
303
+ ),
304
+ };
305
+ ```
306
+
307
+ #### Standalone — `Form.handle`
308
+
309
+ ```ts
310
+ import { Form } from '@sourceregistry/sveltekit-enhance';
311
+
312
+ await Form.handle(request, ({ form }) => {
313
+ const name = form.string$('name');
314
+ const age = form.number('age');
315
+ return { name, age };
316
+ });
317
+ ```
318
+
319
+ #### Field extractors
320
+
321
+ Optional variants return `undefined` when the field is absent. Required variants (`$` suffix) throw `fail(400)`.
322
+
323
+ | Method | Returns | Notes |
324
+ |--------|---------|-------|
325
+ | `string(name)` / `string$(name)` | `string \| null \| undefined` | |
326
+ | `pattern$(name, pattern)` | `string` | `RegExp` or pattern string |
327
+ | `number(name)` / `number$(name)` | `number \| undefined` | |
328
+ | `boolean(name)` / `boolean$(name)` | `boolean \| undefined` | Accepts `true/false`, `1/0`, `on/off` |
329
+ | `date(name, parser?)` / `date$(name, parser)` | `Date \| undefined` | Custom parser supported |
330
+ | `json<T>(name, transformer?)` / `json$(name)` | `T \| undefined` | Optional transform fn |
331
+ | `jsond(options)` | `any` | All FormData → nested object via dot-notation keys |
332
+ | `file(name)` / `file$(name)` | `File \| null \| undefined` | |
333
+ | `files(name)` | `File[]` | Non-empty, named files only |
334
+ | `fileRecord(prefix, removePrefix?)` | `Record<string, File[]>` | Groups files by key prefix |
335
+ | `array<T>(name, mapper?)` / `array$(name)` | `T[] \| undefined` | |
336
+ | `enum(name, Enum)` / `enum$(name, Enum)` | `keyof E \| undefined` | |
337
+ | `record(options?)` | `Record<string, any>` | All entries; optional `filter` / `transformer` |
338
+ | `validate(schema, options?)` | `T` | Runs `Validator<T>`, throws `fail(400)` on failure |
339
+
340
+ #### Conditional helpers
341
+
342
+ ```ts
343
+ form.onlyIf(condition, trueVal, falseVal?)
344
+ form.onlyIfPresent(key, (entry) => ..., fallback?)
345
+ form.onlyIfArrayPresent(key, (entries) => ..., fallback)
346
+ form.selector({ fieldName: (entry, key) => ..., $default?, $error? })
347
+ form.selector$({ ... }) // throws fail(400) if no case matches
348
+ form.basedOn(val, processor)
349
+ form.process(name, parser, processor)
350
+ ```
351
+
352
+ #### Standalone functions
353
+
354
+ All `FormContext` methods are also exported as standalone functions taking `FormData` as the first argument, plus:
355
+
356
+ ```ts
357
+ arrayString(formdata, name, delimiter, mapper?)
358
+ hasOneOf(formdata, names)
359
+ reviver(key, value) // JSON.parse reviver — coerces strings to typed primitives
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Type Reference
365
+
366
+ ```ts
367
+ // Core
368
+ EnhanceInput<CallType> // event passed to all enhancers
369
+ EnhanceFunction<CallType> // (event: EnhanceInput) => MaybePromise<object | Response>
370
+ EnhanceHandle // final handle fn — ({ event, resolve, context }) => MaybePromise<Response>
371
+ EnhanceLoad // final load fn
372
+ EnhanceAction // final action fn
373
+ EnhanceMethod // final method fn
374
+ MaybePromise<T> // T | Promise<T>
375
+
376
+ // Form
377
+ Validator<T> // (value: unknown, path?: ValidationPath) => ValidationResult<T>
378
+ ValidationResult<T> // { success: true; data: T } | { success: false; errors: ValidationIssue[] }
379
+ ValidationIssue // { path: string; message: string; code?: string }
380
+ FormContext // fluent API returned by Form.enhance / Form.handle
381
+ InferValidator<V> // infers T from Validator<T>
382
+
383
+ // Helpers
384
+ TraceLogger // { debug, info, warn, error }
385
+ TraceOptions // { logger?: TraceLogger; record?: (entry) => any }
386
+ RecordTraceMetricEntry // { method: string; path: string; status: number; durationMs: number }
387
+ RequestTraceLocals // { trace?: { id: string; started_at: bigint } }
388
+ RequestCorrelationLocals // { correlation_id?: string; request_started_at?: number }
389
+ CSRFChecker // { regex(...patterns): checker; list(...paths): checker }
390
+ ```
106
391
 
107
- - SvelteKit 2+
108
- - Node.js runtime (matching your SvelteKit adapter/runtime support)
392
+ ---
109
393
 
110
394
  ## License
111
395
 
112
- Apache-2.0
396
+ [Apache-2.0](./LICENSE) © [A.P.A. Slaa](https://github.com/SourceRegistry)
@@ -0,0 +1,14 @@
1
+ import { type EnhanceFunction, type EnhanceInput } from "../index.js";
2
+ import type { MaybePromise } from "../index.js";
3
+ import type { Logger } from "./logger.js";
4
+ export declare const CSRF: {
5
+ inspect: (checker: (input: EnhanceInput) => MaybePromise<boolean>, logger?: Logger) => EnhanceFunction<"handle">;
6
+ };
7
+ export declare const CSRFChecker: {
8
+ regex: (...oneOf: RegExp[]) => (input: {
9
+ url: URL;
10
+ }) => boolean;
11
+ list: (...oneOf: string[]) => (input: {
12
+ url: URL;
13
+ }) => boolean;
14
+ };
@@ -0,0 +1,38 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { error } from "../index.js";
3
+ function isContentType(request, ...types) {
4
+ const type = request.headers.get('content-type')?.split(';', 1)[0].trim() ?? '';
5
+ return types.includes(type.toLowerCase());
6
+ }
7
+ function isFormContentType(request) {
8
+ return isContentType(request, 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain');
9
+ }
10
+ const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
11
+ const SAFE_LOG_HEADERS = new Set(['content-type', 'origin', 'referer', 'user-agent', 'accept', 'host']);
12
+ export const CSRF = {
13
+ inspect: (checker, logger = console) => async (input) => {
14
+ const { request, url } = input;
15
+ const origin = request.headers.get('origin');
16
+ const forbidden = isFormContentType(request) &&
17
+ MUTATING_METHODS.has(request.method) &&
18
+ origin !== null &&
19
+ origin !== url.origin &&
20
+ !(await checker(input));
21
+ if (forbidden) {
22
+ const message = `Cross-site ${request.method} form submissions are forbidden`;
23
+ logger.warn('CSRF Violation detected', {
24
+ url: url.toString(),
25
+ headers: Object.fromEntries([...request.headers.entries()].filter(([k]) => SAFE_LOG_HEADERS.has(k.toLowerCase())))
26
+ });
27
+ if (request.headers.get('accept') === 'application/json') {
28
+ return json({ message }, { status: 403 });
29
+ }
30
+ return error(403, { message });
31
+ }
32
+ return { csrf_valid: true };
33
+ }
34
+ };
35
+ export const CSRFChecker = {
36
+ regex: (...oneOf) => (input) => oneOf.some((exp) => exp[Symbol.match](input.url.pathname)),
37
+ list: (...oneOf) => (input) => oneOf.includes(input.url.pathname)
38
+ };
@@ -3,3 +3,6 @@ export * from './devtools.js';
3
3
  export * from './featureflag.js';
4
4
  export * from './form.ts';
5
5
  export * from './request-correlation.ts';
6
+ export * from './request-monitor.js';
7
+ export * from './CSRF.js';
8
+ export * from './logger.js';
@@ -3,3 +3,6 @@ export * from './devtools.js';
3
3
  export * from './featureflag.js';
4
4
  export * from "./form.js";
5
5
  export * from "./request-correlation.js";
6
+ export * from './request-monitor.js';
7
+ export * from './CSRF.js';
8
+ export * from './logger.js';
@@ -0,0 +1,6 @@
1
+ export type Logger = {
2
+ debug: (...args: any[]) => any;
3
+ info: (...args: any[]) => any;
4
+ warn: (...args: any[]) => any;
5
+ error: (...args: any[]) => any;
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { EnhanceFunction } from "../index.js";
2
+ import type { Logger } from "./logger.js";
3
+ export type RequestTraceLocals = {
4
+ trace?: {
5
+ id: string;
6
+ started_at: bigint;
7
+ };
8
+ };
9
+ export type RecordTraceMetricEntry = {
10
+ method: string;
11
+ path: string;
12
+ status: number;
13
+ durationMs: number;
14
+ };
15
+ export type TraceOptions = {
16
+ logger?: Logger;
17
+ record?: (entry: RecordTraceMetricEntry) => any;
18
+ };
19
+ export declare const RequestMonitor: {
20
+ trace: (options?: TraceOptions) => EnhanceFunction<"handle">;
21
+ };
@@ -0,0 +1,72 @@
1
+ const durationMs = (startedAt) => Number(process.hrtime.bigint() - startedAt) / 1_000_000;
2
+ const resolveRoute = (event) => event.route.id ?? event.url.pathname;
3
+ export const RequestMonitor = {
4
+ trace: (options = {}) => {
5
+ const { logger = console, record, } = options;
6
+ return async (event) => {
7
+ const locals = event.locals;
8
+ const requestId = locals.requestId ?? "unknown";
9
+ const route = resolveRoute(event);
10
+ locals.trace = {
11
+ id: requestId,
12
+ started_at: process.hrtime.bigint()
13
+ };
14
+ logger.debug("http.request.started", {
15
+ request_id: requestId,
16
+ method: event.request.method,
17
+ route,
18
+ client_ip: event?.getClientAddress?.()
19
+ });
20
+ try {
21
+ const response = await event.resolve(event.event);
22
+ if (!response)
23
+ return response;
24
+ const elapsedMs = Number(durationMs(locals.trace.started_at).toFixed(2));
25
+ record?.({
26
+ method: event.request.method,
27
+ path: route,
28
+ status: response.status,
29
+ durationMs: elapsedMs,
30
+ });
31
+ const context = {
32
+ request_id: requestId,
33
+ method: event.request.method,
34
+ route,
35
+ status: response.status,
36
+ duration_ms: elapsedMs,
37
+ client_ip: event?.getClientAddress?.()
38
+ };
39
+ if (response.status >= 500) {
40
+ logger.error("http.request.completed", context);
41
+ return response;
42
+ }
43
+ if (response.status >= 400) {
44
+ logger.warn("http.request.completed", context);
45
+ return response;
46
+ }
47
+ logger.info("http.request.completed", context);
48
+ return response;
49
+ }
50
+ catch (error) {
51
+ const elapsedMs = Number(durationMs(locals.trace.started_at).toFixed(2));
52
+ record?.({
53
+ method: event.request.method,
54
+ path: route,
55
+ status: 500,
56
+ durationMs: elapsedMs,
57
+ });
58
+ logger.error("http.request.failed", {
59
+ request_id: requestId,
60
+ method: event.request.method,
61
+ route,
62
+ duration_ms: elapsedMs,
63
+ client_ip: event?.getClientAddress?.(),
64
+ error: error instanceof Error
65
+ ? { name: error.name, message: error.message }
66
+ : { value: String(error) }
67
+ });
68
+ throw error;
69
+ }
70
+ };
71
+ }
72
+ };
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export type EnhanceInput<CallType extends EnhanceCallType = EnhanceCallType, Par
18
18
  request: Request;
19
19
  callType: CallType;
20
20
  fetch: typeof fetch;
21
+ getClientAddress?: () => string;
21
22
  get errorHandlers(): EnhanceErrorHandler[];
22
23
  } & (CallType extends 'handle' ? {
23
24
  get responseHandlers(): EnhanceResponseHandler[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sourceregistry/sveltekit-enhance",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Composable enhance and form utilities for SvelteKit actions, loads, methods, and hooks.",
5
5
  "author": "A.P.A. Slaa (a.p.a.slaa@projectsource.nl)",
6
6
  "scripts": {
@@ -31,6 +31,10 @@
31
31
  "types": "./dist/index.d.ts",
32
32
  "svelte": "./dist/index.js",
33
33
  "default": "./dist/index.js"
34
+ },
35
+ "./helpers": {
36
+ "types": "./dist/helpers/index.d.ts",
37
+ "default": "./dist/helpers/index.js"
34
38
  }
35
39
  },
36
40
  "repository": {