@appwarden/middleware 1.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/cloudflare.js ADDED
@@ -0,0 +1,389 @@
1
+ import {
2
+ useContentSecurityPolicy
3
+ } from "./chunk-NZNMFDZ7.js";
4
+ import {
5
+ BooleanSchema,
6
+ LockValue,
7
+ MemoryCache,
8
+ getErrors
9
+ } from "./chunk-47MLTBFC.js";
10
+ import {
11
+ APPWARDEN_CACHE_KEY,
12
+ APPWARDEN_TEST_ROUTE,
13
+ APPWARDEN_USER_AGENT,
14
+ debug,
15
+ printMessage
16
+ } from "./chunk-JWUAFJ2E.js";
17
+
18
+ // src/runners/appwarden-on-cloudflare.ts
19
+ import { ZodError } from "zod";
20
+
21
+ // src/schemas/cloudflare.ts
22
+ import { z as z2 } from "zod";
23
+
24
+ // src/schemas/use-appwarden.ts
25
+ import { z } from "zod";
26
+ var UseAppwardenInputSchema = z.object({
27
+ debug: BooleanSchema.default(false),
28
+ lockPageSlug: z.string(),
29
+ appwardenApiToken: z.string().refine((val) => !!val, { path: ["appwardenApiToken"] })
30
+ });
31
+
32
+ // src/schemas/cloudflare.ts
33
+ var ConfigFnInputSchema = z2.function().args(z2.custom()).returns(
34
+ UseAppwardenInputSchema.extend({
35
+ middleware: z2.object({ before: z2.custom().array().default([]) }).default({})
36
+ })
37
+ );
38
+
39
+ // src/utils/middleware.ts
40
+ var usePipeline = (...initMiddlewares) => {
41
+ const stack = [...initMiddlewares];
42
+ const execute = async (context) => {
43
+ const runner = async (prevIndex, index) => {
44
+ if (index === prevIndex) {
45
+ throw new Error("next() called multiple times");
46
+ }
47
+ if (index >= stack.length) {
48
+ return;
49
+ }
50
+ const middleware = stack[index];
51
+ const next = async () => runner(index, index + 1);
52
+ await middleware(context, next);
53
+ };
54
+ await runner(-1, 0);
55
+ };
56
+ return {
57
+ execute
58
+ };
59
+ };
60
+
61
+ // src/utils/render-lock-page.ts
62
+ var renderLockPage = (context) => fetch(new URL(context.lockPageSlug, context.requestUrl.origin), {
63
+ headers: {
64
+ // no browser caching, otherwise we need to hard refresh to disable lock screen
65
+ "Cache-Control": "no-store"
66
+ }
67
+ });
68
+
69
+ // src/utils/cloudflare/cloudflare-cache.ts
70
+ var store = {
71
+ json: (context, cacheKey, options) => {
72
+ const cacheKeyUrl = new URL(cacheKey, context.serviceOrigin);
73
+ return {
74
+ getValue: () => getCacheValue(context, cacheKeyUrl),
75
+ updateValue: (json) => updateCacheValue(context, cacheKeyUrl, json, options?.ttl),
76
+ deleteValue: () => clearCache(context, cacheKeyUrl)
77
+ };
78
+ }
79
+ };
80
+ var getCacheValue = async (context, cacheKey) => {
81
+ const match = await context.cache.match(cacheKey);
82
+ if (!match) {
83
+ debug(`[${cacheKey.pathname}] Cache MISS!`);
84
+ return void 0;
85
+ }
86
+ debug(`[${cacheKey.pathname}] Cache MATCH!`);
87
+ return match;
88
+ };
89
+ var updateCacheValue = async (context, cacheKey, value, ttl) => {
90
+ debug(
91
+ "updating cache...",
92
+ cacheKey.href,
93
+ value,
94
+ ttl ? `expires in ${ttl}s` : ""
95
+ );
96
+ await context.cache.put(
97
+ cacheKey,
98
+ new Response(JSON.stringify(value), {
99
+ headers: {
100
+ "content-type": "application/json",
101
+ ...ttl && {
102
+ "cache-control": `max-age=${ttl}`
103
+ }
104
+ }
105
+ })
106
+ );
107
+ };
108
+ var clearCache = (context, cacheKey) => context.cache.delete(cacheKey);
109
+
110
+ // src/utils/cloudflare/delete-edge-value.ts
111
+ var deleteEdgeValue = async (context) => {
112
+ try {
113
+ switch (context.provider) {
114
+ case "cloudflare-cache": {
115
+ const success = await context.edgeCache.deleteValue();
116
+ if (!success) {
117
+ throw new Error();
118
+ }
119
+ break;
120
+ }
121
+ default:
122
+ throw new Error(`Unsupported provider: ${context.provider}`);
123
+ }
124
+ } catch (e) {
125
+ const message = "Failed to delete edge value";
126
+ console.error(
127
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
128
+ );
129
+ }
130
+ };
131
+
132
+ // src/utils/cloudflare/get-lock-value.ts
133
+ var getLockValue = async (context) => {
134
+ try {
135
+ let shouldDeleteEdgeValue = false;
136
+ let cacheResponse, lockValue = {
137
+ isLocked: 0,
138
+ isLockedTest: 0,
139
+ lastCheck: Date.now(),
140
+ code: ""
141
+ };
142
+ switch (context.provider) {
143
+ case "cloudflare-cache": {
144
+ cacheResponse = await context.edgeCache.getValue();
145
+ break;
146
+ }
147
+ default:
148
+ throw new Error(`Unsupported provider: ${context.provider}`);
149
+ }
150
+ if (!cacheResponse) {
151
+ return { lockValue: void 0 };
152
+ }
153
+ try {
154
+ const clonedResponse = cacheResponse?.clone();
155
+ lockValue = LockValue.parse(
156
+ clonedResponse ? await clonedResponse.json() : void 0
157
+ );
158
+ } catch (error) {
159
+ console.error(
160
+ printMessage(
161
+ `Failed to parse ${context.keyName} from edge cache - ${error}`
162
+ )
163
+ );
164
+ shouldDeleteEdgeValue = true;
165
+ }
166
+ return { lockValue, shouldDeleteEdgeValue };
167
+ } catch (e) {
168
+ const message = "Failed to retrieve edge value";
169
+ console.error(
170
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
171
+ );
172
+ return { lockValue: void 0 };
173
+ }
174
+ };
175
+
176
+ // src/utils/cloudflare/insert-errors-logs.ts
177
+ var insertErrorLogs = async (context, error) => {
178
+ const errors = getErrors(error);
179
+ for (const err of errors) {
180
+ console.log(printMessage(err));
181
+ }
182
+ return new HTMLRewriter().on("body", {
183
+ element: (elem) => {
184
+ elem.append(
185
+ `<script>
186
+ ${errors.map((err) => `console.error(\`${printMessage(err)}\`)`).join("\n")}
187
+ </script>`,
188
+ { html: true }
189
+ );
190
+ }
191
+ }).transform(await fetch(context.request));
192
+ };
193
+
194
+ // src/utils/cloudflare/sync-edge-value.ts
195
+ var APIError = class extends Error {
196
+ constructor(message) {
197
+ super(message);
198
+ this.name = "APIError";
199
+ }
200
+ };
201
+ var syncEdgeValue = async (context) => {
202
+ debug(`syncing with api`);
203
+ try {
204
+ const response = await fetch(new URL("/v1/status/check", "https://bot-gateway.appwarden.io"), {
205
+ method: "POST",
206
+ headers: { "content-type": "application/json" },
207
+ body: JSON.stringify({
208
+ service: "cloudflare",
209
+ provider: context.provider,
210
+ fqdn: context.requestUrl.hostname,
211
+ appwardenApiToken: context.appwardenApiToken
212
+ })
213
+ });
214
+ if (response.status !== 200) {
215
+ throw new Error(`${response.status} ${response.statusText}`);
216
+ }
217
+ if (response.headers.get("content-type")?.includes("application/json")) {
218
+ const result = await response.json();
219
+ if (result.error) {
220
+ throw new APIError(result.error.message);
221
+ }
222
+ if (!result.content) {
223
+ throw new APIError("no content from api");
224
+ }
225
+ try {
226
+ const parsedValue = LockValue.omit({ lastCheck: true }).parse(
227
+ result.content
228
+ );
229
+ debug(`syncing with api...DONE ${JSON.stringify(parsedValue, null, 2)}`);
230
+ await context.edgeCache.updateValue({
231
+ ...parsedValue,
232
+ lastCheck: Date.now()
233
+ });
234
+ } catch (error) {
235
+ throw new APIError(`Failed to parse check endpoint result - ${error}`);
236
+ }
237
+ }
238
+ } catch (e) {
239
+ const message = "Failed to fetch from check endpoint";
240
+ console.error(
241
+ printMessage(
242
+ e instanceof APIError ? e.message : e instanceof Error ? `${message} - ${e.message}` : message
243
+ )
244
+ );
245
+ }
246
+ };
247
+
248
+ // src/handlers/maybe-quarantine.ts
249
+ var resolveLockValue = async (context, options) => {
250
+ const { lockValue, shouldDeleteEdgeValue } = await getLockValue(context);
251
+ if (shouldDeleteEdgeValue) {
252
+ await deleteEdgeValue(context);
253
+ }
254
+ if (lockValue?.isLocked || context.requestUrl.pathname === APPWARDEN_TEST_ROUTE && !MemoryCache.isTestExpired(lockValue)) {
255
+ await options.onLocked();
256
+ }
257
+ return lockValue;
258
+ };
259
+ var maybeQuarantine = async (context, options) => {
260
+ const cachedLockValue = await resolveLockValue(context, {
261
+ onLocked: options.onLocked
262
+ });
263
+ const shouldRecheck = MemoryCache.isExpired(cachedLockValue);
264
+ if (shouldRecheck) {
265
+ if (!cachedLockValue || cachedLockValue.isLocked) {
266
+ await syncEdgeValue(context);
267
+ await resolveLockValue(context, {
268
+ onLocked: options.onLocked
269
+ });
270
+ } else {
271
+ context.waitUntil(syncEdgeValue(context));
272
+ }
273
+ }
274
+ };
275
+
276
+ // src/handlers/reset-cache.ts
277
+ var isResetCacheRequest = (request) => request.method === "POST" && new URL(request.url).pathname === "/__appwarden/reset-cache" && request.headers.get("content-type") === "application/json";
278
+ var handleResetCache = async (keyName, provider, edgeCache, request) => {
279
+ const { lockValue } = await getLockValue({
280
+ keyName,
281
+ provider,
282
+ edgeCache
283
+ });
284
+ try {
285
+ const body = await request.clone().json();
286
+ if (body.code === lockValue?.code) {
287
+ await edgeCache.deleteValue();
288
+ }
289
+ } catch (error) {
290
+ }
291
+ };
292
+
293
+ // src/middlewares/use-appwarden.ts
294
+ var useAppwarden = (input) => async (context, next) => {
295
+ await next();
296
+ const { request, response } = context;
297
+ try {
298
+ const requestUrl = new URL(request.url);
299
+ const provider = "cloudflare-cache";
300
+ const keyName = APPWARDEN_CACHE_KEY;
301
+ const edgeCache = store.json(
302
+ {
303
+ serviceOrigin: requestUrl.origin,
304
+ cache: await caches.open("appwarden:lock")
305
+ },
306
+ keyName
307
+ );
308
+ if (isResetCacheRequest(request)) {
309
+ await handleResetCache(keyName, provider, edgeCache, request);
310
+ return;
311
+ }
312
+ const isHTMLRequest = response.headers.get("Content-Type")?.includes("text/html");
313
+ const isMonitoringRequest = request.headers.get("User-Agent") === APPWARDEN_USER_AGENT;
314
+ if (isHTMLRequest && !isMonitoringRequest) {
315
+ const innerContext = {
316
+ keyName,
317
+ request,
318
+ edgeCache,
319
+ requestUrl,
320
+ provider,
321
+ debug: input.debug,
322
+ lockPageSlug: input.lockPageSlug,
323
+ appwardenApiToken: input.appwardenApiToken,
324
+ waitUntil: (fn) => context.waitUntil(fn)
325
+ };
326
+ await maybeQuarantine(innerContext, {
327
+ onLocked: async () => {
328
+ context.response = await renderLockPage(innerContext);
329
+ }
330
+ });
331
+ }
332
+ } catch (e) {
333
+ const message = "Appwarden encountered an unknown error. Please contact Appwarden support at https://appwarden.io/join-community.";
334
+ console.error(
335
+ printMessage(
336
+ e instanceof Error ? `${message} - ${e.message}` : message
337
+ )
338
+ );
339
+ }
340
+ };
341
+
342
+ // src/middlewares/use-fetch-origin.ts
343
+ var useFetchOrigin = () => async (context, next) => {
344
+ context.response = await fetch(
345
+ new Request(context.request, {
346
+ ...context.request,
347
+ redirect: "follow"
348
+ })
349
+ );
350
+ await next();
351
+ };
352
+
353
+ // src/runners/appwarden-on-cloudflare.ts
354
+ var appwardenOnCloudflare = (inputFn) => async (request, env, ctx) => {
355
+ ctx.passThroughOnException();
356
+ const context = {
357
+ request,
358
+ hostname: new URL(request.url).host,
359
+ response: new Response("Unhandled response"),
360
+ // https://developers.cloudflare.com/workers/observability/errors/#illegal-invocation-errors
361
+ waitUntil: (fn) => ctx.waitUntil(fn)
362
+ };
363
+ const parsedInput = ConfigFnInputSchema.safeParse(inputFn);
364
+ if (!parsedInput.success) {
365
+ return insertErrorLogs(context, parsedInput.error);
366
+ }
367
+ try {
368
+ const input = parsedInput.data({ env, ctx, cf: {} });
369
+ const pipeline = [
370
+ ...input.middleware.before,
371
+ useAppwarden(input),
372
+ useFetchOrigin()
373
+ ];
374
+ await usePipeline(...pipeline).execute(context);
375
+ } catch (error) {
376
+ if (error instanceof ZodError) {
377
+ return insertErrorLogs(context, error);
378
+ }
379
+ throw error;
380
+ }
381
+ return context.response;
382
+ };
383
+
384
+ // src/bundles/cloudflare.ts
385
+ var withAppwarden = appwardenOnCloudflare;
386
+ export {
387
+ useContentSecurityPolicy,
388
+ withAppwarden
389
+ };
package/index.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ export { B as Bindings } from './cloudflare-2PkEr25r.js';
2
+ export { C as CSPDirectivesSchema, a as CSPModeSchema, M as Middleware, u as useContentSecurityPolicy } from './use-content-security-policy-C89AROtC.js';
3
+ import { z } from 'zod';
4
+
5
+ declare const LOCKDOWN_TEST_EXPIRY_MS: number;
6
+ declare const APPWARDEN_USER_AGENT: "Appwarden-Monitor";
7
+ declare const APPWARDEN_CACHE_KEY: "appwarden-lock";
8
+
9
+ /**
10
+ * Extracts the Edge Config ID from a valid Edge Config URL
11
+ * @param value The Edge Config URL
12
+ * @returns The Edge Config ID or undefined if the URL is invalid
13
+ */
14
+ declare const getEdgeConfigId: (value?: string) => string | undefined;
15
+ /**
16
+ * Checks if a URL is a valid cache URL (less strict validation)
17
+ */
18
+ declare const isCacheUrl: {
19
+ /**
20
+ * Checks if a URL is a valid Edge Config URL (hostname check only)
21
+ * @param value The URL to check
22
+ * @returns True if the URL is a valid Edge Config URL, false otherwise
23
+ */
24
+ edgeConfig: (value?: string) => boolean;
25
+ /**
26
+ * Checks if a URL is a valid Upstash URL (hostname check only)
27
+ * @param value The URL to check
28
+ * @returns True if the URL is a valid Upstash URL, false otherwise
29
+ */
30
+ upstash: (value?: string) => boolean;
31
+ };
32
+ /**
33
+ * Performs strict validation of cache URLs
34
+ */
35
+ declare const isValidCacheUrl: {
36
+ /**
37
+ * Strictly validates an Edge Config URL
38
+ * @param value The URL to validate
39
+ * @returns True if the URL is a valid Edge Config URL, false otherwise
40
+ */
41
+ edgeConfig: (value?: string) => boolean;
42
+ /**
43
+ * Strictly validates an Upstash URL
44
+ * @param value The URL to validate
45
+ * @returns The password if the URL is valid, false otherwise
46
+ */
47
+ upstash: (value?: string) => string | boolean;
48
+ };
49
+
50
+ declare const LockValue: z.ZodObject<{
51
+ isLocked: z.ZodNumber;
52
+ isLockedTest: z.ZodNumber;
53
+ lastCheck: z.ZodNumber;
54
+ code: z.ZodString;
55
+ }, "strip", z.ZodTypeAny, {
56
+ code: string;
57
+ isLocked: number;
58
+ isLockedTest: number;
59
+ lastCheck: number;
60
+ }, {
61
+ code: string;
62
+ isLocked: number;
63
+ isLockedTest: number;
64
+ lastCheck: number;
65
+ }>;
66
+ type LockValueType = z.infer<typeof LockValue>;
67
+
68
+ export { APPWARDEN_CACHE_KEY, APPWARDEN_USER_AGENT, LOCKDOWN_TEST_EXPIRY_MS, type LockValueType, getEdgeConfigId, isCacheUrl, isValidCacheUrl };
package/index.js ADDED
@@ -0,0 +1,26 @@
1
+ import {
2
+ getEdgeConfigId,
3
+ isCacheUrl,
4
+ isValidCacheUrl
5
+ } from "./chunk-QEFORWCW.js";
6
+ import {
7
+ CSPDirectivesSchema,
8
+ CSPModeSchema,
9
+ useContentSecurityPolicy
10
+ } from "./chunk-NZNMFDZ7.js";
11
+ import {
12
+ APPWARDEN_CACHE_KEY,
13
+ APPWARDEN_USER_AGENT,
14
+ LOCKDOWN_TEST_EXPIRY_MS
15
+ } from "./chunk-JWUAFJ2E.js";
16
+ export {
17
+ APPWARDEN_CACHE_KEY,
18
+ APPWARDEN_USER_AGENT,
19
+ CSPDirectivesSchema,
20
+ CSPModeSchema,
21
+ LOCKDOWN_TEST_EXPIRY_MS,
22
+ getEdgeConfigId,
23
+ isCacheUrl,
24
+ isValidCacheUrl,
25
+ useContentSecurityPolicy
26
+ };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@appwarden/middleware",
3
+ "version": "1.0.0",
4
+ "description": "Instantly shut off access your app deployed on Cloudflare or Vercel",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Appwarden <support@appwarden.io>",
8
+ "homepage": "https://appwarden.io/docs",
9
+ "bugs": {
10
+ "url": "https://github.com/appwarden/middleware/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/appwarden/middleware.git"
15
+ },
16
+ "keywords": [
17
+ "appwarden",
18
+ "nextjs",
19
+ "remixjs",
20
+ "cloudflare",
21
+ "web3",
22
+ "monitoring"
23
+ ],
24
+ "main": "./index.js",
25
+ "types": "./index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./index.d.ts",
29
+ "default": "./index.js"
30
+ },
31
+ "./vercel": {
32
+ "types": "./vercel.d.ts",
33
+ "default": "./vercel.js"
34
+ },
35
+ "./cloudflare": {
36
+ "types": "./cloudflare.d.ts",
37
+ "default": "./cloudflare.js"
38
+ }
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "dependencies": {
44
+ "@cloudflare/next-on-pages": "1.13.12",
45
+ "@upstash/redis": "^1.34.0",
46
+ "@vercel/edge-config": "^1.4.0",
47
+ "zod": "^3.24.4"
48
+ },
49
+ "peerDependencies": {
50
+ "next": ">=13"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "next": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "publishConfig": {
58
+ "registry": "https://registry.npmjs.org",
59
+ "access": "public",
60
+ "provenance": true
61
+ },
62
+ "pnpm": {
63
+ "overrides": {
64
+ "undici@>=4.5.0 <5.28.5": ">=5.28.5",
65
+ "esbuild@<=0.24.2": ">=0.25.0",
66
+ "cookie@<0.7.0": ">=0.7.0",
67
+ "undici@<5.29.0": ">=5.29.0"
68
+ }
69
+ }
70
+ }