@holo-js/security 0.1.3 → 0.1.5
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/dist/{chunk-3J5QRTPZ.mjs → chunk-EWQKJSFA.mjs} +8 -11
- package/dist/chunk-FUOZWKHK.mjs +79 -0
- package/dist/chunk-Q3A7RJ67.mjs +1171 -0
- package/dist/client.d.ts +6 -11
- package/dist/client.mjs +10 -37
- package/dist/contracts.d.ts +17 -10
- package/dist/contracts.mjs +1 -1
- package/dist/drivers/redis-adapter.d.ts +2 -0
- package/dist/drivers/redis-adapter.mjs +32 -3
- package/dist/index.d.ts +62 -10
- package/dist/index.mjs +44 -866
- package/dist/next/server.d.ts +16 -0
- package/dist/next/server.mjs +84 -0
- package/dist/nuxt/server.d.ts +11 -0
- package/dist/nuxt/server.mjs +109 -0
- package/dist/sveltekit/server.d.ts +37 -0
- package/dist/sveltekit/server.mjs +68 -0
- package/package.json +31 -6
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type NextCsrfRequest = Request & {
|
|
2
|
+
readonly nextUrl?: URL;
|
|
3
|
+
readonly cookies?: {
|
|
4
|
+
get(name: string): string | {
|
|
5
|
+
readonly value?: string;
|
|
6
|
+
} | undefined;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
type NextCsrfMiddleware = (request: NextCsrfRequest) => Response | undefined | Promise<Response | undefined>;
|
|
10
|
+
declare function issueCsrfCookie(request: NextCsrfRequest): Promise<Response | undefined>;
|
|
11
|
+
declare function csrfProtection(): NextCsrfMiddleware;
|
|
12
|
+
declare const nextSecurityInternals: {
|
|
13
|
+
issueCsrfCookie: typeof issueCsrfCookie;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export { type NextCsrfMiddleware, csrfProtection, nextSecurityInternals };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
csrf,
|
|
3
|
+
csrfInternals,
|
|
4
|
+
getSecurityRuntime,
|
|
5
|
+
isSecureRequest,
|
|
6
|
+
protect
|
|
7
|
+
} from "../chunk-Q3A7RJ67.mjs";
|
|
8
|
+
import {
|
|
9
|
+
SECURITY_CLIENT_CONFIG_COOKIE,
|
|
10
|
+
createSecurityClientConfig,
|
|
11
|
+
serializeSecurityClientConfig
|
|
12
|
+
} from "../chunk-FUOZWKHK.mjs";
|
|
13
|
+
import {
|
|
14
|
+
SecurityCsrfError
|
|
15
|
+
} from "../chunk-EWQKJSFA.mjs";
|
|
16
|
+
|
|
17
|
+
// src/next/server.ts
|
|
18
|
+
function isSafeMethod(method) {
|
|
19
|
+
const normalized = method.trim().toUpperCase();
|
|
20
|
+
return normalized === "GET" || normalized === "HEAD";
|
|
21
|
+
}
|
|
22
|
+
function createCsrfErrorResponse(error) {
|
|
23
|
+
return new Response(error.message, {
|
|
24
|
+
status: error.status,
|
|
25
|
+
headers: {
|
|
26
|
+
"content-type": "text/plain; charset=utf-8"
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function getRequestCookie(request, name) {
|
|
31
|
+
const cookie = request.cookies?.get(name);
|
|
32
|
+
if (typeof cookie === "string") {
|
|
33
|
+
return cookie;
|
|
34
|
+
}
|
|
35
|
+
return typeof cookie?.value === "string" ? cookie.value : void 0;
|
|
36
|
+
}
|
|
37
|
+
async function issueCsrfCookie(request) {
|
|
38
|
+
const { config } = getSecurityRuntime();
|
|
39
|
+
if (!config.csrf.enabled || !isSafeMethod(request.method)) {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
const existingCsrfToken = getRequestCookie(request, config.csrf.cookie);
|
|
43
|
+
const shouldIssueCsrfToken = !existingCsrfToken || !csrfInternals.isValidSignedCsrfToken(existingCsrfToken);
|
|
44
|
+
const clientConfig = serializeSecurityClientConfig(createSecurityClientConfig(config));
|
|
45
|
+
const shouldIssueClientConfig = getRequestCookie(request, SECURITY_CLIENT_CONFIG_COOKIE) !== clientConfig;
|
|
46
|
+
if (!shouldIssueCsrfToken && !shouldIssueClientConfig) {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
const { NextResponse } = await import("next/server");
|
|
50
|
+
const response = NextResponse.next();
|
|
51
|
+
const cookieOptions = {
|
|
52
|
+
httpOnly: false,
|
|
53
|
+
path: "/",
|
|
54
|
+
sameSite: "lax",
|
|
55
|
+
secure: isSecureRequest(request)
|
|
56
|
+
};
|
|
57
|
+
if (shouldIssueCsrfToken) {
|
|
58
|
+
response.cookies.set(config.csrf.cookie, await csrf.token(request), cookieOptions);
|
|
59
|
+
}
|
|
60
|
+
if (shouldIssueClientConfig) {
|
|
61
|
+
response.cookies.set(SECURITY_CLIENT_CONFIG_COOKIE, clientConfig, cookieOptions);
|
|
62
|
+
}
|
|
63
|
+
return response;
|
|
64
|
+
}
|
|
65
|
+
function csrfProtection() {
|
|
66
|
+
return async (request) => {
|
|
67
|
+
try {
|
|
68
|
+
await protect(request);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof SecurityCsrfError) {
|
|
71
|
+
return createCsrfErrorResponse(error);
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return await issueCsrfCookie(request);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var nextSecurityInternals = {
|
|
79
|
+
issueCsrfCookie
|
|
80
|
+
};
|
|
81
|
+
export {
|
|
82
|
+
csrfProtection,
|
|
83
|
+
nextSecurityInternals
|
|
84
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineEventHandler, H3Event } from 'h3';
|
|
2
|
+
|
|
3
|
+
declare function createRequest(event: H3Event): Promise<Request>;
|
|
4
|
+
declare function issueCsrfCookie(event: H3Event, request: Request): Promise<void>;
|
|
5
|
+
declare function csrfProtection(): ReturnType<typeof defineEventHandler>;
|
|
6
|
+
declare const nuxtSecurityInternals: {
|
|
7
|
+
createRequest: typeof createRequest;
|
|
8
|
+
issueCsrfCookie: typeof issueCsrfCookie;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export { csrfProtection, nuxtSecurityInternals };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
csrf,
|
|
3
|
+
csrfInternals,
|
|
4
|
+
getSecurityRuntime,
|
|
5
|
+
isSecureRequest,
|
|
6
|
+
protect
|
|
7
|
+
} from "../chunk-Q3A7RJ67.mjs";
|
|
8
|
+
import {
|
|
9
|
+
SECURITY_CLIENT_CONFIG_COOKIE,
|
|
10
|
+
createSecurityClientConfig,
|
|
11
|
+
serializeSecurityClientConfig
|
|
12
|
+
} from "../chunk-FUOZWKHK.mjs";
|
|
13
|
+
import {
|
|
14
|
+
SecurityCsrfError
|
|
15
|
+
} from "../chunk-EWQKJSFA.mjs";
|
|
16
|
+
|
|
17
|
+
// src/nuxt/server.ts
|
|
18
|
+
import {
|
|
19
|
+
createError,
|
|
20
|
+
defineEventHandler,
|
|
21
|
+
getCookie,
|
|
22
|
+
getMethod,
|
|
23
|
+
getRequestHeaders,
|
|
24
|
+
getRequestURL,
|
|
25
|
+
readRawBody,
|
|
26
|
+
setCookie
|
|
27
|
+
} from "h3";
|
|
28
|
+
function isSafeMethod(method) {
|
|
29
|
+
const normalized = method.trim().toUpperCase();
|
|
30
|
+
return normalized === "GET" || normalized === "HEAD";
|
|
31
|
+
}
|
|
32
|
+
function createHeaders(event) {
|
|
33
|
+
const headers = new Headers();
|
|
34
|
+
for (const [name, value] of Object.entries(getRequestHeaders(event))) {
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
headers.append(name, value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return headers;
|
|
40
|
+
}
|
|
41
|
+
function createRequestBody(body) {
|
|
42
|
+
if (!body) {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
const copy = new Uint8Array(body.byteLength);
|
|
46
|
+
copy.set(body);
|
|
47
|
+
return copy.buffer;
|
|
48
|
+
}
|
|
49
|
+
async function createRequest(event) {
|
|
50
|
+
const method = getMethod(event);
|
|
51
|
+
const headers = createHeaders(event);
|
|
52
|
+
const body = isSafeMethod(method) ? void 0 : await readRawBody(event, false);
|
|
53
|
+
return new Request(getRequestURL(event), {
|
|
54
|
+
method,
|
|
55
|
+
headers,
|
|
56
|
+
body: createRequestBody(body)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function issueCsrfCookie(event, request) {
|
|
60
|
+
const { config } = getSecurityRuntime();
|
|
61
|
+
if (!config.csrf.enabled || !isSafeMethod(request.method)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const existingCsrfToken = getCookie(event, config.csrf.cookie);
|
|
65
|
+
const shouldIssueCsrfToken = !existingCsrfToken || !csrfInternals.isValidSignedCsrfToken(existingCsrfToken);
|
|
66
|
+
const clientConfig = serializeSecurityClientConfig(createSecurityClientConfig(config));
|
|
67
|
+
const shouldIssueClientConfig = getCookie(event, SECURITY_CLIENT_CONFIG_COOKIE) !== clientConfig;
|
|
68
|
+
if (!shouldIssueCsrfToken && !shouldIssueClientConfig) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const cookieOptions = {
|
|
72
|
+
httpOnly: false,
|
|
73
|
+
path: "/",
|
|
74
|
+
sameSite: "lax",
|
|
75
|
+
secure: isSecureRequest(request)
|
|
76
|
+
};
|
|
77
|
+
if (shouldIssueCsrfToken) {
|
|
78
|
+
setCookie(event, config.csrf.cookie, await csrf.token(request), cookieOptions);
|
|
79
|
+
}
|
|
80
|
+
if (shouldIssueClientConfig) {
|
|
81
|
+
setCookie(event, SECURITY_CLIENT_CONFIG_COOKIE, clientConfig, cookieOptions);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function csrfProtection() {
|
|
85
|
+
return defineEventHandler(async (event) => {
|
|
86
|
+
const request = await createRequest(event);
|
|
87
|
+
try {
|
|
88
|
+
await protect(request);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error instanceof SecurityCsrfError) {
|
|
91
|
+
throw createError({
|
|
92
|
+
statusCode: error.status,
|
|
93
|
+
statusMessage: error.message,
|
|
94
|
+
message: error.message
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
await issueCsrfCookie(event, request);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
var nuxtSecurityInternals = {
|
|
103
|
+
createRequest,
|
|
104
|
+
issueCsrfCookie
|
|
105
|
+
};
|
|
106
|
+
export {
|
|
107
|
+
csrfProtection,
|
|
108
|
+
nuxtSecurityInternals
|
|
109
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type SvelteKitCookieOptions = {
|
|
2
|
+
path: string;
|
|
3
|
+
secure?: boolean;
|
|
4
|
+
httpOnly?: boolean;
|
|
5
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
6
|
+
};
|
|
7
|
+
type SvelteKitCsrfEvent = {
|
|
8
|
+
readonly url: URL;
|
|
9
|
+
readonly request: Request;
|
|
10
|
+
readonly cookies: {
|
|
11
|
+
get(name: string): string | undefined;
|
|
12
|
+
set(name: string, value: string, options: SvelteKitCookieOptions): void;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
type SvelteKitResolveOptions = {
|
|
16
|
+
readonly transformPageChunk?: (input: {
|
|
17
|
+
readonly html: string;
|
|
18
|
+
readonly done: boolean;
|
|
19
|
+
}) => string | Promise<string>;
|
|
20
|
+
readonly filterSerializedResponseHeaders?: (name: string, value: string) => boolean;
|
|
21
|
+
readonly preload?: (input: {
|
|
22
|
+
readonly type: 'js' | 'css' | 'font' | 'asset';
|
|
23
|
+
readonly path: string;
|
|
24
|
+
}) => boolean;
|
|
25
|
+
};
|
|
26
|
+
type SvelteKitCsrfHandleInput<TEvent extends SvelteKitCsrfEvent = SvelteKitCsrfEvent> = {
|
|
27
|
+
readonly event: TEvent;
|
|
28
|
+
readonly resolve: (event: TEvent, options?: SvelteKitResolveOptions) => Response | Promise<Response>;
|
|
29
|
+
};
|
|
30
|
+
type SvelteKitCsrfHandle = <TEvent extends SvelteKitCsrfEvent>(input: SvelteKitCsrfHandleInput<TEvent>) => Response | Promise<Response>;
|
|
31
|
+
declare function issueCsrfCookie(event: SvelteKitCsrfEvent): Promise<void>;
|
|
32
|
+
declare function csrfProtection(): SvelteKitCsrfHandle;
|
|
33
|
+
declare const svelteKitSecurityInternals: {
|
|
34
|
+
issueCsrfCookie: typeof issueCsrfCookie;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { type SvelteKitCsrfHandle, type SvelteKitCsrfHandleInput, csrfProtection, svelteKitSecurityInternals };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
csrf,
|
|
3
|
+
getSecurityRuntime,
|
|
4
|
+
isSecureRequest,
|
|
5
|
+
protect
|
|
6
|
+
} from "../chunk-Q3A7RJ67.mjs";
|
|
7
|
+
import {
|
|
8
|
+
SECURITY_CLIENT_CONFIG_COOKIE,
|
|
9
|
+
createSecurityClientConfig,
|
|
10
|
+
serializeSecurityClientConfig
|
|
11
|
+
} from "../chunk-FUOZWKHK.mjs";
|
|
12
|
+
import {
|
|
13
|
+
SecurityCsrfError
|
|
14
|
+
} from "../chunk-EWQKJSFA.mjs";
|
|
15
|
+
|
|
16
|
+
// src/sveltekit/server.ts
|
|
17
|
+
function isSafeMethod(method) {
|
|
18
|
+
const normalized = method.trim().toUpperCase();
|
|
19
|
+
return normalized === "GET" || normalized === "HEAD";
|
|
20
|
+
}
|
|
21
|
+
async function issueCsrfCookie(event) {
|
|
22
|
+
const runtime = getSecurityRuntime();
|
|
23
|
+
const { config } = runtime;
|
|
24
|
+
if (!config.csrf.enabled || !isSafeMethod(event.request.method)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
event.cookies.set(config.csrf.cookie, await csrf.token(event.request), {
|
|
28
|
+
httpOnly: false,
|
|
29
|
+
path: "/",
|
|
30
|
+
sameSite: "lax",
|
|
31
|
+
secure: isSecureRequest(event.request)
|
|
32
|
+
});
|
|
33
|
+
event.cookies.set(SECURITY_CLIENT_CONFIG_COOKIE, serializeSecurityClientConfig(createSecurityClientConfig(config)), {
|
|
34
|
+
httpOnly: false,
|
|
35
|
+
path: "/",
|
|
36
|
+
sameSite: "lax",
|
|
37
|
+
secure: isSecureRequest(event.request)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function createCsrfErrorResponse(error) {
|
|
41
|
+
return new Response(error.message, {
|
|
42
|
+
status: error.status,
|
|
43
|
+
headers: {
|
|
44
|
+
"content-type": "text/plain; charset=utf-8"
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function csrfProtection() {
|
|
49
|
+
return async ({ event, resolve }) => {
|
|
50
|
+
try {
|
|
51
|
+
await protect(event.request);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof SecurityCsrfError) {
|
|
54
|
+
return createCsrfErrorResponse(error);
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
await issueCsrfCookie(event);
|
|
59
|
+
return resolve(event);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
var svelteKitSecurityInternals = {
|
|
63
|
+
issueCsrfCookie
|
|
64
|
+
};
|
|
65
|
+
export {
|
|
66
|
+
csrfProtection,
|
|
67
|
+
svelteKitSecurityInternals
|
|
68
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holo-js/security",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Holo-JS Framework - CSRF and rate-limit contracts, config, and runtime seams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,21 @@
|
|
|
20
20
|
"import": "./dist/contracts.mjs",
|
|
21
21
|
"default": "./dist/contracts.mjs"
|
|
22
22
|
},
|
|
23
|
+
"./next/server": {
|
|
24
|
+
"types": "./dist/next/server.d.ts",
|
|
25
|
+
"import": "./dist/next/server.mjs",
|
|
26
|
+
"default": "./dist/next/server.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./nuxt/server": {
|
|
29
|
+
"types": "./dist/nuxt/server.d.ts",
|
|
30
|
+
"import": "./dist/nuxt/server.mjs",
|
|
31
|
+
"default": "./dist/nuxt/server.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./sveltekit/server": {
|
|
34
|
+
"types": "./dist/sveltekit/server.d.ts",
|
|
35
|
+
"import": "./dist/sveltekit/server.mjs",
|
|
36
|
+
"default": "./dist/sveltekit/server.mjs"
|
|
37
|
+
},
|
|
23
38
|
"./drivers/redis-adapter": {
|
|
24
39
|
"types": "./dist/drivers/redis-adapter.d.ts",
|
|
25
40
|
"import": "./dist/drivers/redis-adapter.mjs",
|
|
@@ -35,24 +50,34 @@
|
|
|
35
50
|
"build": "tsup",
|
|
36
51
|
"stub": "tsup",
|
|
37
52
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
-
"test": "vitest --run"
|
|
53
|
+
"test": "vitest --run --coverage"
|
|
39
54
|
},
|
|
40
55
|
"dependencies": {
|
|
41
|
-
"@holo-js/config": "^0.1.
|
|
56
|
+
"@holo-js/config": "^0.1.5"
|
|
42
57
|
},
|
|
43
58
|
"peerDependencies": {
|
|
44
|
-
"
|
|
59
|
+
"h3": "^1.15.11",
|
|
60
|
+
"next": "^16.2.4",
|
|
61
|
+
"ioredis": "^5.4.2"
|
|
45
62
|
},
|
|
46
63
|
"peerDependenciesMeta": {
|
|
64
|
+
"h3": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"next": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
47
70
|
"ioredis": {
|
|
48
71
|
"optional": true
|
|
49
72
|
}
|
|
50
73
|
},
|
|
51
74
|
"devDependencies": {
|
|
52
75
|
"@types/node": "^22.10.2",
|
|
53
|
-
"
|
|
76
|
+
"h3": "^1.15.11",
|
|
77
|
+
"ioredis": "^5.4.2",
|
|
78
|
+
"next": "^16.2.4",
|
|
54
79
|
"tsup": "^8.3.5",
|
|
55
80
|
"typescript": "^5.7.2",
|
|
56
|
-
"vitest": "^
|
|
81
|
+
"vitest": "^4.1.5"
|
|
57
82
|
}
|
|
58
83
|
}
|