@appwarden/middleware 3.2.1 → 3.4.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.
@@ -0,0 +1,332 @@
1
+ // src/utils/cloudflare/cloudflare-cache.ts
2
+ var store = {
3
+ json: (context, cacheKey, options) => {
4
+ const cacheKeyUrl = new URL(cacheKey, context.serviceOrigin);
5
+ return {
6
+ getValue: () => getCacheValue(context, cacheKeyUrl),
7
+ updateValue: (json) => updateCacheValue(context, cacheKeyUrl, json, options?.ttl),
8
+ deleteValue: () => clearCache(context, cacheKeyUrl)
9
+ };
10
+ }
11
+ };
12
+ var getCacheValue = async (context, cacheKey) => {
13
+ const match = await context.cache.match(cacheKey);
14
+ if (!match) {
15
+ context.debug(`[${cacheKey.pathname}] Cache MISS!`);
16
+ return void 0;
17
+ }
18
+ context.debug(`[${cacheKey.pathname}] Cache MATCH!`);
19
+ return match;
20
+ };
21
+ var updateCacheValue = async (context, cacheKey, value, ttl) => {
22
+ context.debug(
23
+ "updating cache...",
24
+ cacheKey.href,
25
+ value,
26
+ ttl ? `expires in ${ttl}s` : ""
27
+ );
28
+ await context.cache.put(
29
+ cacheKey,
30
+ new Response(JSON.stringify(value), {
31
+ headers: {
32
+ "content-type": "application/json",
33
+ ...ttl && {
34
+ "cache-control": `max-age=${ttl}`
35
+ }
36
+ }
37
+ })
38
+ );
39
+ };
40
+ var clearCache = (context, cacheKey) => context.cache.delete(cacheKey);
41
+
42
+ // src/utils/cloudflare/csp-keywords.ts
43
+ var CSP_KEYWORDS = [
44
+ "self",
45
+ "none",
46
+ "unsafe-inline",
47
+ "unsafe-eval",
48
+ "unsafe-hashes",
49
+ "strict-dynamic",
50
+ "report-sample",
51
+ "unsafe-allow-redirects",
52
+ "wasm-unsafe-eval",
53
+ "trusted-types-eval",
54
+ "report-sha256",
55
+ "report-sha384",
56
+ "report-sha512",
57
+ "unsafe-webtransport-hashes"
58
+ ];
59
+ var CSP_KEYWORDS_SET = new Set(CSP_KEYWORDS);
60
+ var isCSPKeyword = (value) => {
61
+ return CSP_KEYWORDS_SET.has(value.toLowerCase());
62
+ };
63
+ var isQuoted = (value) => {
64
+ return value.startsWith("'") && value.endsWith("'");
65
+ };
66
+ var autoQuoteCSPKeyword = (value) => {
67
+ const trimmed = value.trim();
68
+ if (isQuoted(trimmed)) {
69
+ return trimmed;
70
+ }
71
+ if (isCSPKeyword(trimmed)) {
72
+ return `'${trimmed}'`;
73
+ }
74
+ return trimmed;
75
+ };
76
+ var autoQuoteCSPDirectiveValue = (value) => {
77
+ return value.trim().split(/\s+/).filter(Boolean).map(autoQuoteCSPKeyword).join(" ");
78
+ };
79
+ var autoQuoteCSPDirectiveArray = (values) => {
80
+ return values.map(autoQuoteCSPKeyword);
81
+ };
82
+
83
+ // src/utils/print-message.ts
84
+ var addSlashes = (str) => str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\u0000/g, "\\0").replace(/<\/script>/gi, "<\\/script>");
85
+ var printMessage = (message) => `[@appwarden/middleware] ${addSlashes(message)}`;
86
+
87
+ // src/utils/cloudflare/delete-edge-value.ts
88
+ var deleteEdgeValue = async (context) => {
89
+ try {
90
+ switch (context.provider) {
91
+ case "cloudflare-cache": {
92
+ const success = await context.edgeCache.deleteValue();
93
+ if (!success) {
94
+ throw new Error();
95
+ }
96
+ break;
97
+ }
98
+ default:
99
+ throw new Error(`Unsupported provider: ${context.provider}`);
100
+ }
101
+ } catch (e) {
102
+ const message = "Failed to delete edge value";
103
+ console.error(
104
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
105
+ );
106
+ }
107
+ };
108
+
109
+ // src/schemas/helpers.ts
110
+ import { z } from "zod";
111
+ var BoolOrStringSchema = z.union([z.string(), z.boolean()]).optional();
112
+ var BooleanSchema = BoolOrStringSchema.transform((val) => {
113
+ if (val === "true" || val === true) {
114
+ return true;
115
+ } else if (val === "false" || val === false) {
116
+ return false;
117
+ }
118
+ throw new Error("Invalid value");
119
+ });
120
+ var AppwardenApiTokenSchema = z.string().refine((val) => !!val, { message: "appwardenApiToken is required" });
121
+ var LockValue = z.object({
122
+ isLocked: z.number(),
123
+ isLockedTest: z.number(),
124
+ lastCheck: z.number(),
125
+ code: z.string()
126
+ });
127
+
128
+ // src/utils/errors.ts
129
+ var errorsMap = {
130
+ mode: '`CSP_MODE` must be one of "disabled", "report-only", or "enforced"',
131
+ directives: {
132
+ ["DirectivesRequired" /* DirectivesRequired */]: '`CSP_DIRECTIVES` must be provided when `CSP_MODE` is "report-only" or "enforced"',
133
+ ["DirectivesBadParse" /* DirectivesBadParse */]: "Failed to parse `CSP_DIRECTIVES`. Is it a valid JSON string?"
134
+ },
135
+ appwardenApiToken: "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/guides/api-token-management."
136
+ };
137
+ var getErrors = (error) => {
138
+ const matches = [];
139
+ const errors = [...Object.entries(error.flatten().fieldErrors)];
140
+ for (const issue of error.issues) {
141
+ errors.push(
142
+ ...Object.entries(
143
+ "returnTypeError" in issue ? issue.returnTypeError.flatten().fieldErrors : {}
144
+ )
145
+ );
146
+ }
147
+ for (const [field, maybeSchemaErrorKey] of errors) {
148
+ let match = errorsMap[field];
149
+ if (match) {
150
+ if (match instanceof Object) {
151
+ if (maybeSchemaErrorKey) {
152
+ match = match[maybeSchemaErrorKey[0]];
153
+ }
154
+ }
155
+ matches.push(match);
156
+ }
157
+ }
158
+ return matches;
159
+ };
160
+
161
+ // src/utils/cloudflare/get-lock-value.ts
162
+ var getLockValue = async (context) => {
163
+ try {
164
+ let shouldDeleteEdgeValue = false;
165
+ let cacheResponse, lockValue = {
166
+ isLocked: 0,
167
+ isLockedTest: 0,
168
+ lastCheck: Date.now(),
169
+ code: ""
170
+ };
171
+ switch (context.provider) {
172
+ case "cloudflare-cache": {
173
+ cacheResponse = await context.edgeCache.getValue();
174
+ break;
175
+ }
176
+ default:
177
+ throw new Error(`Unsupported provider: ${context.provider}`);
178
+ }
179
+ if (!cacheResponse) {
180
+ return { lockValue: void 0 };
181
+ }
182
+ try {
183
+ const clonedResponse = cacheResponse?.clone();
184
+ lockValue = LockValue.parse(
185
+ clonedResponse ? await clonedResponse.json() : void 0
186
+ );
187
+ } catch (error) {
188
+ console.error(
189
+ printMessage(
190
+ `Failed to parse ${context.keyName} from edge cache - ${error}`
191
+ )
192
+ );
193
+ shouldDeleteEdgeValue = true;
194
+ }
195
+ return { lockValue, shouldDeleteEdgeValue };
196
+ } catch (e) {
197
+ const message = "Failed to retrieve edge value";
198
+ console.error(
199
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
200
+ );
201
+ return { lockValue: void 0 };
202
+ }
203
+ };
204
+
205
+ // src/utils/cloudflare/insert-errors-logs.ts
206
+ var insertErrorLogs = async (context, error) => {
207
+ const errors = getErrors(error);
208
+ for (const err of errors) {
209
+ console.log(printMessage(err));
210
+ }
211
+ return new HTMLRewriter().on("body", {
212
+ element: (elem) => {
213
+ elem.append(
214
+ `<script>
215
+ ${errors.map((err) => `console.error(\`${printMessage(err)}\`)`).join("\n")}
216
+ </script>`,
217
+ { html: true }
218
+ );
219
+ }
220
+ }).transform(await fetch(context.request));
221
+ };
222
+
223
+ // src/utils/cloudflare/make-csp-header.ts
224
+ var addNonce = (value, cspNonce) => value.replace("{{nonce}}", `'nonce-${cspNonce}'`);
225
+ var makeCSPHeader = (cspNonce, directives, mode) => {
226
+ const namesSeen = /* @__PURE__ */ new Set(), result = [];
227
+ Object.entries(directives ?? {}).forEach(([originalName, value]) => {
228
+ const name = originalName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
229
+ if (namesSeen.has(name)) {
230
+ throw new Error(`${originalName} is specified more than once`);
231
+ }
232
+ namesSeen.add(name);
233
+ let directiveValue;
234
+ if (Array.isArray(value)) {
235
+ directiveValue = autoQuoteCSPDirectiveArray(value).join(" ");
236
+ } else if (value === true) {
237
+ directiveValue = "";
238
+ } else if (typeof value === "string") {
239
+ directiveValue = autoQuoteCSPDirectiveValue(value);
240
+ } else {
241
+ return;
242
+ }
243
+ if (directiveValue) {
244
+ result.push(`${name} ${addNonce(directiveValue, cspNonce)}`);
245
+ } else {
246
+ result.push(name);
247
+ }
248
+ });
249
+ return [
250
+ mode === "enforced" ? "Content-Security-Policy" : "Content-Security-Policy-Report-Only",
251
+ result.join("; ")
252
+ ];
253
+ };
254
+
255
+ // src/utils/cloudflare/sync-edge-value.ts
256
+ var APIError = class extends Error {
257
+ constructor(message) {
258
+ super(message);
259
+ this.name = "APIError";
260
+ }
261
+ };
262
+ var DEFAULT_API_HOSTNAME = "https://api.appwarden.io";
263
+ var syncEdgeValue = async (context) => {
264
+ const apiHostname = context.appwardenApiHostname ?? DEFAULT_API_HOSTNAME;
265
+ context.debug(`Fetching lock value from API: ${apiHostname}`);
266
+ try {
267
+ const response = await fetch(new URL("/v1/status/check", apiHostname), {
268
+ method: "POST",
269
+ headers: { "content-type": "application/json" },
270
+ body: JSON.stringify({
271
+ service: "cloudflare",
272
+ provider: context.provider,
273
+ fqdn: context.requestUrl.hostname,
274
+ appwardenApiToken: context.appwardenApiToken
275
+ })
276
+ });
277
+ if (response.status !== 200) {
278
+ throw new Error(`${response.status} ${response.statusText}`);
279
+ }
280
+ if (response.headers.get("content-type")?.includes("application/json")) {
281
+ const result = await response.json();
282
+ if (result.error) {
283
+ throw new APIError(result.error.message);
284
+ }
285
+ if (!result.content) {
286
+ throw new APIError("no content from api");
287
+ }
288
+ try {
289
+ const parsedValue = LockValue.omit({ lastCheck: true }).parse(
290
+ result.content
291
+ );
292
+ context.debug(
293
+ `API call to ${apiHostname} succeeded`,
294
+ `Lock status: ${parsedValue.isLocked ? "LOCKED" : "UNLOCKED"}`
295
+ );
296
+ await context.edgeCache.updateValue({
297
+ ...parsedValue,
298
+ lastCheck: Date.now()
299
+ });
300
+ } catch (error) {
301
+ throw new APIError(`Failed to parse check endpoint result - ${error}`);
302
+ }
303
+ }
304
+ } catch (e) {
305
+ const message = `API call to ${apiHostname} failed`;
306
+ console.error(
307
+ printMessage(
308
+ e instanceof APIError ? e.message : e instanceof Error ? `${message}: ${e.message}` : message
309
+ )
310
+ );
311
+ }
312
+ };
313
+
314
+ export {
315
+ printMessage,
316
+ getErrors,
317
+ BooleanSchema,
318
+ AppwardenApiTokenSchema,
319
+ LockValue,
320
+ store,
321
+ CSP_KEYWORDS,
322
+ isCSPKeyword,
323
+ isQuoted,
324
+ autoQuoteCSPKeyword,
325
+ autoQuoteCSPDirectiveValue,
326
+ autoQuoteCSPDirectiveArray,
327
+ deleteEdgeValue,
328
+ getLockValue,
329
+ insertErrorLogs,
330
+ makeCSPHeader,
331
+ syncEdgeValue
332
+ };
@@ -0,0 +1,97 @@
1
+ // src/constants.ts
2
+ var LOCKDOWN_TEST_EXPIRY_MS = 5 * 60 * 1e3;
3
+ var errors = { badCacheConnection: "BAD_CACHE_CONNECTION" };
4
+ var globalErrors = [errors.badCacheConnection];
5
+ var APPWARDEN_TEST_ROUTE = "/_appwarden/test";
6
+ var APPWARDEN_CACHE_KEY = "appwarden-lock";
7
+
8
+ // src/schemas/use-content-security-policy.ts
9
+ import { z as z2 } from "zod";
10
+
11
+ // src/types/csp.ts
12
+ import { z } from "zod";
13
+ var stringySchema = z.union([z.array(z.string()), z.string(), z.boolean()]);
14
+ var ContentSecurityPolicySchema = z.object({
15
+ "default-src": stringySchema.optional(),
16
+ "script-src": stringySchema.optional(),
17
+ "style-src": stringySchema.optional(),
18
+ "img-src": stringySchema.optional(),
19
+ "connect-src": stringySchema.optional(),
20
+ "font-src": stringySchema.optional(),
21
+ "object-src": stringySchema.optional(),
22
+ "media-src": stringySchema.optional(),
23
+ "frame-src": stringySchema.optional(),
24
+ sandbox: stringySchema.optional(),
25
+ "report-uri": stringySchema.optional(),
26
+ "child-src": stringySchema.optional(),
27
+ "form-action": stringySchema.optional(),
28
+ "frame-ancestors": stringySchema.optional(),
29
+ "plugin-types": stringySchema.optional(),
30
+ "base-uri": stringySchema.optional(),
31
+ "report-to": stringySchema.optional(),
32
+ "worker-src": stringySchema.optional(),
33
+ "manifest-src": stringySchema.optional(),
34
+ "prefetch-src": stringySchema.optional(),
35
+ "navigate-to": stringySchema.optional(),
36
+ "require-sri-for": stringySchema.optional(),
37
+ "block-all-mixed-content": stringySchema.optional(),
38
+ "upgrade-insecure-requests": stringySchema.optional(),
39
+ "trusted-types": stringySchema.optional(),
40
+ "require-trusted-types-for": stringySchema.optional()
41
+ });
42
+
43
+ // src/utils/request-checks.ts
44
+ function isHTMLResponse(response) {
45
+ return response.headers.get("Content-Type")?.includes("text/html") ?? false;
46
+ }
47
+ function isHTMLRequest(request) {
48
+ return request.headers.get("accept")?.includes("text/html") ?? false;
49
+ }
50
+
51
+ // src/schemas/use-content-security-policy.ts
52
+ var CSPDirectivesSchema = z2.union([
53
+ z2.string(),
54
+ ContentSecurityPolicySchema
55
+ ]);
56
+ var CSPModeSchema = z2.union([
57
+ z2.literal("disabled"),
58
+ z2.literal("report-only"),
59
+ z2.literal("enforced")
60
+ ]).optional().default("disabled");
61
+ var UseCSPInputSchema = z2.object({
62
+ mode: CSPModeSchema,
63
+ directives: CSPDirectivesSchema.optional().refine(
64
+ (val) => {
65
+ try {
66
+ if (typeof val === "string") {
67
+ JSON.parse(val);
68
+ }
69
+ return true;
70
+ } catch (error) {
71
+ return false;
72
+ }
73
+ },
74
+ { message: "DirectivesBadParse" /* DirectivesBadParse */ }
75
+ ).transform(
76
+ (val) => typeof val === "string" ? JSON.parse(val) : val
77
+ )
78
+ }).refine(
79
+ (values) => (
80
+ // validate that directives are provided when the mode is "report-only" or "enforced"
81
+ ["report-only", "enforced"].includes(values.mode) ? !!values.directives : true
82
+ ),
83
+ { path: ["directives"], message: "DirectivesRequired" /* DirectivesRequired */ }
84
+ );
85
+
86
+ export {
87
+ LOCKDOWN_TEST_EXPIRY_MS,
88
+ errors,
89
+ globalErrors,
90
+ APPWARDEN_TEST_ROUTE,
91
+ APPWARDEN_CACHE_KEY,
92
+ CSPDirectivesSchema,
93
+ CSPModeSchema,
94
+ UseCSPInputSchema,
95
+ isHTMLResponse,
96
+ isHTMLRequest
97
+ };
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  LOCKDOWN_TEST_EXPIRY_MS
3
- } from "./chunk-L5EQIJZB.js";
3
+ } from "./chunk-HCGLR3Z3.js";
4
+ import {
5
+ printMessage
6
+ } from "./chunk-G4RRFNHY.js";
4
7
 
5
8
  // src/utils/build-lock-page-url.ts
6
9
  function normalizeLockPageSlug(lockPageSlug) {
@@ -34,6 +37,31 @@ var createRedirect = (url) => {
34
37
  });
35
38
  };
36
39
 
40
+ // src/utils/debug.ts
41
+ var debug = (isDebug) => (...msg) => {
42
+ if (!isDebug) return;
43
+ const formatted = msg.map((m) => {
44
+ let content;
45
+ if (m instanceof Error) {
46
+ content = m.stack ?? m.message;
47
+ } else if (typeof m === "object" && m !== null) {
48
+ try {
49
+ content = JSON.stringify(m);
50
+ } catch {
51
+ try {
52
+ content = String(m);
53
+ } catch {
54
+ content = "[Unserializable value]";
55
+ }
56
+ }
57
+ } else {
58
+ content = String(m);
59
+ }
60
+ return printMessage(content);
61
+ });
62
+ console.log(...formatted);
63
+ };
64
+
37
65
  // src/utils/memory-cache.ts
38
66
  var MemoryCache = class {
39
67
  cache = /* @__PURE__ */ new Map();
@@ -81,66 +109,11 @@ var MemoryCache = class {
81
109
  };
82
110
  };
83
111
 
84
- // src/utils/errors.ts
85
- var errorsMap = {
86
- mode: '`CSP_MODE` must be one of "disabled", "report-only", or "enforced"',
87
- directives: {
88
- ["DirectivesRequired" /* DirectivesRequired */]: '`CSP_DIRECTIVES` must be provided when `CSP_MODE` is "report-only" or "enforced"',
89
- ["DirectivesBadParse" /* DirectivesBadParse */]: "Failed to parse `CSP_DIRECTIVES`. Is it a valid JSON string?"
90
- },
91
- appwardenApiToken: "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/guides/api-token-management."
92
- };
93
- var getErrors = (error) => {
94
- const matches = [];
95
- const errors = [...Object.entries(error.flatten().fieldErrors)];
96
- for (const issue of error.issues) {
97
- errors.push(
98
- ...Object.entries(
99
- "returnTypeError" in issue ? issue.returnTypeError.flatten().fieldErrors : {}
100
- )
101
- );
102
- }
103
- for (const [field, maybeSchemaErrorKey] of errors) {
104
- let match = errorsMap[field];
105
- if (match) {
106
- if (match instanceof Object) {
107
- if (maybeSchemaErrorKey) {
108
- match = match[maybeSchemaErrorKey[0]];
109
- }
110
- }
111
- matches.push(match);
112
- }
113
- }
114
- return matches;
115
- };
116
-
117
- // src/schemas/helpers.ts
118
- import { z } from "zod";
119
- var BoolOrStringSchema = z.union([z.string(), z.boolean()]).optional();
120
- var BooleanSchema = BoolOrStringSchema.transform((val) => {
121
- if (val === "true" || val === true) {
122
- return true;
123
- } else if (val === "false" || val === false) {
124
- return false;
125
- }
126
- throw new Error("Invalid value");
127
- });
128
- var AppwardenApiTokenSchema = z.string().refine((val) => !!val, { message: "appwardenApiToken is required" });
129
- var LockValue = z.object({
130
- isLocked: z.number(),
131
- isLockedTest: z.number(),
132
- lastCheck: z.number(),
133
- code: z.string()
134
- });
135
-
136
112
  export {
137
113
  buildLockPageUrl,
138
114
  isOnLockPage,
139
115
  TEMPORARY_REDIRECT_STATUS,
140
116
  createRedirect,
141
- getErrors,
142
- MemoryCache,
143
- BooleanSchema,
144
- AppwardenApiTokenSchema,
145
- LockValue
117
+ debug,
118
+ MemoryCache
146
119
  };
@@ -1,9 +1,7 @@
1
1
  import {
2
- getErrors
3
- } from "./chunk-ZX5QO4Y2.js";
4
- import {
2
+ getErrors,
5
3
  printMessage
6
- } from "./chunk-L5EQIJZB.js";
4
+ } from "./chunk-G4RRFNHY.js";
7
5
 
8
6
  // src/utils/validate-config.ts
9
7
  function validateConfig(config, schema) {
@@ -0,0 +1,53 @@
1
+ import {
2
+ UseCSPInputSchema,
3
+ isHTMLResponse
4
+ } from "./chunk-HCGLR3Z3.js";
5
+ import {
6
+ makeCSPHeader
7
+ } from "./chunk-G4RRFNHY.js";
8
+
9
+ // src/middlewares/use-content-security-policy.ts
10
+ var AppendAttribute = (attribute, nonce) => ({
11
+ element: function(element) {
12
+ element.setAttribute(attribute, nonce);
13
+ }
14
+ });
15
+ var useContentSecurityPolicy = (input) => {
16
+ const parsedInput = UseCSPInputSchema.safeParse(input);
17
+ if (!parsedInput.success) {
18
+ throw parsedInput.error;
19
+ }
20
+ const config = parsedInput.data;
21
+ return async (context, next) => {
22
+ await next();
23
+ const { response } = context;
24
+ if (
25
+ // if the csp is disabled
26
+ !["enforced", "report-only"].includes(config.mode)
27
+ ) {
28
+ context.debug("CSP is disabled");
29
+ return;
30
+ }
31
+ if (response.headers.has("Content-Type") && !isHTMLResponse(response)) {
32
+ return;
33
+ }
34
+ const cspNonce = crypto.randomUUID();
35
+ const [cspHeaderName, cspHeaderValue] = makeCSPHeader(
36
+ cspNonce,
37
+ config.directives,
38
+ config.mode
39
+ );
40
+ context.debug(
41
+ `Applying CSP in ${config.mode} mode`,
42
+ `Directives: ${config.directives ? Object.keys(config.directives).join(", ") : "none"}`
43
+ );
44
+ const nextResponse = new Response(response.body, response);
45
+ nextResponse.headers.set(cspHeaderName, cspHeaderValue);
46
+ nextResponse.headers.set("content-type", "text/html; charset=utf-8");
47
+ context.response = new HTMLRewriter().on("style", AppendAttribute("nonce", cspNonce)).on("script", AppendAttribute("nonce", cspNonce)).transform(nextResponse);
48
+ };
49
+ };
50
+
51
+ export {
52
+ useContentSecurityPolicy
53
+ };
@@ -1,5 +1,7 @@
1
1
  import { Runtime } from '@astrojs/cloudflare';
2
2
  import { APIContext, MiddlewareHandler } from 'astro';
3
+ import { U as UseCSPInput } from '../use-content-security-policy-DUYpyUPy.js';
4
+ import 'zod';
3
5
 
4
6
  /**
5
7
  * Cloudflare runtime context provided by Astro on Cloudflare Workers.
@@ -20,6 +22,8 @@ interface AstroAppwardenConfig {
20
22
  appwardenApiHostname?: string;
21
23
  /** Enable debug logging */
22
24
  debug?: boolean;
25
+ /** Optional Content Security Policy configuration */
26
+ contentSecurityPolicy?: UseCSPInput;
23
27
  }
24
28
  /**
25
29
  * Configuration function that receives the Cloudflare runtime and returns the config.