@appwarden/middleware 1.5.1 → 2.0.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,6 +1,6 @@
1
1
  # @appwarden/middleware
2
2
 
3
- ![Test Coverage](https://img.shields.io/badge/coverage-97%25-brightgreen)
3
+ ![Test Coverage](https://img.shields.io/badge/coverage-95.82%25-brightgreen)
4
4
  [![npm version](https://img.shields.io/npm/v/@appwarden/middleware.svg)](https://www.npmjs.com/package/@appwarden/middleware)
5
5
  [![npm provenance](https://img.shields.io/badge/npm-provenance-green)](https://docs.npmjs.com/generating-provenance-statements)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -13,7 +13,7 @@
13
13
 
14
14
  - **Instant Quarantine**: Immediately redirects all visitors to a maintenance page when activated
15
15
  - **Discord Integration**: Trigger lockdowns via Discord commands (`/quarantine lock your.app.io`)
16
- - **Nonce-based Content Security Policy**: On Cloudflare, deploy a nonce-based Content Security Policy to supercharge your website security
16
+ - **Nonce-based Content Security Policy (Cloudflare only)**: Deploy a nonce-based Content Security Policy to supercharge your website security
17
17
  - **Minimal Runtime Overhead**: Negligible performance impact by using `event.waitUntil` for status checks
18
18
 
19
19
  ## Installation
@@ -22,57 +22,26 @@ Compatible with websites powered by [Cloudflare](https://developers.cloudflare.c
22
22
 
23
23
  For detailed usage instructions, please refer to our [documentation](https://appwarden.io/docs).
24
24
 
25
- ### Cloudflare
26
-
27
- We recommend using the [`@appwarden/build-cloudflare-action`](https://github.com/appwarden/build-cloudflare-action) Github Action to deploy automatically on Cloudflare.
28
-
29
- > Read the docs [to get started](https://appwarden.io/docs/guides/cloudflare-integration)
30
-
31
- ```typescript
32
- import {
33
- withAppwarden,
34
- useContentSecurityPolicy,
35
- } from "@appwarden/middleware/cloudflare"
36
-
37
- export default {
38
- fetch: withAppwarden((context) => ({
39
- debug: context.env.DEBUG,
40
- lockPageSlug: context.env.LOCK_PAGE_SLUG,
41
- appwardenApiToken: context.env.APPWARDEN_API_TOKEN,
42
- middleware: {
43
- before: [
44
- useContentSecurityPolicy({
45
- mode: "enforced",
46
- directives: {
47
- "script-src": ["self", "{{nonce}}"],
48
- "style-src": ["self", "{{nonce}}"],
49
- },
50
- }),
51
- ],
52
- },
53
- })),
54
- }
55
- ```
25
+ ## Supported Frameworks
56
26
 
57
- ### Vercel
27
+ ### On Cloudflare
58
28
 
59
- > Read the docs [to get started](https://appwarden.io/docs/guides/vercel-integration)
29
+ Cloudflare has two deployment options: [pages.dev](https://pages.dev) and [Workers static assets](https://developers.cloudflare.com/workers/static-assets/). We support both.
60
30
 
61
- ```typescript
62
- import { withAppwarden } from "@appwarden/middleware/vercel"
31
+ #### On pages.dev
63
32
 
64
- export default withAppwarden({
65
- cacheUrl: process.env.EDGE_CONFIG_URL || process.env.UPSTASH_URL,
66
- appwardenApiToken: process.env.APPWARDEN_API_TOKEN,
67
- vercelApiToken: process.env.VERCEL_API_TOKEN,
68
- lockPageSlug: "/maintenance",
69
- })
33
+ - [All websites on pages.dev](https://appwarden.io/docs/guides/cloudflare-integration)
70
34
 
71
- // Configures middleware to match all routes
72
- export const config = {
73
- matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
74
- }
75
- ```
35
+ #### On Workers static assets
36
+
37
+ - [Astro](https://appwarden.io/docs/guides/astro-cloudflare)
38
+ - [React Router](https://appwarden.io/docs/guides/react-router-cloudflare)
39
+ - [TanStack Start](https://appwarden.io/docs/guides/tanstack-start-cloudflare)
40
+ - [Next.js](https://appwarden.io/docs/guides/nextjs-cloudflare)
41
+
42
+ ### On Vercel
43
+
44
+ - [All websites on Vercel](https://appwarden.io/docs/guides/vercel-integration)
76
45
 
77
46
  ## Contributing
78
47
 
@@ -0,0 +1,232 @@
1
+ import {
2
+ LockValue,
3
+ MemoryCache
4
+ } from "./chunk-B5IE7V77.js";
5
+ import {
6
+ APPWARDEN_CACHE_KEY,
7
+ APPWARDEN_TEST_ROUTE,
8
+ debug,
9
+ printMessage
10
+ } from "./chunk-7UTT3M2S.js";
11
+
12
+ // src/utils/cloudflare/cloudflare-cache.ts
13
+ var store = {
14
+ json: (context, cacheKey, options) => {
15
+ const cacheKeyUrl = new URL(cacheKey, context.serviceOrigin);
16
+ return {
17
+ getValue: () => getCacheValue(context, cacheKeyUrl),
18
+ updateValue: (json) => updateCacheValue(context, cacheKeyUrl, json, options?.ttl),
19
+ deleteValue: () => clearCache(context, cacheKeyUrl)
20
+ };
21
+ }
22
+ };
23
+ var getCacheValue = async (context, cacheKey) => {
24
+ const match = await context.cache.match(cacheKey);
25
+ if (!match) {
26
+ debug(`[${cacheKey.pathname}] Cache MISS!`);
27
+ return void 0;
28
+ }
29
+ debug(`[${cacheKey.pathname}] Cache MATCH!`);
30
+ return match;
31
+ };
32
+ var updateCacheValue = async (context, cacheKey, value, ttl) => {
33
+ debug(
34
+ "updating cache...",
35
+ cacheKey.href,
36
+ value,
37
+ ttl ? `expires in ${ttl}s` : ""
38
+ );
39
+ await context.cache.put(
40
+ cacheKey,
41
+ new Response(JSON.stringify(value), {
42
+ headers: {
43
+ "content-type": "application/json",
44
+ ...ttl && {
45
+ "cache-control": `max-age=${ttl}`
46
+ }
47
+ }
48
+ })
49
+ );
50
+ };
51
+ var clearCache = (context, cacheKey) => context.cache.delete(cacheKey);
52
+
53
+ // src/utils/cloudflare/delete-edge-value.ts
54
+ var deleteEdgeValue = async (context) => {
55
+ try {
56
+ switch (context.provider) {
57
+ case "cloudflare-cache": {
58
+ const success = await context.edgeCache.deleteValue();
59
+ if (!success) {
60
+ throw new Error();
61
+ }
62
+ break;
63
+ }
64
+ default:
65
+ throw new Error(`Unsupported provider: ${context.provider}`);
66
+ }
67
+ } catch (e) {
68
+ const message = "Failed to delete edge value";
69
+ console.error(
70
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
71
+ );
72
+ }
73
+ };
74
+
75
+ // src/utils/cloudflare/get-lock-value.ts
76
+ var getLockValue = async (context) => {
77
+ try {
78
+ let shouldDeleteEdgeValue = false;
79
+ let cacheResponse, lockValue = {
80
+ isLocked: 0,
81
+ isLockedTest: 0,
82
+ lastCheck: Date.now(),
83
+ code: ""
84
+ };
85
+ switch (context.provider) {
86
+ case "cloudflare-cache": {
87
+ cacheResponse = await context.edgeCache.getValue();
88
+ break;
89
+ }
90
+ default:
91
+ throw new Error(`Unsupported provider: ${context.provider}`);
92
+ }
93
+ if (!cacheResponse) {
94
+ return { lockValue: void 0 };
95
+ }
96
+ try {
97
+ const clonedResponse = cacheResponse?.clone();
98
+ lockValue = LockValue.parse(
99
+ clonedResponse ? await clonedResponse.json() : void 0
100
+ );
101
+ } catch (error) {
102
+ console.error(
103
+ printMessage(
104
+ `Failed to parse ${context.keyName} from edge cache - ${error}`
105
+ )
106
+ );
107
+ shouldDeleteEdgeValue = true;
108
+ }
109
+ return { lockValue, shouldDeleteEdgeValue };
110
+ } catch (e) {
111
+ const message = "Failed to retrieve edge value";
112
+ console.error(
113
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
114
+ );
115
+ return { lockValue: void 0 };
116
+ }
117
+ };
118
+
119
+ // src/utils/cloudflare/sync-edge-value.ts
120
+ var APIError = class extends Error {
121
+ constructor(message) {
122
+ super(message);
123
+ this.name = "APIError";
124
+ }
125
+ };
126
+ var DEFAULT_API_HOSTNAME = "https://api.appwarden.io";
127
+ var syncEdgeValue = async (context) => {
128
+ debug(`syncing with api`);
129
+ try {
130
+ const apiHostname = context.appwardenApiHostname ?? DEFAULT_API_HOSTNAME;
131
+ const response = await fetch(new URL("/v1/status/check", apiHostname), {
132
+ method: "POST",
133
+ headers: { "content-type": "application/json" },
134
+ body: JSON.stringify({
135
+ service: "cloudflare",
136
+ provider: context.provider,
137
+ fqdn: context.requestUrl.hostname,
138
+ appwardenApiToken: context.appwardenApiToken
139
+ })
140
+ });
141
+ if (response.status !== 200) {
142
+ throw new Error(`${response.status} ${response.statusText}`);
143
+ }
144
+ if (response.headers.get("content-type")?.includes("application/json")) {
145
+ const result = await response.json();
146
+ if (result.error) {
147
+ throw new APIError(result.error.message);
148
+ }
149
+ if (!result.content) {
150
+ throw new APIError("no content from api");
151
+ }
152
+ try {
153
+ const parsedValue = LockValue.omit({ lastCheck: true }).parse(
154
+ result.content
155
+ );
156
+ debug(`syncing with api...DONE ${JSON.stringify(parsedValue, null, 2)}`);
157
+ await context.edgeCache.updateValue({
158
+ ...parsedValue,
159
+ lastCheck: Date.now()
160
+ });
161
+ } catch (error) {
162
+ throw new APIError(`Failed to parse check endpoint result - ${error}`);
163
+ }
164
+ }
165
+ } catch (e) {
166
+ const message = "Failed to fetch from check endpoint";
167
+ console.error(
168
+ printMessage(
169
+ e instanceof APIError ? e.message : e instanceof Error ? `${message} - ${e.message}` : message
170
+ )
171
+ );
172
+ }
173
+ };
174
+
175
+ // src/core/check-lock-status.ts
176
+ var createContext = async (config) => {
177
+ const requestUrl = new URL(config.request.url);
178
+ const keyName = APPWARDEN_CACHE_KEY;
179
+ const provider = "cloudflare-cache";
180
+ const edgeCache = store.json(
181
+ {
182
+ serviceOrigin: requestUrl.origin,
183
+ cache: await caches.open("appwarden:lock")
184
+ },
185
+ keyName
186
+ );
187
+ return {
188
+ keyName,
189
+ request: config.request,
190
+ edgeCache,
191
+ requestUrl,
192
+ provider,
193
+ debug: config.debug ?? false,
194
+ lockPageSlug: config.lockPageSlug,
195
+ appwardenApiToken: config.appwardenApiToken,
196
+ appwardenApiHostname: config.appwardenApiHostname,
197
+ waitUntil: config.waitUntil
198
+ };
199
+ };
200
+ var resolveLockStatus = async (context) => {
201
+ const { lockValue, shouldDeleteEdgeValue } = await getLockValue(context);
202
+ if (shouldDeleteEdgeValue) {
203
+ await deleteEdgeValue(context);
204
+ }
205
+ const isTestRoute = context.requestUrl.pathname === APPWARDEN_TEST_ROUTE;
206
+ const isTestLock = isTestRoute && !MemoryCache.isTestExpired(lockValue) && !!lockValue;
207
+ return {
208
+ isLocked: !!lockValue?.isLocked || isTestLock,
209
+ isTestLock,
210
+ lockValue,
211
+ wasDeleted: shouldDeleteEdgeValue ?? false
212
+ };
213
+ };
214
+ var checkLockStatus = async (config) => {
215
+ const context = await createContext(config);
216
+ let { isLocked, isTestLock, lockValue, wasDeleted } = await resolveLockStatus(context);
217
+ if (MemoryCache.isExpired(lockValue) || wasDeleted) {
218
+ if (!lockValue || wasDeleted || lockValue.isLocked) {
219
+ await syncEdgeValue(context);
220
+ ({ isLocked, isTestLock } = await resolveLockStatus(context));
221
+ } else {
222
+ config.waitUntil(syncEdgeValue(context));
223
+ }
224
+ }
225
+ return { isLocked, isTestLock };
226
+ };
227
+
228
+ export {
229
+ store,
230
+ getLockValue,
231
+ checkLockStatus
232
+ };
@@ -0,0 +1,27 @@
1
+ import {
2
+ getErrors
3
+ } from "./chunk-B5IE7V77.js";
4
+ import {
5
+ printMessage
6
+ } from "./chunk-7UTT3M2S.js";
7
+
8
+ // src/utils/validate-config.ts
9
+ function validateConfig(config, schema) {
10
+ const result = schema.safeParse(config);
11
+ const hasErrors = !result.success;
12
+ if (hasErrors) {
13
+ const mappedErrors = getErrors(result.error);
14
+ if (mappedErrors.length > 0) {
15
+ for (const error of mappedErrors) {
16
+ console.error(printMessage(error));
17
+ }
18
+ } else {
19
+ console.error(printMessage(result.error.message));
20
+ }
21
+ }
22
+ return hasErrors;
23
+ }
24
+
25
+ export {
26
+ validateConfig
27
+ };
@@ -3,7 +3,6 @@ var LOCKDOWN_TEST_EXPIRY_MS = 5 * 60 * 1e3;
3
3
  var errors = { badCacheConnection: "BAD_CACHE_CONNECTION" };
4
4
  var globalErrors = [errors.badCacheConnection];
5
5
  var APPWARDEN_TEST_ROUTE = "/_appwarden/test";
6
- var APPWARDEN_USER_AGENT = "Appwarden-Monitor";
7
6
  var APPWARDEN_CACHE_KEY = "appwarden-lock";
8
7
 
9
8
  // src/utils/debug.ts
@@ -17,13 +16,22 @@ var debug = (...msg) => {
17
16
  var addSlashes = (str) => str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\u0000/g, "\\0").replace(/<\/script>/gi, "<\\/script>");
18
17
  var printMessage = (message) => `[@appwarden/middleware] ${addSlashes(message)}`;
19
18
 
19
+ // src/utils/request-checks.ts
20
+ function isHTMLResponse(response) {
21
+ return response.headers.get("Content-Type")?.includes("text/html") ?? false;
22
+ }
23
+ function isHTMLRequest(request) {
24
+ return request.headers.get("accept")?.includes("text/html") ?? false;
25
+ }
26
+
20
27
  export {
21
28
  LOCKDOWN_TEST_EXPIRY_MS,
22
29
  errors,
23
30
  globalErrors,
24
31
  APPWARDEN_TEST_ROUTE,
25
- APPWARDEN_USER_AGENT,
26
32
  APPWARDEN_CACHE_KEY,
27
33
  debug,
28
- printMessage
34
+ printMessage,
35
+ isHTMLResponse,
36
+ isHTMLRequest
29
37
  };
@@ -1,39 +1,6 @@
1
1
  import {
2
2
  LOCKDOWN_TEST_EXPIRY_MS
3
- } from "./chunk-FDIKUQ3E.js";
4
-
5
- // src/utils/errors.ts
6
- var errorsMap = {
7
- mode: '`CSP_MODE` must be one of "disabled", "report-only", or "enforced"',
8
- directives: {
9
- ["DirectivesRequired" /* DirectivesRequired */]: '`CSP_DIRECTIVES` must be provided when `CSP_MODE` is "report-only" or "enforced"',
10
- ["DirectivesBadParse" /* DirectivesBadParse */]: "Failed to parse `CSP_DIRECTIVES`. Is it a valid JSON string?"
11
- },
12
- appwardenApiToken: "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/guides/api-token-management."
13
- };
14
- var getErrors = (error) => {
15
- const matches = [];
16
- const errors = [...Object.entries(error.flatten().fieldErrors)];
17
- for (const issue of error.issues) {
18
- errors.push(
19
- ...Object.entries(
20
- "returnTypeError" in issue ? issue.returnTypeError.flatten().fieldErrors : {}
21
- )
22
- );
23
- }
24
- for (const [field, maybeSchemaErrorKey] of errors) {
25
- let match = errorsMap[field];
26
- if (match) {
27
- if (match instanceof Object) {
28
- if (maybeSchemaErrorKey) {
29
- match = match[maybeSchemaErrorKey[0]];
30
- }
31
- }
32
- matches.push(match);
33
- }
34
- }
35
- return matches;
36
- };
3
+ } from "./chunk-7UTT3M2S.js";
37
4
 
38
5
  // src/utils/memory-cache.ts
39
6
  var MemoryCache = class {
@@ -82,6 +49,39 @@ var MemoryCache = class {
82
49
  };
83
50
  };
84
51
 
52
+ // src/utils/errors.ts
53
+ var errorsMap = {
54
+ mode: '`CSP_MODE` must be one of "disabled", "report-only", or "enforced"',
55
+ directives: {
56
+ ["DirectivesRequired" /* DirectivesRequired */]: '`CSP_DIRECTIVES` must be provided when `CSP_MODE` is "report-only" or "enforced"',
57
+ ["DirectivesBadParse" /* DirectivesBadParse */]: "Failed to parse `CSP_DIRECTIVES`. Is it a valid JSON string?"
58
+ },
59
+ appwardenApiToken: "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/guides/api-token-management."
60
+ };
61
+ var getErrors = (error) => {
62
+ const matches = [];
63
+ const errors = [...Object.entries(error.flatten().fieldErrors)];
64
+ for (const issue of error.issues) {
65
+ errors.push(
66
+ ...Object.entries(
67
+ "returnTypeError" in issue ? issue.returnTypeError.flatten().fieldErrors : {}
68
+ )
69
+ );
70
+ }
71
+ for (const [field, maybeSchemaErrorKey] of errors) {
72
+ let match = errorsMap[field];
73
+ if (match) {
74
+ if (match instanceof Object) {
75
+ if (maybeSchemaErrorKey) {
76
+ match = match[maybeSchemaErrorKey[0]];
77
+ }
78
+ }
79
+ matches.push(match);
80
+ }
81
+ }
82
+ return matches;
83
+ };
84
+
85
85
  // src/schemas/helpers.ts
86
86
  import { z } from "zod";
87
87
  var BoolOrStringSchema = z.union([z.string(), z.boolean()]).optional();
@@ -93,6 +93,7 @@ var BooleanSchema = BoolOrStringSchema.transform((val) => {
93
93
  }
94
94
  throw new Error("Invalid value");
95
95
  });
96
+ var AppwardenApiTokenSchema = z.string().refine((val) => !!val, { message: "appwardenApiToken is required" });
96
97
  var LockValue = z.object({
97
98
  isLocked: z.number(),
98
99
  isLockedTest: z.number(),
@@ -104,5 +105,6 @@ export {
104
105
  getErrors,
105
106
  MemoryCache,
106
107
  BooleanSchema,
108
+ AppwardenApiTokenSchema,
107
109
  LockValue
108
110
  };
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  debug,
3
+ isHTMLResponse,
3
4
  printMessage
4
- } from "./chunk-FDIKUQ3E.js";
5
+ } from "./chunk-7UTT3M2S.js";
5
6
 
6
7
  // src/schemas/use-content-security-policy.ts
7
8
  import { z as z2 } from "zod";
@@ -126,7 +127,7 @@ var useContentSecurityPolicy = (input) => {
126
127
  debug(printMessage("csp is disabled"));
127
128
  return;
128
129
  }
129
- if (response.headers.has("Content-Type") && !response.headers.get("Content-Type")?.includes("text/html")) {
130
+ if (response.headers.has("Content-Type") && !isHTMLResponse(response)) {
130
131
  return;
131
132
  }
132
133
  const cspNonce = crypto.randomUUID();
@@ -0,0 +1,22 @@
1
+ // src/utils/build-lock-page-url.ts
2
+ function buildLockPageUrl(lockPageSlug, requestUrl) {
3
+ const normalizedSlug = lockPageSlug.startsWith("/") ? lockPageSlug : `/${lockPageSlug}`;
4
+ return new URL(normalizedSlug, requestUrl);
5
+ }
6
+
7
+ // src/utils/create-redirect.ts
8
+ var TEMPORARY_REDIRECT_STATUS = 302;
9
+ var createRedirect = (url) => {
10
+ return new Response(null, {
11
+ status: TEMPORARY_REDIRECT_STATUS,
12
+ headers: {
13
+ Location: url.toString()
14
+ }
15
+ });
16
+ };
17
+
18
+ export {
19
+ buildLockPageUrl,
20
+ TEMPORARY_REDIRECT_STATUS,
21
+ createRedirect
22
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Cloudflare runtime context provided by Astro on Cloudflare Workers.
3
+ * This is the shape of `context.locals.runtime` when using @astrojs/cloudflare adapter.
4
+ */
5
+ interface AstroCloudflareRuntime {
6
+ env: CloudflareEnv;
7
+ ctx: ExecutionContext;
8
+ }
9
+ /**
10
+ * Configuration for the Appwarden middleware.
11
+ */
12
+ interface AstroAppwardenConfig {
13
+ /** The slug/path of the lock page to redirect to when the site is locked */
14
+ lockPageSlug: string;
15
+ /** The Appwarden API token for authentication */
16
+ appwardenApiToken: string;
17
+ /** Optional custom API hostname (defaults to https://api.appwarden.io) */
18
+ appwardenApiHostname?: string;
19
+ /** Enable debug logging */
20
+ debug?: boolean;
21
+ }
22
+ /**
23
+ * Configuration function that receives the Cloudflare runtime and returns the config.
24
+ * This allows dynamic configuration based on environment variables.
25
+ */
26
+ type AstroConfigFn = (runtime: AstroCloudflareRuntime) => AstroAppwardenConfig;
27
+ /**
28
+ * Astro middleware context type.
29
+ * This matches Astro's APIContext shape for middleware.
30
+ */
31
+ interface AstroMiddlewareContext {
32
+ /** The incoming request */
33
+ request: Request;
34
+ /** Object for storing request-specific data */
35
+ locals: {
36
+ runtime?: AstroCloudflareRuntime;
37
+ [key: string]: unknown;
38
+ };
39
+ /** Helper to create redirect responses */
40
+ redirect: (path: string, status?: number) => Response;
41
+ }
42
+ /**
43
+ * Astro middleware function signature.
44
+ * This matches the onRequest export type in Astro's middleware system.
45
+ */
46
+ type AstroMiddlewareFunction = (context: AstroMiddlewareContext, next: () => Promise<Response>) => Promise<Response>;
47
+ /**
48
+ * Creates an Appwarden middleware function for Astro.
49
+ *
50
+ * This middleware checks if the site is locked and redirects to the lock page if so.
51
+ * It should be used with Astro's `sequence()` function or exported directly as `onRequest`.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * // src/middleware.ts
56
+ * import { sequence } from "astro:middleware"
57
+ * import { createAppwardenMiddleware } from "@appwarden/middleware/astro"
58
+ *
59
+ * const appwarden = createAppwardenMiddleware(({ env }) => ({
60
+ * lockPageSlug: env.APPWARDEN_LOCK_PAGE_SLUG,
61
+ * appwardenApiToken: env.APPWARDEN_API_TOKEN,
62
+ * }))
63
+ *
64
+ * export const onRequest = sequence(appwarden)
65
+ * ```
66
+ *
67
+ * @param configFn - A function that receives the Cloudflare runtime and returns the config
68
+ * @returns An Astro middleware function
69
+ */
70
+ declare function createAppwardenMiddleware(configFn: AstroConfigFn): AstroMiddlewareFunction;
71
+
72
+ export { type AstroAppwardenConfig, type AstroCloudflareRuntime, type AstroConfigFn, type AstroMiddlewareContext, type AstroMiddlewareFunction, createAppwardenMiddleware };
@@ -0,0 +1,90 @@
1
+ import {
2
+ TEMPORARY_REDIRECT_STATUS,
3
+ buildLockPageUrl,
4
+ createRedirect
5
+ } from "../chunk-N6AUTMZO.js";
6
+ import {
7
+ validateConfig
8
+ } from "../chunk-6PUA5YXP.js";
9
+ import {
10
+ checkLockStatus
11
+ } from "../chunk-5DEXVBY6.js";
12
+ import {
13
+ AppwardenApiTokenSchema,
14
+ BooleanSchema
15
+ } from "../chunk-B5IE7V77.js";
16
+ import {
17
+ isHTMLRequest,
18
+ printMessage
19
+ } from "../chunk-7UTT3M2S.js";
20
+
21
+ // src/schemas/astro-cloudflare.ts
22
+ import { z } from "zod";
23
+ var AstroCloudflareConfigSchema = z.object({
24
+ /** The slug/path of the lock page to redirect to when the site is locked */
25
+ lockPageSlug: z.string(),
26
+ /** The Appwarden API token for authentication */
27
+ appwardenApiToken: AppwardenApiTokenSchema,
28
+ /** Optional custom API hostname (defaults to https://api.appwarden.io) */
29
+ appwardenApiHostname: z.string().optional(),
30
+ /** Enable debug logging */
31
+ debug: BooleanSchema.default(false)
32
+ });
33
+
34
+ // src/adapters/astro-cloudflare.ts
35
+ function createAppwardenMiddleware(configFn) {
36
+ return async (context, next) => {
37
+ const { request, locals } = context;
38
+ try {
39
+ const runtime = locals.runtime;
40
+ if (!runtime) {
41
+ console.error(
42
+ printMessage(
43
+ "Cloudflare runtime not found. Ensure @astrojs/cloudflare adapter is configured."
44
+ )
45
+ );
46
+ return next();
47
+ }
48
+ if (!isHTMLRequest(request)) {
49
+ return next();
50
+ }
51
+ const config = configFn(runtime);
52
+ const hasError = validateConfig(config, AstroCloudflareConfigSchema);
53
+ if (hasError) {
54
+ return next();
55
+ }
56
+ const result = await checkLockStatus({
57
+ request,
58
+ appwardenApiToken: config.appwardenApiToken,
59
+ appwardenApiHostname: config.appwardenApiHostname,
60
+ debug: config.debug,
61
+ lockPageSlug: config.lockPageSlug,
62
+ waitUntil: (fn) => runtime.ctx.waitUntil(fn)
63
+ });
64
+ if (result.isLocked) {
65
+ const lockPageUrl = buildLockPageUrl(config.lockPageSlug, request.url);
66
+ if (context.redirect) {
67
+ return context.redirect(
68
+ lockPageUrl.toString(),
69
+ TEMPORARY_REDIRECT_STATUS
70
+ );
71
+ }
72
+ return createRedirect(lockPageUrl);
73
+ }
74
+ return next();
75
+ } catch (error) {
76
+ if (error instanceof Response) {
77
+ throw error;
78
+ }
79
+ console.error(
80
+ printMessage(
81
+ `Unhandled error: ${error instanceof Error ? error.message : String(error)}`
82
+ )
83
+ );
84
+ return next();
85
+ }
86
+ };
87
+ }
88
+ export {
89
+ createAppwardenMiddleware
90
+ };