@appwarden/middleware 3.2.0 → 3.3.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/vercel.js CHANGED
@@ -4,22 +4,27 @@ import {
4
4
  } from "./chunk-QEFORWCW.js";
5
5
  import {
6
6
  validateConfig
7
- } from "./chunk-COV6SHCD.js";
7
+ } from "./chunk-7AVYENM2.js";
8
8
  import {
9
- LockValue,
10
9
  MemoryCache,
11
10
  TEMPORARY_REDIRECT_STATUS,
12
11
  buildLockPageUrl,
13
12
  isOnLockPage
14
- } from "./chunk-ZX5QO4Y2.js";
13
+ } from "./chunk-SUZPTFWY.js";
15
14
  import {
16
15
  APPWARDEN_CACHE_KEY,
17
- debug,
16
+ CSPDirectivesSchema,
17
+ CSPModeSchema,
18
18
  errors,
19
19
  globalErrors,
20
- isHTMLRequest,
20
+ isHTMLRequest
21
+ } from "./chunk-HCGLR3Z3.js";
22
+ import {
23
+ LockValue,
24
+ debug,
25
+ makeCSPHeader,
21
26
  printMessage
22
- } from "./chunk-L5EQIJZB.js";
27
+ } from "./chunk-ZBYVJ3HA.js";
23
28
 
24
29
  // src/runners/appwarden-on-vercel.ts
25
30
  import { waitUntil } from "@vercel/functions";
@@ -134,11 +139,42 @@ var syncEdgeValue = async (context) => {
134
139
  };
135
140
 
136
141
  // src/schemas/vercel.ts
142
+ var VercelCSPSchema = z.object({
143
+ mode: CSPModeSchema,
144
+ directives: z.lazy(() => CSPDirectivesSchema).optional().refine(
145
+ (val) => {
146
+ try {
147
+ if (typeof val === "string") {
148
+ JSON.parse(val);
149
+ }
150
+ return true;
151
+ } catch {
152
+ return false;
153
+ }
154
+ },
155
+ { message: "DirectivesBadParse" /* DirectivesBadParse */ }
156
+ ).refine(
157
+ (val) => {
158
+ if (!val) return true;
159
+ const serialized = typeof val === "string" ? val : JSON.stringify(val);
160
+ return !serialized.includes("{{nonce}}");
161
+ },
162
+ {
163
+ message: "Nonce-based CSP is not supported in Vercel Edge Middleware. Remove '{{nonce}}' placeholders from your CSP directives, as Vercel does not support nonce injection."
164
+ }
165
+ ).transform(
166
+ (val) => typeof val === "string" ? JSON.parse(val) : val
167
+ )
168
+ }).refine(
169
+ (values) => ["report-only", "enforced"].includes(values.mode) ? !!values.directives : true,
170
+ { path: ["directives"], message: "DirectivesRequired" /* DirectivesRequired */ }
171
+ );
137
172
  var BaseNextJsConfigSchema = z.object({
138
173
  cacheUrl: z.string(),
139
174
  appwardenApiToken: z.string(),
140
175
  vercelApiToken: z.string().optional(),
141
- lockPageSlug: z.string().default("").transform((val) => val.replace(/^\/?/, "/"))
176
+ lockPageSlug: z.string().default("").transform((val) => val.replace(/^\/?/, "/")),
177
+ contentSecurityPolicy: VercelCSPSchema.optional()
142
178
  });
143
179
  var AppwardenConfigSchema = BaseNextJsConfigSchema.refine(
144
180
  (data) => {
@@ -203,6 +239,18 @@ function createAppwardenMiddleware(config) {
203
239
  return NextResponse.next();
204
240
  }
205
241
  const parsedConfig = AppwardenConfigSchema.parse(config);
242
+ const applyCspHeaders = (response) => {
243
+ const cspConfig = parsedConfig.contentSecurityPolicy;
244
+ if (cspConfig && ["enforced", "report-only"].includes(cspConfig.mode)) {
245
+ const [headerName, headerValue] = makeCSPHeader(
246
+ "",
247
+ cspConfig.directives,
248
+ cspConfig.mode
249
+ );
250
+ response.headers.set(headerName, headerValue);
251
+ }
252
+ return response;
253
+ };
206
254
  try {
207
255
  const requestUrl = new URL(request.url);
208
256
  const isHTML = isHTMLRequest(request);
@@ -239,12 +287,14 @@ function createAppwardenMiddleware(config) {
239
287
  parsedConfig.lockPageSlug,
240
288
  request.url
241
289
  );
242
- return Response.redirect(
290
+ const redirectResponse = Response.redirect(
243
291
  lockPageUrl.toString(),
244
292
  TEMPORARY_REDIRECT_STATUS
245
293
  );
294
+ return applyCspHeaders(redirectResponse);
246
295
  }
247
- return NextResponse.next();
296
+ const response = NextResponse.next();
297
+ return applyCspHeaders(response);
248
298
  } catch (e) {
249
299
  const message = "Appwarden encountered an unknown error. Please contact Appwarden support at https://appwarden.io/join-community.";
250
300
  if (e instanceof Error) {
package/chunk-L5EQIJZB.js DELETED
@@ -1,54 +0,0 @@
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/utils/debug.ts
9
- var debug = (...msg) => {
10
- if (true) {
11
- const formatted = msg.map((m) => {
12
- if (typeof m === "object" && m !== null) {
13
- if (m instanceof Error) {
14
- return m.stack ?? m.message;
15
- }
16
- try {
17
- return JSON.stringify(m);
18
- } catch {
19
- try {
20
- return String(m);
21
- } catch {
22
- return "[Unserializable value]";
23
- }
24
- }
25
- }
26
- return m;
27
- });
28
- console.log(...formatted);
29
- }
30
- };
31
-
32
- // src/utils/print-message.ts
33
- var addSlashes = (str) => str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\u0000/g, "\\0").replace(/<\/script>/gi, "<\\/script>");
34
- var printMessage = (message) => `[@appwarden/middleware] ${addSlashes(message)}`;
35
-
36
- // src/utils/request-checks.ts
37
- function isHTMLResponse(response) {
38
- return response.headers.get("Content-Type")?.includes("text/html") ?? false;
39
- }
40
- function isHTMLRequest(request) {
41
- return request.headers.get("accept")?.includes("text/html") ?? false;
42
- }
43
-
44
- export {
45
- LOCKDOWN_TEST_EXPIRY_MS,
46
- errors,
47
- globalErrors,
48
- APPWARDEN_TEST_ROUTE,
49
- APPWARDEN_CACHE_KEY,
50
- debug,
51
- printMessage,
52
- isHTMLResponse,
53
- isHTMLRequest
54
- };
package/chunk-MDODCAA3.js DELETED
@@ -1,232 +0,0 @@
1
- import {
2
- LockValue,
3
- MemoryCache
4
- } from "./chunk-ZX5QO4Y2.js";
5
- import {
6
- APPWARDEN_CACHE_KEY,
7
- APPWARDEN_TEST_ROUTE,
8
- debug,
9
- printMessage
10
- } from "./chunk-L5EQIJZB.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
- };