@sourceregistry/sveltekit-enhance 1.2.0 → 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 +34 -0
- package/dist/helpers/CSRF.d.ts +14 -0
- package/dist/helpers/CSRF.js +38 -0
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/index.js +2 -0
- package/dist/helpers/logger.d.ts +6 -0
- package/dist/helpers/logger.js +1 -0
- package/dist/helpers/request-monitor.d.ts +2 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,6 +138,39 @@ All helpers are available from `@sourceregistry/sveltekit-enhance` or `@sourcere
|
|
|
138
138
|
|
|
139
139
|
---
|
|
140
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.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { CSRF, CSRFChecker } from '@sourceregistry/sveltekit-enhance';
|
|
147
|
+
|
|
148
|
+
export const handle = enhance.handle(
|
|
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
|
+
|
|
141
174
|
### `Auth`
|
|
142
175
|
|
|
143
176
|
Extracts and validates `Authorization: Bearer <token>` headers.
|
|
@@ -353,6 +386,7 @@ TraceOptions // { logger?: TraceLogger; record?: (entry) => any }
|
|
|
353
386
|
RecordTraceMetricEntry // { method: string; path: string; status: number; durationMs: number }
|
|
354
387
|
RequestTraceLocals // { trace?: { id: string; started_at: bigint } }
|
|
355
388
|
RequestCorrelationLocals // { correlation_id?: string; request_started_at?: number }
|
|
389
|
+
CSRFChecker // { regex(...patterns): checker; list(...paths): checker }
|
|
356
390
|
```
|
|
357
391
|
|
|
358
392
|
---
|
|
@@ -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
|
+
};
|
package/dist/helpers/index.d.ts
CHANGED
package/dist/helpers/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EnhanceFunction } from "../index.js";
|
|
2
|
+
import type { Logger } from "./logger.js";
|
|
2
3
|
export type RequestTraceLocals = {
|
|
3
4
|
trace?: {
|
|
4
5
|
id: string;
|
|
@@ -11,14 +12,8 @@ export type RecordTraceMetricEntry = {
|
|
|
11
12
|
status: number;
|
|
12
13
|
durationMs: number;
|
|
13
14
|
};
|
|
14
|
-
export type TraceLogger = {
|
|
15
|
-
debug: (...args: any[]) => any;
|
|
16
|
-
info: (...args: any[]) => any;
|
|
17
|
-
warn: (...args: any[]) => any;
|
|
18
|
-
error: (...args: any[]) => any;
|
|
19
|
-
};
|
|
20
15
|
export type TraceOptions = {
|
|
21
|
-
logger?:
|
|
16
|
+
logger?: Logger;
|
|
22
17
|
record?: (entry: RecordTraceMetricEntry) => any;
|
|
23
18
|
};
|
|
24
19
|
export declare const RequestMonitor: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sourceregistry/sveltekit-enhance",
|
|
3
|
-
"version": "1.
|
|
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": {
|