@appwarden/middleware 1.0.18 → 1.0.19

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,232 @@
1
+ import {
2
+ APPWARDEN_TEST_ROUTE,
3
+ LockValue,
4
+ MemoryCache,
5
+ debug,
6
+ printMessage
7
+ } from "./chunk-D55SBZQF.js";
8
+
9
+ // src/utils/cloudflare/cloudflare-cache.ts
10
+ var store = {
11
+ json: (context, cacheKey, options) => {
12
+ const cacheKeyUrl = new URL(cacheKey, context.serviceOrigin);
13
+ return {
14
+ getValue: () => getCacheValue(context, cacheKeyUrl),
15
+ updateValue: (json) => updateCacheValue(
16
+ context,
17
+ cacheKeyUrl,
18
+ json,
19
+ options?.cacheExpirationSeconds
20
+ ),
21
+ deleteValue: () => clearCache(context, cacheKeyUrl)
22
+ };
23
+ }
24
+ };
25
+ var getCacheValue = async (context, cacheKey) => {
26
+ const match = await context.cache.match(cacheKey);
27
+ if (!match) {
28
+ debug(`[${cacheKey.pathname}] Cache MISS!`);
29
+ return void 0;
30
+ }
31
+ debug(`[${cacheKey.pathname}] Cache MATCH!`);
32
+ return match;
33
+ };
34
+ var updateCacheValue = async (context, cacheKey, value, cacheExpirationSeconds) => {
35
+ debug(
36
+ "updating cache...",
37
+ cacheKey.pathname,
38
+ value,
39
+ cacheExpirationSeconds ? `expires in ${cacheExpirationSeconds}s` : ""
40
+ );
41
+ await context.cache.put(
42
+ cacheKey,
43
+ new Response(JSON.stringify(value), {
44
+ headers: {
45
+ "content-type": "application/json",
46
+ ...cacheExpirationSeconds && {
47
+ "cache-control": `max-age=${cacheExpirationSeconds}`
48
+ }
49
+ }
50
+ })
51
+ );
52
+ };
53
+ var clearCache = (context, cacheKey) => context.cache.delete(cacheKey);
54
+
55
+ // src/utils/cloudflare/create-response.ts
56
+ var createResponse = (body, status) => new Response(body, {
57
+ status
58
+ });
59
+
60
+ // src/utils/cloudflare/delete-edge-value.ts
61
+ var deleteEdgeValue = async (context) => {
62
+ try {
63
+ switch (context.provider) {
64
+ case "cloudflare-cache": {
65
+ const success = await context.edgeCache.deleteValue();
66
+ if (!success) {
67
+ throw new Error();
68
+ }
69
+ break;
70
+ }
71
+ default:
72
+ throw new Error(`Unsupported provider: ${context.provider}`);
73
+ }
74
+ } catch (e) {
75
+ const message = "Failed to delete edge value";
76
+ console.error(
77
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
78
+ );
79
+ }
80
+ };
81
+
82
+ // src/utils/cloudflare/get-lock-value.ts
83
+ var getLockValue = async (context) => {
84
+ try {
85
+ let shouldDeleteEdgeValue = false;
86
+ let cacheResponse, lockValue = {
87
+ isLockedTest: 0,
88
+ isLocked: 0,
89
+ lastCheck: Date.now(),
90
+ code: ""
91
+ };
92
+ switch (context.provider) {
93
+ case "cloudflare-cache": {
94
+ cacheResponse = await context.edgeCache.getValue();
95
+ break;
96
+ }
97
+ default:
98
+ throw new Error(`Unsupported provider: ${context.provider}`);
99
+ }
100
+ if (!cacheResponse) {
101
+ return { lockValue: void 0 };
102
+ }
103
+ try {
104
+ const clonedResponse = cacheResponse?.clone();
105
+ lockValue = LockValue.parse(
106
+ clonedResponse ? await clonedResponse.json() : void 0
107
+ );
108
+ } catch (error) {
109
+ console.error(
110
+ printMessage(
111
+ `Failed to parse ${context.keyName} from edge cache - ${error}`
112
+ )
113
+ );
114
+ shouldDeleteEdgeValue = true;
115
+ }
116
+ return { lockValue, shouldDeleteEdgeValue };
117
+ } catch (e) {
118
+ const message = "Failed to retrieve edge value";
119
+ console.error(
120
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
121
+ );
122
+ return { lockValue: void 0 };
123
+ }
124
+ };
125
+
126
+ // src/utils/cloudflare/sync-edge-value.ts
127
+ var APIError = class extends Error {
128
+ constructor(message) {
129
+ super(message);
130
+ this.name = "APIError";
131
+ }
132
+ };
133
+ var syncEdgeValue = async (context) => {
134
+ debug(`syncing with api ${"https://bot-gateway.appwarden.io"}`);
135
+ try {
136
+ const response = await fetch(new URL("/v1/status/check", "https://bot-gateway.appwarden.io"), {
137
+ method: "POST",
138
+ headers: { "content-type": "application/json" },
139
+ body: JSON.stringify({
140
+ service: "cloudflare",
141
+ provider: context.provider,
142
+ fqdn: context.requestUrl.hostname,
143
+ appwardenApiToken: context.appwardenApiToken
144
+ })
145
+ });
146
+ if (response.status !== 200) {
147
+ throw new Error(`${response.status} ${response.statusText}`);
148
+ }
149
+ if (response.headers.get("content-type")?.includes("application/json")) {
150
+ const result = await response.json();
151
+ if (result.error) {
152
+ throw new APIError(result.error.message);
153
+ }
154
+ if (result.content) {
155
+ try {
156
+ const parsedValue = LockValue.omit({ lastCheck: true }).parse(
157
+ result.content
158
+ );
159
+ debug(
160
+ `syncing with api...DONE ${JSON.stringify(parsedValue, null, 2)}`
161
+ );
162
+ await context.edgeCache.updateValue({
163
+ ...parsedValue,
164
+ lastCheck: Date.now()
165
+ });
166
+ } catch (error) {
167
+ throw new APIError(`Failed to parse check endpoint result - ${error}`);
168
+ }
169
+ }
170
+ }
171
+ } catch (e) {
172
+ const message = "Failed to fetch from check endpoint";
173
+ console.error(
174
+ printMessage(
175
+ e instanceof APIError ? e.message : e instanceof Error ? `${message} - ${e.message}` : message
176
+ )
177
+ );
178
+ }
179
+ };
180
+
181
+ // src/handlers/maybe-quarantine.ts
182
+ var resolveLockValue = async (context, options) => {
183
+ const { lockValue, shouldDeleteEdgeValue } = await getLockValue(context);
184
+ if (shouldDeleteEdgeValue) {
185
+ await deleteEdgeValue(context);
186
+ }
187
+ if (lockValue?.isLocked || context.requestUrl.pathname === APPWARDEN_TEST_ROUTE && !MemoryCache.isTestExpired(lockValue)) {
188
+ await options.onLocked();
189
+ }
190
+ return lockValue;
191
+ };
192
+ var maybeQuarantine = async (context, options) => {
193
+ const cachedLockValue = await resolveLockValue(context, {
194
+ onLocked: options.onLocked
195
+ });
196
+ const shouldRecheck = MemoryCache.isExpired(cachedLockValue);
197
+ if (shouldRecheck) {
198
+ if (!cachedLockValue || cachedLockValue.isLocked) {
199
+ await syncEdgeValue(context);
200
+ await resolveLockValue(context, {
201
+ onLocked: options.onLocked
202
+ });
203
+ } else {
204
+ context.waitUntil(syncEdgeValue(context));
205
+ }
206
+ }
207
+ };
208
+
209
+ // src/handlers/reset-cache.ts
210
+ var isResetCacheRequest = (request) => request.method === "POST" && new URL(request.url).pathname === "/__appwarden/reset-cache" && request.headers.get("content-type") === "application/json";
211
+ var handleResetCache = async (keyName, provider, edgeCache, request) => {
212
+ const { lockValue } = await getLockValue({
213
+ keyName,
214
+ provider,
215
+ edgeCache
216
+ });
217
+ try {
218
+ const body = await request.clone().json();
219
+ if (body.code === lockValue?.code) {
220
+ await edgeCache.deleteValue();
221
+ }
222
+ } catch (error) {
223
+ }
224
+ };
225
+
226
+ export {
227
+ store,
228
+ createResponse,
229
+ maybeQuarantine,
230
+ isResetCacheRequest,
231
+ handleResetCache
232
+ };
@@ -0,0 +1,413 @@
1
+ // src/constants.ts
2
+ var LOCKDOWN_TEST_EXPIRY_MS = 5 * 60 * 1e3;
3
+ var removedHeaders = ["X-Powered-By", "Server"];
4
+ var securityHeaders = [
5
+ ["Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload"],
6
+ ["X-Frame-Options", "DENY"],
7
+ ["X-XSS-Protection", "1; mode=block"],
8
+ ["X-Content-Type-Options", "nosniff"],
9
+ ["Referrer-Policy", "no-referrer, strict-origin-when-cross-origin"],
10
+ ["X-DNS-Prefetch-Control", "on"]
11
+ ];
12
+ var errors = { badCacheConnection: "BAD_CACHE_CONNECTION" };
13
+ var globalErrors = [errors.badCacheConnection];
14
+ var APPWARDEN_TEST_ROUTE = "_appwarden/test";
15
+
16
+ // src/schemas/vercel.ts
17
+ import { z as z4 } from "zod";
18
+
19
+ // src/utils/debug.ts
20
+ var debug = (...msg) => {
21
+ if (true) {
22
+ console.log(...msg);
23
+ }
24
+ };
25
+
26
+ // src/utils/memory-cache.ts
27
+ var MemoryCache = class {
28
+ cache = /* @__PURE__ */ new Map();
29
+ maxSize;
30
+ constructor(options) {
31
+ this.maxSize = options.maxSize;
32
+ }
33
+ get(key) {
34
+ let item;
35
+ if (this.cache.has(key)) {
36
+ item = this.cache.get(key);
37
+ this.cache.delete(key);
38
+ this.cache.set(key, item);
39
+ }
40
+ return item;
41
+ }
42
+ put(key, value) {
43
+ if (this.cache.has(key)) {
44
+ this.cache.delete(key);
45
+ } else if (this.cache.size >= this.maxSize) {
46
+ const firstKey = this.cache.keys().next().value;
47
+ this.cache.delete(firstKey);
48
+ }
49
+ this.cache.set(key, value);
50
+ }
51
+ getValues() {
52
+ return this.cache;
53
+ }
54
+ // the default value will be expired here
55
+ static isExpired = (lockValue) => {
56
+ if (!lockValue) {
57
+ return true;
58
+ }
59
+ return Date.now() > lockValue.lastCheck + 3e4;
60
+ };
61
+ static isTestExpired = (lockValue) => {
62
+ if (!lockValue) {
63
+ return true;
64
+ }
65
+ return Date.now() > lockValue.isLockedTest + LOCKDOWN_TEST_EXPIRY_MS;
66
+ };
67
+ };
68
+
69
+ // src/utils/print-message.ts
70
+ var printMessage = (message) => `[@appwarden/middleware] ${message}`;
71
+
72
+ // src/utils/vercel/delete-edge-value.ts
73
+ var deleteEdgeValue = async ({
74
+ keyName,
75
+ provider,
76
+ connectionString,
77
+ edgeConfigId,
78
+ vercelApiToken
79
+ }) => {
80
+ try {
81
+ switch (provider) {
82
+ case "edge-config": {
83
+ const res = await fetch(
84
+ `https://api.vercel.com/v1/edge-config/${edgeConfigId}/items`,
85
+ {
86
+ method: "PATCH",
87
+ headers: {
88
+ Authorization: `Bearer ${vercelApiToken}`,
89
+ "Content-Type": "application/json"
90
+ },
91
+ body: JSON.stringify({
92
+ items: [
93
+ {
94
+ key: keyName,
95
+ operation: "delete"
96
+ }
97
+ ]
98
+ })
99
+ }
100
+ );
101
+ if (res.status !== 200) {
102
+ let response = void 0;
103
+ try {
104
+ response = await res.json();
105
+ } catch (error) {
106
+ }
107
+ throw new Error(
108
+ `api.vercel.com/v1/edge-config responded with ${res.status} - ${res.statusText}${response?.error?.message ? ` - ${response?.error?.message}` : ""}`
109
+ );
110
+ }
111
+ break;
112
+ }
113
+ case "upstash": {
114
+ const [url, token] = connectionString.split("@");
115
+ const { Redis } = await import("@upstash/redis");
116
+ const redis = new Redis({ url, token });
117
+ await redis.del(keyName);
118
+ break;
119
+ }
120
+ default:
121
+ throw new Error(`Unsupported provider: ${provider}`);
122
+ }
123
+ } catch (e) {
124
+ const message = "Failed to delete edge value";
125
+ console.error(
126
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
127
+ );
128
+ }
129
+ };
130
+
131
+ // src/schemas/cloudflare.ts
132
+ import { z as z2 } from "zod";
133
+
134
+ // src/schemas/helpers.ts
135
+ import { z } from "zod";
136
+ var BoolOrStringSchema = z.union([z.string(), z.boolean()]).optional();
137
+ var BooleanSchema = BoolOrStringSchema.transform((val) => {
138
+ if (val === "true" || val === true) {
139
+ return true;
140
+ } else if (val === "false" || val === false) {
141
+ return false;
142
+ }
143
+ throw new Error("Invalid value");
144
+ });
145
+ var BaseOutputConfigSchema = z.object({
146
+ debug: BooleanSchema.default(false),
147
+ appwardenApiToken: z.string().refine((appwardenApiToken) => !!appwardenApiToken, {
148
+ message: printMessage(
149
+ "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/api-tokens."
150
+ ),
151
+ path: ["appwardenApiToken"]
152
+ }),
153
+ lockPageSlug: z.string()
154
+ });
155
+ var LockValue = z.object({
156
+ isLocked: z.number(),
157
+ isLockedTest: z.number(),
158
+ lastCheck: z.number(),
159
+ code: z.string()
160
+ });
161
+
162
+ // src/schemas/cloudflare.ts
163
+ var ConfigOutputSchema = BaseOutputConfigSchema.extend({
164
+ middleware: z2.object({ before: z2.custom().array().default([]) }).default({})
165
+ });
166
+ var ConfigFnInputSchema = z2.function().args(z2.custom()).returns(
167
+ ConfigOutputSchema.extend({
168
+ debug: BoolOrStringSchema
169
+ })
170
+ );
171
+ var CloudflareConfigFnOutputSchema = z2.function().args(z2.custom()).returns(ConfigOutputSchema);
172
+
173
+ // src/schemas/nextjs.ts
174
+ import { z as z3 } from "zod";
175
+ var ConfigOutputSchema2 = BaseOutputConfigSchema;
176
+ var NextJsConfigFnOutputSchema = z3.function().args(z3.custom()).returns(ConfigOutputSchema2);
177
+
178
+ // src/utils/vercel/get-lock-value.ts
179
+ var getLockValue = async (context) => {
180
+ try {
181
+ let shouldDeleteEdgeValue = false;
182
+ let serializedValue, lockValue = {
183
+ isLocked: 0,
184
+ isLockedTest: 0,
185
+ lastCheck: 0,
186
+ code: ""
187
+ };
188
+ switch (context.provider) {
189
+ case "edge-config": {
190
+ const { createClient } = await import("@vercel/edge-config");
191
+ const edgeConfig = createClient(context.connectionString);
192
+ serializedValue = await edgeConfig.get(
193
+ context.keyName
194
+ );
195
+ break;
196
+ }
197
+ case "upstash": {
198
+ const [url, token] = context.connectionString.split("@");
199
+ const { Redis } = await import("@upstash/redis");
200
+ const redis = new Redis({ url, token });
201
+ const redisValue = await redis.get(context.keyName);
202
+ serializedValue = redisValue === null ? void 0 : redisValue;
203
+ break;
204
+ }
205
+ default:
206
+ throw new Error(`Unsupported provider: ${context.provider}`);
207
+ }
208
+ if (!serializedValue) {
209
+ return { lockValue: void 0 };
210
+ }
211
+ try {
212
+ lockValue = LockValue.parse(JSON.parse(serializedValue));
213
+ } catch (error) {
214
+ console.error(
215
+ printMessage(
216
+ `Failed to parse ${context.keyName} from edge cache - ${error}`
217
+ )
218
+ );
219
+ shouldDeleteEdgeValue = true;
220
+ }
221
+ return { lockValue, shouldDeleteEdgeValue };
222
+ } catch (e) {
223
+ const message = "Failed to retrieve edge value";
224
+ console.error(
225
+ printMessage(e instanceof Error ? `${message} - ${e.message}` : message)
226
+ );
227
+ if (e instanceof Error) {
228
+ if (e.message.includes("Invalid connection string provided")) {
229
+ throw new Error(errors.badCacheConnection);
230
+ }
231
+ }
232
+ return { lockValue: void 0 };
233
+ }
234
+ };
235
+
236
+ // src/utils/vercel/sync-edge-value.ts
237
+ var APIError = class extends Error {
238
+ constructor(message) {
239
+ super(message);
240
+ this.name = "APIError";
241
+ }
242
+ };
243
+ var syncEdgeValue = async (context) => {
244
+ debug("syncing with api");
245
+ try {
246
+ const response = await fetch(new URL("/v1/status/check", "https://bot-gateway.appwarden.io"), {
247
+ method: "POST",
248
+ headers: { "content-type": "application/json" },
249
+ body: JSON.stringify({
250
+ service: "vercel",
251
+ provider: context.provider,
252
+ fqdn: context.requestUrl.hostname,
253
+ edgeConfigId: context.edgeConfigId ?? "",
254
+ vercelApiToken: context.vercelApiToken ?? "",
255
+ appwardenApiToken: context.appwardenApiToken,
256
+ connectionString: context.connectionString
257
+ })
258
+ });
259
+ if (response.status !== 200) {
260
+ throw new Error(`${response.status} ${response.statusText}`);
261
+ }
262
+ if (response.headers.get("content-type")?.includes("application/json")) {
263
+ const result = await response.json();
264
+ if (result.error) {
265
+ throw new APIError(result.error.message);
266
+ }
267
+ }
268
+ } catch (e) {
269
+ const message = "Failed to fetch from check endpoint";
270
+ console.error(
271
+ printMessage(
272
+ e instanceof APIError ? e.message : e instanceof Error ? `${message} - ${e.message}` : message
273
+ )
274
+ );
275
+ }
276
+ };
277
+
278
+ // src/utils/handle-vercel-request.ts
279
+ var handleVercelRequest = async (context, options) => {
280
+ let cachedLockValue = context.memoryCache.get(context.keyName);
281
+ const shouldRecheck = MemoryCache.isExpired(cachedLockValue);
282
+ if (shouldRecheck) {
283
+ const { lockValue, shouldDeleteEdgeValue } = await getLockValue(context);
284
+ if (lockValue) {
285
+ context.memoryCache.put(context.keyName, lockValue);
286
+ }
287
+ if (shouldDeleteEdgeValue) {
288
+ await deleteEdgeValue(context);
289
+ }
290
+ cachedLockValue = lockValue;
291
+ }
292
+ if (cachedLockValue?.isLocked || context.requestUrl.pathname === APPWARDEN_TEST_ROUTE && !MemoryCache.isTestExpired(cachedLockValue)) {
293
+ options.onLocked();
294
+ }
295
+ return cachedLockValue;
296
+ };
297
+
298
+ // src/utils/middleware.ts
299
+ var usePipeline = (...initMiddlewares) => {
300
+ const stack = [...initMiddlewares];
301
+ const execute = async (context) => {
302
+ const runner = async (prevIndex, index) => {
303
+ if (index === prevIndex) {
304
+ throw new Error("next() called multiple times");
305
+ }
306
+ if (index >= stack.length) {
307
+ return;
308
+ }
309
+ const middleware = stack[index];
310
+ const next = async () => runner(index, index + 1);
311
+ await middleware(context, next);
312
+ };
313
+ await runner(-1, 0);
314
+ };
315
+ return {
316
+ execute
317
+ };
318
+ };
319
+
320
+ // src/utils/render-lock-page.ts
321
+ var renderLockPage = (context) => fetch(new URL(context.lockPageSlug, context.requestUrl.origin), {
322
+ headers: {
323
+ // no browser caching, otherwise we need to hard refresh to disable lock screen
324
+ "Cache-Control": "no-store"
325
+ }
326
+ });
327
+
328
+ // src/schemas/vercel.ts
329
+ var MiddlewareSchema = z4.custom(
330
+ (val) => typeof val === "function"
331
+ );
332
+ var BaseNextJsConfigSchema = z4.object({
333
+ provider: z4.enum(["upstash", "edge-config"]),
334
+ lockPageSlug: z4.string(),
335
+ connectionString: z4.string(),
336
+ edgeConfigId: z4.string().optional(),
337
+ vercelApiToken: z4.string().optional(),
338
+ appwardenApiToken: z4.string()
339
+ });
340
+ var AppwardenConfigSchema = BaseNextJsConfigSchema.refine(
341
+ (data) => !!data.appwardenApiToken,
342
+ {
343
+ message: printMessage(
344
+ "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/api-tokens."
345
+ ),
346
+ path: ["appwardenApiToken"]
347
+ }
348
+ ).refine(
349
+ (data) => {
350
+ return data.provider === "upstash" && data.connectionString.includes("upstash.io") || data.provider === "edge-config" && data.connectionString.includes("edge-config.vercel.com");
351
+ },
352
+ {
353
+ message: printMessage(
354
+ "Invalid connection string for the selected `provider`"
355
+ ),
356
+ path: ["connectionString"]
357
+ }
358
+ ).refine(
359
+ (data) => {
360
+ return data.provider === "upstash" && data.connectionString.includes("upstash") || data.provider === "edge-config" && data.connectionString.includes("edge-config");
361
+ },
362
+ {
363
+ message: printMessage(
364
+ "Invalid connection string for the selected `provider`"
365
+ ),
366
+ path: ["connectionString"]
367
+ }
368
+ ).refine(
369
+ (data) => {
370
+ if (data.provider === "edge-config") {
371
+ return !!data.vercelApiToken;
372
+ }
373
+ return true;
374
+ },
375
+ {
376
+ message: printMessage("Missing vercelApiToken when provider=edge-config"),
377
+ path: ["vercelApiToken"]
378
+ }
379
+ ).refine(
380
+ (data) => {
381
+ if (data.provider === "edge-config") {
382
+ return !!data.edgeConfigId;
383
+ }
384
+ return true;
385
+ },
386
+ {
387
+ message: printMessage(
388
+ "Missing `edgeConfigId` when `provider=edge-config`"
389
+ ),
390
+ path: ["edgeConfigId"]
391
+ }
392
+ );
393
+
394
+ export {
395
+ LOCKDOWN_TEST_EXPIRY_MS,
396
+ removedHeaders,
397
+ securityHeaders,
398
+ globalErrors,
399
+ APPWARDEN_TEST_ROUTE,
400
+ debug,
401
+ MemoryCache,
402
+ printMessage,
403
+ BoolOrStringSchema,
404
+ LockValue,
405
+ CloudflareConfigFnOutputSchema,
406
+ NextJsConfigFnOutputSchema,
407
+ BaseNextJsConfigSchema,
408
+ AppwardenConfigSchema,
409
+ syncEdgeValue,
410
+ handleVercelRequest,
411
+ usePipeline,
412
+ renderLockPage
413
+ };