@rimelight/security 0.0.20 → 0.0.22

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.
@@ -1,3 +1,3 @@
1
- import { i as SecurityOptions, n as RateLimitOptions, r as RateLimiterBinding, t as ConstructionOptions } from "../types-Dcvwj-af.mjs";
1
+ import { i as SecurityOptions, n as RateLimitOptions, r as RateLimiterBinding, t as ConstructionOptions } from "../types-CL5HooHU.mjs";
2
2
  import { buildCspHeader } from "../csp.mjs";
3
3
  export { ConstructionOptions, RateLimitOptions, RateLimiterBinding, SecurityOptions, buildCspHeader };
package/dist/csp.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as SecurityOptions } from "./types-Dcvwj-af.mjs";
1
+ import { i as SecurityOptions } from "./types-CL5HooHU.mjs";
2
2
  //#region src/csp.d.ts
3
3
  /**
4
4
  * Builds the Content-Security-Policy header string dynamically for a given request nonce.
package/dist/csp.mjs CHANGED
@@ -4,27 +4,31 @@
4
4
  */
5
5
  function buildCspHeader(options = {}, nonce) {
6
6
  const domain = options.domain || "";
7
+ const imgSources = Array.from(/* @__PURE__ */ new Set([
8
+ "'self'",
9
+ "data:",
10
+ ...domain ? [`https://cdn.${domain}`] : [],
11
+ "https://i3.ytimg.com",
12
+ "https://www.youtube.com",
13
+ "https://www.youtube-nocookie.com",
14
+ ...options.imgSrc || []
15
+ ]));
16
+ const allowTurnstile = options.turnstile !== false;
7
17
  const defaultDirectives = {
8
18
  "default-src": ["'none'"],
9
- "img-src": Array.from(/* @__PURE__ */ new Set([
10
- "'self'",
11
- "data:",
12
- ...domain ? [`https://cdn.${domain}`] : [],
13
- "https://i3.ytimg.com",
14
- "https://www.youtube.com",
15
- "https://www.youtube-nocookie.com",
16
- ...options.imgSrc || []
17
- ])),
19
+ "img-src": imgSources,
18
20
  "font-src": ["'self'"],
19
21
  "connect-src": Array.from(/* @__PURE__ */ new Set([
20
22
  "'self'",
21
23
  "https://cloudflareinsights.com",
24
+ ...allowTurnstile ? ["https://challenges.cloudflare.com"] : [],
22
25
  ...options.connectSrc || []
23
26
  ])),
24
27
  "frame-ancestors": ["'none'"],
25
28
  "frame-src": Array.from(/* @__PURE__ */ new Set([
26
29
  "https://www.youtube.com",
27
30
  "https://www.youtube-nocookie.com",
31
+ ...allowTurnstile ? ["https://challenges.cloudflare.com"] : [],
28
32
  ...options.frameSrc || []
29
33
  ])),
30
34
  "script-src": Array.from(/* @__PURE__ */ new Set([
@@ -32,6 +36,7 @@ function buildCspHeader(options = {}, nonce) {
32
36
  ...nonce ? [`'nonce-${nonce}'`] : [],
33
37
  "https://static.cloudflareinsights.com",
34
38
  "https://betterlytics.io/analytics.js",
39
+ ...allowTurnstile ? ["https://challenges.cloudflare.com"] : [],
35
40
  "'inline-speculation-rules'",
36
41
  "'sha256-ZuMSxilKU+4KIM8LWna4lqBEKzd6WaiAjz6gamhm7zM='",
37
42
  ...options.scriptResources || []
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
- import { i as SecurityOptions, n as RateLimitOptions, r as RateLimiterBinding, t as ConstructionOptions } from "./types-Dcvwj-af.mjs";
1
+ import { i as SecurityOptions, n as RateLimitOptions, r as RateLimiterBinding, t as ConstructionOptions } from "./types-CL5HooHU.mjs";
2
2
  import { buildCspHeader } from "./csp.mjs";
3
3
  import { RimelightSecurityPlugin, SecurityPluginOptions, security } from "./vite.mjs";
4
4
  import { CONSTRUCTION_GUEST_COOKIE, construction, isConstructionGuest, signInConstructionGuest } from "./middleware/construction.mjs";
5
5
  import rimelightSecurity, { RimelightSecurityOptions } from "./integration.mjs";
6
- export { CONSTRUCTION_GUEST_COOKIE, ConstructionOptions, RateLimitOptions, RateLimiterBinding, RimelightSecurityOptions, RimelightSecurityPlugin, SecurityOptions, SecurityPluginOptions, buildCspHeader, construction, isConstructionGuest, rimelightSecurity, security, security as sri, signInConstructionGuest };
6
+ import { TurnstileVerifyOptions, TurnstileVerifyResult, getTurnstileSecretKey, getTurnstileSiteKey, isTurnstileEnabled, verifyTurnstile } from "./turnstile.mjs";
7
+ export { CONSTRUCTION_GUEST_COOKIE, ConstructionOptions, RateLimitOptions, RateLimiterBinding, RimelightSecurityOptions, RimelightSecurityPlugin, SecurityOptions, SecurityPluginOptions, TurnstileVerifyOptions, TurnstileVerifyResult, buildCspHeader, construction, getTurnstileSecretKey, getTurnstileSiteKey, isConstructionGuest, isTurnstileEnabled, rimelightSecurity, security, security as sri, signInConstructionGuest, verifyTurnstile };
package/dist/index.mjs CHANGED
@@ -3,4 +3,5 @@ import { buildCspHeader } from "./csp.mjs";
3
3
  import { security } from "./vite.mjs";
4
4
  import { CONSTRUCTION_GUEST_COOKIE, construction, isConstructionGuest, signInConstructionGuest } from "./middleware/construction.mjs";
5
5
  import rimelightSecurity from "./integration.mjs";
6
- export { CONSTRUCTION_GUEST_COOKIE, buildCspHeader, construction, isConstructionGuest, rimelightSecurity, security, security as sri, signInConstructionGuest };
6
+ import { getTurnstileSecretKey, getTurnstileSiteKey, isTurnstileEnabled, verifyTurnstile } from "./turnstile.mjs";
7
+ export { CONSTRUCTION_GUEST_COOKIE, buildCspHeader, construction, getTurnstileSecretKey, getTurnstileSiteKey, isConstructionGuest, isTurnstileEnabled, rimelightSecurity, security, security as sri, signInConstructionGuest, verifyTurnstile };
@@ -1,4 +1,4 @@
1
- import { i as SecurityOptions, t as ConstructionOptions } from "./types-Dcvwj-af.mjs";
1
+ import { i as SecurityOptions, t as ConstructionOptions } from "./types-CL5HooHU.mjs";
2
2
  import { AstroIntegration } from "astro";
3
3
  //#region src/integration.d.ts
4
4
  export interface RimelightSecurityOptions extends SecurityOptions {
@@ -1,4 +1,4 @@
1
- import { t as ConstructionOptions } from "../types-Dcvwj-af.mjs";
1
+ import { t as ConstructionOptions } from "../types-CL5HooHU.mjs";
2
2
  //#region src/middleware/construction.d.ts
3
3
  export declare const CONSTRUCTION_GUEST_COOKIE = "rimelight-construction-guest";
4
4
  /**
@@ -8,8 +8,14 @@ const DEFAULT_WHITELISTED_PATTERNS = [
8
8
  "/_astro/",
9
9
  "/_image",
10
10
  "/favicon.",
11
+ "/apple-touch-icon",
12
+ "/site.webmanifest",
13
+ "manifest.json",
14
+ ".webmanifest",
11
15
  "/robots.txt",
12
16
  "/sitemap",
17
+ "/rss.xml",
18
+ "/llms.txt",
13
19
  "/.well-known/"
14
20
  ];
15
21
  const getPassphrase = (c, options) => {
@@ -1,4 +1,4 @@
1
- import { n as RateLimitOptions } from "../types-Dcvwj-af.mjs";
1
+ import { n as RateLimitOptions } from "../types-CL5HooHU.mjs";
2
2
  //#region src/middleware/ratelimit.d.ts
3
3
  /**
4
4
  * Middleware to enforce rate limits on sensitive endpoints using Cloudflare Rate Limiting bindings.
@@ -18,5 +18,5 @@ import { n as RateLimitOptions } from "../types-Dcvwj-af.mjs";
18
18
  * })
19
19
  * )
20
20
  */
21
- export declare const ratelimit: (options?: RateLimitOptions) => (c: any, next: () => Promise<any> | any) => Promise<Response | any>;
21
+ export declare const ratelimit: (options?: RateLimitOptions) => (c: any, next: () => Promise<any>) => Promise<any>;
22
22
  //#endregion
@@ -1,4 +1,4 @@
1
- import { i as SecurityOptions } from "../types-Dcvwj-af.mjs";
1
+ import { i as SecurityOptions } from "../types-CL5HooHU.mjs";
2
2
  //#region src/middleware/security.d.ts
3
3
  /**
4
4
  * Universal Security Middleware for Hono and Web Standards (Fetch API). Injects security headers,
@@ -122,16 +122,7 @@ function security(options = {}) {
122
122
  };
123
123
  const reportOnlySimulation = {
124
124
  ...mergedOptions,
125
- directives: {
126
- ...mergedOptions.directives,
127
- "script-src-attr": ["'none'"],
128
- "style-src": ["'self'", "'unsafe-inline'"],
129
- "script-src": [
130
- "'self'",
131
- "'unsafe-inline'",
132
- "'unsafe-eval'"
133
- ]
134
- }
125
+ directives: { ...mergedOptions.directives }
135
126
  };
136
127
  c.res = applySecurityHeaders(response, {
137
128
  ...mergedOptions,
@@ -0,0 +1,53 @@
1
+ //#region src/turnstile.d.ts
2
+ export interface TurnstileVerifyOptions {
3
+ /**
4
+ * The response token provided by the Turnstile client-side widget (e.g. `cf-turnstile-response`).
5
+ */
6
+ token?: string | null | undefined;
7
+ /**
8
+ * The secret key for Turnstile verification. Can be passed directly or read from `env`.
9
+ */
10
+ secretKey?: string | undefined;
11
+ /**
12
+ * The Cloudflare environment or process environment object containing TURNSTILE_SECRET_KEY /
13
+ * TURNSTILE_SITE_KEY.
14
+ */
15
+ env?: any;
16
+ /**
17
+ * The client's IP address (e.g. from `CF-Connecting-IP` or `Astro.clientAddress`).
18
+ */
19
+ remoteIp?: string | undefined;
20
+ /**
21
+ * Whether to fail open (succeed) if keys are not configured or during network errors. Defaults to
22
+ * `true` (safe for local development).
23
+ */
24
+ failOpen?: boolean | undefined;
25
+ /**
26
+ * Custom verify endpoint URL. Defaults to
27
+ * `https://challenges.cloudflare.com/turnstile/v0/siteverify`.
28
+ */
29
+ endpoint?: string | undefined;
30
+ }
31
+ export interface TurnstileVerifyResult {
32
+ success: boolean;
33
+ error?: string | undefined;
34
+ errorCodes?: string[] | undefined;
35
+ }
36
+ /**
37
+ * Checks if Turnstile keys are present in the provided environment or `process.env`.
38
+ */
39
+ export declare function isTurnstileEnabled(env?: any): boolean;
40
+ /**
41
+ * Resolves the Turnstile secret key from options or environment.
42
+ */
43
+ export declare function getTurnstileSecretKey(options: TurnstileVerifyOptions): string | undefined;
44
+ /**
45
+ * Resolves the Turnstile site key from environment or process.env.
46
+ */
47
+ export declare function getTurnstileSiteKey(env?: any): string | undefined;
48
+ /**
49
+ * Verifies a Cloudflare Turnstile token on the server. Supports both options object and classic
50
+ * signature `(token, env, remoteIp)`.
51
+ */
52
+ export declare function verifyTurnstile(tokenOrOptions: string | null | undefined | TurnstileVerifyOptions, legacyEnv?: any, legacyRemoteIp?: string): Promise<TurnstileVerifyResult>;
53
+ //#endregion
@@ -0,0 +1,92 @@
1
+ //#region src/turnstile.ts
2
+ /**
3
+ * Checks if Turnstile keys are present in the provided environment or `process.env`.
4
+ */
5
+ function isTurnstileEnabled(env) {
6
+ const siteKey = env?.TURNSTILE_SITE_KEY || (typeof process !== "undefined" ? process.env?.["TURNSTILE_SITE_KEY"] : void 0);
7
+ const secretKey = env?.TURNSTILE_SECRET_KEY || (typeof process !== "undefined" ? process.env?.["TURNSTILE_SECRET_KEY"] : void 0);
8
+ return Boolean(siteKey && secretKey);
9
+ }
10
+ /**
11
+ * Resolves the Turnstile secret key from options or environment.
12
+ */
13
+ function getTurnstileSecretKey(options) {
14
+ if (options.secretKey) return options.secretKey;
15
+ return options.env?.TURNSTILE_SECRET_KEY || (typeof process !== "undefined" ? process.env?.["TURNSTILE_SECRET_KEY"] : void 0);
16
+ }
17
+ /**
18
+ * Resolves the Turnstile site key from environment or process.env.
19
+ */
20
+ function getTurnstileSiteKey(env) {
21
+ return env?.TURNSTILE_SITE_KEY || (typeof process !== "undefined" ? process.env?.["TURNSTILE_SITE_KEY"] : void 0);
22
+ }
23
+ /**
24
+ * Verifies a Cloudflare Turnstile token on the server. Supports both options object and classic
25
+ * signature `(token, env, remoteIp)`.
26
+ */
27
+ async function verifyTurnstile(tokenOrOptions, legacyEnv, legacyRemoteIp) {
28
+ const options = typeof tokenOrOptions === "object" && tokenOrOptions !== null ? tokenOrOptions : {
29
+ token: tokenOrOptions,
30
+ env: legacyEnv,
31
+ remoteIp: legacyRemoteIp
32
+ };
33
+ const failOpen = options.failOpen ?? true;
34
+ const token = options.token;
35
+ const secretKey = getTurnstileSecretKey(options);
36
+ if (!secretKey) {
37
+ if (failOpen) {
38
+ console.log("[Turnstile] Keys missing or not configured. Skipping verification (Failing Open for local dev).");
39
+ return { success: true };
40
+ }
41
+ return {
42
+ success: false,
43
+ error: "Turnstile secret key is not configured."
44
+ };
45
+ }
46
+ if (!token) return {
47
+ success: false,
48
+ error: "Security check token is missing."
49
+ };
50
+ try {
51
+ const endpoint = options.endpoint || "https://challenges.cloudflare.com/turnstile/v0/siteverify";
52
+ const response = await fetch(endpoint, {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({
56
+ secret: secretKey,
57
+ response: token,
58
+ remoteip: options.remoteIp
59
+ })
60
+ });
61
+ if (!response.ok) {
62
+ if (failOpen) {
63
+ console.error(`[Turnstile] API returned status ${response.status}. Failing open.`);
64
+ return { success: true };
65
+ }
66
+ return {
67
+ success: false,
68
+ error: `Cloudflare Turnstile API returned status ${response.status}`
69
+ };
70
+ }
71
+ const data = await response.json();
72
+ if (data.success) return { success: true };
73
+ const codes = data["error-codes"] || [];
74
+ let msg = "Bot check verification failed.";
75
+ if (codes.includes("timeout-or-duplicate")) msg = "Verification token expired or already used. Please refresh the security check.";
76
+ else if (codes.includes("invalid-input-response")) msg = "Invalid verification response. Please try again.";
77
+ return {
78
+ success: false,
79
+ error: msg,
80
+ errorCodes: codes
81
+ };
82
+ } catch (err) {
83
+ console.error("[Turnstile Exception]", err);
84
+ if (failOpen) return { success: true };
85
+ return {
86
+ success: false,
87
+ error: err?.message || "Turnstile verification encountered an exception."
88
+ };
89
+ }
90
+ }
91
+ //#endregion
92
+ export { getTurnstileSecretKey, getTurnstileSiteKey, isTurnstileEnabled, verifyTurnstile };
@@ -29,6 +29,11 @@ interface SecurityOptions {
29
29
  * Additional style resources / origins
30
30
  */
31
31
  styleResources?: string[];
32
+ /**
33
+ * Enables Cloudflare Turnstile CSP allowances (challenges.cloudflare.com in script-src,
34
+ * frame-src, connect-src). Defaults to true.
35
+ */
36
+ turnstile?: boolean;
32
37
  /**
33
38
  * Enables the 'require-trusted-types-for' directive
34
39
  */
package/dist/types.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as SecurityOptions, n as RateLimitOptions, r as RateLimiterBinding, t as ConstructionOptions } from "./types-Dcvwj-af.mjs";
1
+ import { i as SecurityOptions, n as RateLimitOptions, r as RateLimiterBinding, t as ConstructionOptions } from "./types-CL5HooHU.mjs";
2
2
  export { ConstructionOptions, RateLimitOptions, RateLimiterBinding, SecurityOptions };
package/dist/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as SecurityOptions } from "./types-Dcvwj-af.mjs";
1
+ import { i as SecurityOptions } from "./types-CL5HooHU.mjs";
2
2
  //#region src/vite.d.ts
3
3
  export interface RimelightSecurityPlugin {
4
4
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/security",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's Security Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -37,6 +37,10 @@
37
37
  "types": "./dist/integration.d.mts",
38
38
  "import": "./dist/integration.mjs"
39
39
  },
40
+ "./turnstile": {
41
+ "types": "./dist/turnstile.d.mts",
42
+ "import": "./dist/turnstile.mjs"
43
+ },
40
44
  "./components/*": "./src/components/*",
41
45
  "./layouts/*": "./src/layouts/*",
42
46
  "./pages/*": "./src/pages/*",
@@ -46,13 +50,13 @@
46
50
  "access": "public"
47
51
  },
48
52
  "dependencies": {
49
- "@rimelight/i18n": "0.0.17",
50
- "@rimelight/seo": "0.0.13",
51
- "@rimelight/ui": "0.0.57"
53
+ "@rimelight/i18n": "0.0.18",
54
+ "@rimelight/seo": "0.0.15",
55
+ "@rimelight/ui": "0.0.58"
52
56
  },
53
57
  "devDependencies": {
54
58
  "@astrojs/check": "0.9.10",
55
- "@rimelight/config": "0.0.16",
59
+ "@rimelight/config": "0.0.18",
56
60
  "astro": "7.3.3",
57
61
  "hono": "4.13.8",
58
62
  "typescript": "6.0.3"
@@ -67,7 +71,7 @@
67
71
  }
68
72
  },
69
73
  "engines": {
70
- "node": ">=26.8.2"
74
+ "node": ">=26.9.0"
71
75
  },
72
76
  "scripts": {
73
77
  "build": "vp pack",
@@ -0,0 +1,94 @@
1
+ ---
2
+ import { getTurnstileSiteKey } from "../turnstile"
3
+
4
+ export interface Props {
5
+ /**
6
+ * Cloudflare Turnstile Site Key. If omitted, attempts to read from `Astro.locals.runtime.env` or `import.meta.env`.
7
+ */
8
+ siteKey?: string
9
+
10
+ /**
11
+ * Theme mode for Turnstile widget: "auto", "light", or "dark". Defaults to "auto".
12
+ */
13
+ theme?: "auto" | "light" | "dark"
14
+
15
+ /**
16
+ * Widget size: "normal", "compact", or "flexible". Defaults to "normal".
17
+ */
18
+ size?: "normal" | "compact" | "flexible"
19
+
20
+ /**
21
+ * Optional custom CSS class.
22
+ */
23
+ class?: string
24
+
25
+ /**
26
+ * Form field name for the generated token. Defaults to "cf-turnstile-response".
27
+ */
28
+ responseFieldName?: string
29
+
30
+ /**
31
+ * Action name used for Turnstile analytics / reporting.
32
+ */
33
+ action?: string
34
+
35
+ /**
36
+ * CData payload if using custom challenge metadata.
37
+ */
38
+ cdata?: string
39
+
40
+ /**
41
+ * Turnstile execution mode: "render" (immediate) or "execute" (explicit).
42
+ */
43
+ execution?: "render" | "execute"
44
+
45
+ /**
46
+ * Turnstile appearance mode: "always", "execute", or "interaction-only".
47
+ */
48
+ appearance?: "always" | "execute" | "interaction-only"
49
+
50
+ /**
51
+ * Automatically load the Cloudflare Turnstile api.js script. Defaults to true.
52
+ */
53
+ injectScript?: boolean
54
+ }
55
+
56
+ const {
57
+ siteKey: propSiteKey,
58
+ theme = "auto",
59
+ size = "normal",
60
+ class: className = "cf-turnstile my-2",
61
+ responseFieldName = "cf-turnstile-response",
62
+ action,
63
+ cdata,
64
+ execution,
65
+ appearance,
66
+ injectScript = true,
67
+ ...rest
68
+ } = Astro.props
69
+
70
+ const env = (Astro.locals as any)?.runtime?.env
71
+ const resolvedSiteKey = propSiteKey || getTurnstileSiteKey(env) || import.meta.env["TURNSTILE_SITE_KEY"] || ""
72
+ ---
73
+
74
+ {
75
+ resolvedSiteKey && (
76
+ <>
77
+ <div
78
+ class={className}
79
+ data-sitekey={resolvedSiteKey}
80
+ data-theme={theme}
81
+ data-size={size}
82
+ data-response-field-name={responseFieldName}
83
+ data-action={action}
84
+ data-cdata={cdata}
85
+ data-execution={execution}
86
+ data-appearance={appearance}
87
+ {...rest}
88
+ />
89
+ {injectScript && (
90
+ <script is:inline src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
91
+ )}
92
+ </>
93
+ )
94
+ }
package/src/csp.ts CHANGED
@@ -18,14 +18,22 @@ export function buildCspHeader(options: SecurityOptions = {}, nonce?: string): s
18
18
  ])
19
19
  )
20
20
 
21
+ const allowTurnstile = options.turnstile !== false
22
+
21
23
  const connectSources = Array.from(
22
- new Set(["'self'", "https://cloudflareinsights.com", ...(options.connectSrc || [])])
24
+ new Set([
25
+ "'self'",
26
+ "https://cloudflareinsights.com",
27
+ ...(allowTurnstile ? ["https://challenges.cloudflare.com"] : []),
28
+ ...(options.connectSrc || [])
29
+ ])
23
30
  )
24
31
 
25
32
  const frameSources = Array.from(
26
33
  new Set([
27
34
  "https://www.youtube.com",
28
35
  "https://www.youtube-nocookie.com",
36
+ ...(allowTurnstile ? ["https://challenges.cloudflare.com"] : []),
29
37
  ...(options.frameSrc || [])
30
38
  ])
31
39
  )
@@ -36,6 +44,7 @@ export function buildCspHeader(options: SecurityOptions = {}, nonce?: string): s
36
44
  ...(nonce ? [`'nonce-${nonce}'`] : []),
37
45
  "https://static.cloudflareinsights.com",
38
46
  "https://betterlytics.io/analytics.js",
47
+ ...(allowTurnstile ? ["https://challenges.cloudflare.com"] : []),
39
48
  "'inline-speculation-rules'",
40
49
  // Astro ClientRouter (View Transitions inline script sha)
41
50
  "'sha256-ZuMSxilKU+4KIM8LWna4lqBEKzd6WaiAjz6gamhm7zM='",
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./types"
3
3
  export * from "./csp"
4
4
  export * from "./middleware/construction"
5
5
  export * from "./integration"
6
+ export * from "./turnstile"
@@ -28,7 +28,6 @@ const {
28
28
  <!doctype html>
29
29
  <html lang="en">
30
30
  <head>
31
- <SecurityHead />
32
31
  <SEOHead
33
32
  title={title}
34
33
  description={description}
@@ -36,6 +35,7 @@ const {
36
35
  favicon={favicon}
37
36
  noindex={noindex}
38
37
  />
38
+ <SecurityHead />
39
39
  <UIHead />
40
40
  <Font cssVariable="--font-sans" preload={true} />
41
41
  <Font cssVariable="--font-serif" preload={true} />
@@ -10,8 +10,14 @@ const DEFAULT_WHITELISTED_PATTERNS = [
10
10
  "/_astro/",
11
11
  "/_image",
12
12
  "/favicon.",
13
+ "/apple-touch-icon",
14
+ "/site.webmanifest",
15
+ "manifest.json",
16
+ ".webmanifest",
13
17
  "/robots.txt",
14
18
  "/sitemap",
19
+ "/rss.xml",
20
+ "/llms.txt",
15
21
  "/.well-known/"
16
22
  ]
17
23
 
@@ -58,7 +58,7 @@ export const ratelimit = (options: RateLimitOptions = {}) => {
58
58
  }
59
59
  } = options
60
60
 
61
- return async (c: any, next: () => Promise<any> | any): Promise<Response | any> => {
61
+ return async (c: any, next: () => Promise<any>): Promise<any> => {
62
62
  const path = c?.req?.path || ""
63
63
 
64
64
  // 1. Check if the current request matches the rate-limited routes
@@ -184,14 +184,12 @@ export function security(options: SecurityOptions = {}) {
184
184
  "img-src": ["'self'", "data:", "blob:", "https:", "http:"]
185
185
  }
186
186
 
187
- // Prod simulation: check strict domain / asset permissions without tripping on Vite HMR inline CSS
187
+ // Prod simulation: check strict production directives in Report-Only mode
188
+ // so any violation that will break in production emits a warning in dev
188
189
  const reportOnlySimulation: SecurityOptions = {
189
190
  ...mergedOptions,
190
191
  directives: {
191
- ...mergedOptions.directives,
192
- "script-src-attr": ["'none'"],
193
- "style-src": ["'self'", "'unsafe-inline'"],
194
- "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'"]
192
+ ...mergedOptions.directives
195
193
  }
196
194
  }
197
195
 
@@ -0,0 +1,172 @@
1
+ export interface TurnstileVerifyOptions {
2
+ /**
3
+ * The response token provided by the Turnstile client-side widget (e.g. `cf-turnstile-response`).
4
+ */
5
+ token?: string | null | undefined
6
+
7
+ /**
8
+ * The secret key for Turnstile verification. Can be passed directly or read from `env`.
9
+ */
10
+ secretKey?: string | undefined
11
+
12
+ /**
13
+ * The Cloudflare environment or process environment object containing TURNSTILE_SECRET_KEY /
14
+ * TURNSTILE_SITE_KEY.
15
+ */
16
+ env?: any
17
+
18
+ /**
19
+ * The client's IP address (e.g. from `CF-Connecting-IP` or `Astro.clientAddress`).
20
+ */
21
+ remoteIp?: string | undefined
22
+
23
+ /**
24
+ * Whether to fail open (succeed) if keys are not configured or during network errors. Defaults to
25
+ * `true` (safe for local development).
26
+ */
27
+ failOpen?: boolean | undefined
28
+
29
+ /**
30
+ * Custom verify endpoint URL. Defaults to
31
+ * `https://challenges.cloudflare.com/turnstile/v0/siteverify`.
32
+ */
33
+ endpoint?: string | undefined
34
+ }
35
+
36
+ export interface TurnstileVerifyResult {
37
+ success: boolean
38
+ error?: string | undefined
39
+ errorCodes?: string[] | undefined
40
+ }
41
+
42
+ interface CloudflareVerifyResponse {
43
+ "success": boolean
44
+ "error-codes"?: string[]
45
+ "challenge_ts"?: string
46
+ "hostname"?: string
47
+ "action"?: string
48
+ "cdata"?: string
49
+ }
50
+
51
+ /**
52
+ * Checks if Turnstile keys are present in the provided environment or `process.env`.
53
+ */
54
+ export function isTurnstileEnabled(env?: any): boolean {
55
+ const siteKey =
56
+ env?.TURNSTILE_SITE_KEY ||
57
+ (typeof process !== "undefined" ? process.env?.["TURNSTILE_SITE_KEY"] : undefined)
58
+
59
+ const secretKey =
60
+ env?.TURNSTILE_SECRET_KEY ||
61
+ (typeof process !== "undefined" ? process.env?.["TURNSTILE_SECRET_KEY"] : undefined)
62
+
63
+ return Boolean(siteKey && secretKey)
64
+ }
65
+
66
+ /**
67
+ * Resolves the Turnstile secret key from options or environment.
68
+ */
69
+ export function getTurnstileSecretKey(options: TurnstileVerifyOptions): string | undefined {
70
+ if (options.secretKey) return options.secretKey
71
+ const env = options.env
72
+ return (
73
+ env?.TURNSTILE_SECRET_KEY ||
74
+ (typeof process !== "undefined" ? process.env?.["TURNSTILE_SECRET_KEY"] : undefined)
75
+ )
76
+ }
77
+
78
+ /**
79
+ * Resolves the Turnstile site key from environment or process.env.
80
+ */
81
+ export function getTurnstileSiteKey(env?: any): string | undefined {
82
+ return (
83
+ env?.TURNSTILE_SITE_KEY ||
84
+ (typeof process !== "undefined" ? process.env?.["TURNSTILE_SITE_KEY"] : undefined)
85
+ )
86
+ }
87
+
88
+ /**
89
+ * Verifies a Cloudflare Turnstile token on the server. Supports both options object and classic
90
+ * signature `(token, env, remoteIp)`.
91
+ */
92
+ export async function verifyTurnstile(
93
+ tokenOrOptions: string | null | undefined | TurnstileVerifyOptions,
94
+ legacyEnv?: any,
95
+ legacyRemoteIp?: string
96
+ ): Promise<TurnstileVerifyResult> {
97
+ const options: TurnstileVerifyOptions =
98
+ typeof tokenOrOptions === "object" && tokenOrOptions !== null
99
+ ? tokenOrOptions
100
+ : {
101
+ token: tokenOrOptions,
102
+ env: legacyEnv,
103
+ remoteIp: legacyRemoteIp
104
+ }
105
+
106
+ const failOpen = options.failOpen ?? true
107
+ const token = options.token
108
+
109
+ // If Turnstile is not enabled (missing keys in env and no explicit secretKey), fail open for local dev
110
+ const secretKey = getTurnstileSecretKey(options)
111
+ if (!secretKey) {
112
+ if (failOpen) {
113
+ console.log(
114
+ "[Turnstile] Keys missing or not configured. Skipping verification (Failing Open for local dev)."
115
+ )
116
+ return { success: true }
117
+ }
118
+ return { success: false, error: "Turnstile secret key is not configured." }
119
+ }
120
+
121
+ if (!token) {
122
+ return { success: false, error: "Security check token is missing." }
123
+ }
124
+
125
+ try {
126
+ const endpoint = options.endpoint || "https://challenges.cloudflare.com/turnstile/v0/siteverify"
127
+ const response = await fetch(endpoint, {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({
131
+ secret: secretKey,
132
+ response: token,
133
+ remoteip: options.remoteIp
134
+ })
135
+ })
136
+
137
+ if (!response.ok) {
138
+ if (failOpen) {
139
+ console.error(`[Turnstile] API returned status ${response.status}. Failing open.`)
140
+ return { success: true }
141
+ }
142
+ return {
143
+ success: false,
144
+ error: `Cloudflare Turnstile API returned status ${response.status}`
145
+ }
146
+ }
147
+
148
+ const data: CloudflareVerifyResponse = await response.json()
149
+ if (data.success) {
150
+ return { success: true }
151
+ }
152
+
153
+ const codes = data["error-codes"] || []
154
+ let msg = "Bot check verification failed."
155
+ if (codes.includes("timeout-or-duplicate")) {
156
+ msg = "Verification token expired or already used. Please refresh the security check."
157
+ } else if (codes.includes("invalid-input-response")) {
158
+ msg = "Invalid verification response. Please try again."
159
+ }
160
+
161
+ return { success: false, error: msg, errorCodes: codes }
162
+ } catch (err: any) {
163
+ console.error("[Turnstile Exception]", err)
164
+ if (failOpen) {
165
+ return { success: true }
166
+ }
167
+ return {
168
+ success: false,
169
+ error: err?.message || "Turnstile verification encountered an exception."
170
+ }
171
+ }
172
+ }
package/src/types.ts CHANGED
@@ -35,6 +35,12 @@ export interface SecurityOptions {
35
35
  */
36
36
  styleResources?: string[]
37
37
 
38
+ /**
39
+ * Enables Cloudflare Turnstile CSP allowances (challenges.cloudflare.com in script-src,
40
+ * frame-src, connect-src). Defaults to true.
41
+ */
42
+ turnstile?: boolean
43
+
38
44
  /**
39
45
  * Enables the 'require-trusted-types-for' directive
40
46
  */