@appwarden/middleware 1.0.18 → 1.1.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/chunk-BHCQJZWE.js +358 -0
- package/chunk-WVVSYKJM.js +525 -0
- package/cloudflare.d.ts +8 -11
- package/cloudflare.js +54 -6
- package/index.d.ts +15 -5
- package/index.js +14 -4
- package/package.json +1 -1
- package/use-content-security-policy-DBWKjDEH.d.ts +509 -0
- package/vercel.d.ts +6 -12
- package/vercel.js +82 -5
- package/chunk-C7APN7T6.js +0 -821
- package/chunk-JXIVUR6E.js +0 -186
- package/cloudflare-hVS30fDq.d.ts +0 -99
- package/nextjs-on-pages.d.ts +0 -30
- package/nextjs-on-pages.js +0 -10
- package/use-content-security-policy-BtEGGIeu.d.ts +0 -245
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APPWARDEN_CACHE_KEY,
|
|
3
|
+
APPWARDEN_TEST_ROUTE,
|
|
4
|
+
LockValue,
|
|
5
|
+
MemoryCache,
|
|
6
|
+
UseCSPInputSchema,
|
|
7
|
+
debug,
|
|
8
|
+
getErrors,
|
|
9
|
+
printMessage,
|
|
10
|
+
removedHeaders,
|
|
11
|
+
renderLockPage
|
|
12
|
+
} from "./chunk-WVVSYKJM.js";
|
|
13
|
+
|
|
14
|
+
// src/utils/cloudflare/cloudflare-cache.ts
|
|
15
|
+
var store = {
|
|
16
|
+
json: (context, cacheKey, options) => {
|
|
17
|
+
const cacheKeyUrl = new URL(cacheKey, context.serviceOrigin);
|
|
18
|
+
return {
|
|
19
|
+
getValue: () => getCacheValue(context, cacheKeyUrl),
|
|
20
|
+
updateValue: (json) => updateCacheValue(
|
|
21
|
+
context,
|
|
22
|
+
cacheKeyUrl,
|
|
23
|
+
json,
|
|
24
|
+
options?.cacheExpirationSeconds
|
|
25
|
+
),
|
|
26
|
+
deleteValue: () => clearCache(context, cacheKeyUrl)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var getCacheValue = async (context, cacheKey) => {
|
|
31
|
+
const match = await context.cache.match(cacheKey);
|
|
32
|
+
if (!match) {
|
|
33
|
+
debug(`[${cacheKey.pathname}] Cache MISS!`);
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
debug(`[${cacheKey.pathname}] Cache MATCH!`);
|
|
37
|
+
return match;
|
|
38
|
+
};
|
|
39
|
+
var updateCacheValue = async (context, cacheKey, value, cacheExpirationSeconds) => {
|
|
40
|
+
debug(
|
|
41
|
+
"updating cache...",
|
|
42
|
+
cacheKey.pathname,
|
|
43
|
+
value,
|
|
44
|
+
cacheExpirationSeconds ? `expires in ${cacheExpirationSeconds}s` : ""
|
|
45
|
+
);
|
|
46
|
+
await context.cache.put(
|
|
47
|
+
cacheKey,
|
|
48
|
+
new Response(JSON.stringify(value), {
|
|
49
|
+
headers: {
|
|
50
|
+
"content-type": "application/json",
|
|
51
|
+
...cacheExpirationSeconds && {
|
|
52
|
+
"cache-control": `max-age=${cacheExpirationSeconds}`
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
var clearCache = (context, cacheKey) => context.cache.delete(cacheKey);
|
|
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/insert-errors-logs.ts
|
|
127
|
+
var insertErrorLogs = async (context, error) => new HTMLRewriter().on("body", {
|
|
128
|
+
element: (elem) => {
|
|
129
|
+
elem.append(
|
|
130
|
+
`<script>
|
|
131
|
+
${getErrors(error).map((err) => `console.error(\`${printMessage(err)}\`)`).join("\n")}
|
|
132
|
+
</script>`,
|
|
133
|
+
{ html: true }
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}).transform(await fetch(context.request));
|
|
137
|
+
|
|
138
|
+
// src/utils/cloudflare/make-csp-header.ts
|
|
139
|
+
var addNonce = (value, cspNonce) => value.replace("{{nonce}}", `'nonce-${cspNonce}'`);
|
|
140
|
+
var makeCSPHeader = (cspNonce, directives, mode) => {
|
|
141
|
+
const namesSeen = /* @__PURE__ */ new Set(), result = [];
|
|
142
|
+
Object.entries(directives ?? {}).forEach(([originalName, value]) => {
|
|
143
|
+
const name = originalName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
144
|
+
if (namesSeen.has(name)) {
|
|
145
|
+
throw new Error(`${originalName} is specified more than once`);
|
|
146
|
+
}
|
|
147
|
+
namesSeen.add(name);
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
value = addNonce(value.join(" "), cspNonce);
|
|
150
|
+
} else if (value === true) {
|
|
151
|
+
value = "";
|
|
152
|
+
}
|
|
153
|
+
if (value) {
|
|
154
|
+
result.push(`${name} ${addNonce(value, cspNonce)}`);
|
|
155
|
+
} else if (value !== false) {
|
|
156
|
+
result.push(name);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return [
|
|
160
|
+
mode === "enforced" ? "Content-Security-Policy" : "Content-Security-Policy-Report-Only",
|
|
161
|
+
result.join("; ")
|
|
162
|
+
];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/utils/cloudflare/sync-edge-value.ts
|
|
166
|
+
var APIError = class extends Error {
|
|
167
|
+
constructor(message) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = "APIError";
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var syncEdgeValue = async (context) => {
|
|
173
|
+
debug(`syncing with api`);
|
|
174
|
+
try {
|
|
175
|
+
const response = await fetch(new URL("/v1/status/check", "https://bot-gateway.appwarden.io"), {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "content-type": "application/json" },
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
service: "cloudflare",
|
|
180
|
+
provider: context.provider,
|
|
181
|
+
fqdn: context.requestUrl.hostname,
|
|
182
|
+
appwardenApiToken: context.appwardenApiToken
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
if (response.status !== 200) {
|
|
186
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
187
|
+
}
|
|
188
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
189
|
+
const result = await response.json();
|
|
190
|
+
if (result.error) {
|
|
191
|
+
throw new APIError(result.error.message);
|
|
192
|
+
}
|
|
193
|
+
if (result.content) {
|
|
194
|
+
try {
|
|
195
|
+
const parsedValue = LockValue.omit({ lastCheck: true }).parse(
|
|
196
|
+
result.content
|
|
197
|
+
);
|
|
198
|
+
debug(
|
|
199
|
+
`syncing with api...DONE ${JSON.stringify(parsedValue, null, 2)}`
|
|
200
|
+
);
|
|
201
|
+
await context.edgeCache.updateValue({
|
|
202
|
+
...parsedValue,
|
|
203
|
+
lastCheck: Date.now()
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
throw new APIError(`Failed to parse check endpoint result - ${error}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
const message = "Failed to fetch from check endpoint";
|
|
212
|
+
console.error(
|
|
213
|
+
printMessage(
|
|
214
|
+
e instanceof APIError ? e.message : e instanceof Error ? `${message} - ${e.message}` : message
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// src/middlewares/use-content-security-policy.ts
|
|
221
|
+
var AppendAttribute = (attribute, nonce) => ({
|
|
222
|
+
element: function(element) {
|
|
223
|
+
element.setAttribute(attribute, nonce);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
var useContentSecurityPolicy = (input) => {
|
|
227
|
+
const parsedInput = UseCSPInputSchema.safeParse(input);
|
|
228
|
+
if (!parsedInput.success) {
|
|
229
|
+
throw parsedInput.error;
|
|
230
|
+
}
|
|
231
|
+
const config = parsedInput.data;
|
|
232
|
+
return async (context, next) => {
|
|
233
|
+
await next();
|
|
234
|
+
const { response } = context;
|
|
235
|
+
if (
|
|
236
|
+
// if the csp is disabled
|
|
237
|
+
!["enforced", "report-only"].includes(config.mode)
|
|
238
|
+
) {
|
|
239
|
+
debug(printMessage("csp is disabled"));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (response.headers.has("Content-Type") && !response.headers.get("Content-Type")?.includes("text/html")) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const cspNonce = btoa(crypto.getRandomValues(new Uint32Array(2)).toString());
|
|
246
|
+
const [cspHeaderName, cspHeaderValue] = makeCSPHeader(
|
|
247
|
+
cspNonce,
|
|
248
|
+
config.directives,
|
|
249
|
+
config.mode
|
|
250
|
+
);
|
|
251
|
+
const nextResponse = new Response(response.body, response);
|
|
252
|
+
nextResponse.headers.set(cspHeaderName, cspHeaderValue);
|
|
253
|
+
nextResponse.headers.set("content-type", "text/html; charset=utf-8");
|
|
254
|
+
removedHeaders.forEach((name) => {
|
|
255
|
+
nextResponse.headers.delete(name);
|
|
256
|
+
});
|
|
257
|
+
context.response = new HTMLRewriter().on("style", AppendAttribute("nonce", cspNonce)).on("script", AppendAttribute("nonce", cspNonce)).transform(nextResponse);
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/handlers/maybe-quarantine.ts
|
|
262
|
+
var resolveLockValue = async (context, options) => {
|
|
263
|
+
const { lockValue, shouldDeleteEdgeValue } = await getLockValue(context);
|
|
264
|
+
if (shouldDeleteEdgeValue) {
|
|
265
|
+
await deleteEdgeValue(context);
|
|
266
|
+
}
|
|
267
|
+
if (lockValue?.isLocked || context.requestUrl.pathname === APPWARDEN_TEST_ROUTE && !MemoryCache.isTestExpired(lockValue)) {
|
|
268
|
+
await options.onLocked();
|
|
269
|
+
}
|
|
270
|
+
return lockValue;
|
|
271
|
+
};
|
|
272
|
+
var maybeQuarantine = async (context, options) => {
|
|
273
|
+
const cachedLockValue = await resolveLockValue(context, {
|
|
274
|
+
onLocked: options.onLocked
|
|
275
|
+
});
|
|
276
|
+
const shouldRecheck = MemoryCache.isExpired(cachedLockValue);
|
|
277
|
+
if (shouldRecheck) {
|
|
278
|
+
if (!cachedLockValue || cachedLockValue.isLocked) {
|
|
279
|
+
await syncEdgeValue(context);
|
|
280
|
+
await resolveLockValue(context, {
|
|
281
|
+
onLocked: options.onLocked
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
context.waitUntil(syncEdgeValue(context));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/handlers/reset-cache.ts
|
|
290
|
+
var isResetCacheRequest = (request) => request.method === "POST" && new URL(request.url).pathname === "/__appwarden/reset-cache" && request.headers.get("content-type") === "application/json";
|
|
291
|
+
var handleResetCache = async (keyName, provider, edgeCache, request) => {
|
|
292
|
+
const { lockValue } = await getLockValue({
|
|
293
|
+
keyName,
|
|
294
|
+
provider,
|
|
295
|
+
edgeCache
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
const body = await request.clone().json();
|
|
299
|
+
if (body.code === lockValue?.code) {
|
|
300
|
+
await edgeCache.deleteValue();
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/middlewares/use-appwarden.ts
|
|
307
|
+
var useAppwarden = (input) => async (context, next) => {
|
|
308
|
+
await next();
|
|
309
|
+
const { request, response } = context;
|
|
310
|
+
try {
|
|
311
|
+
const requestUrl = new URL(request.url);
|
|
312
|
+
const provider = "cloudflare-cache";
|
|
313
|
+
const keyName = APPWARDEN_CACHE_KEY;
|
|
314
|
+
const edgeCache = store.json(
|
|
315
|
+
{
|
|
316
|
+
serviceOrigin: requestUrl.origin,
|
|
317
|
+
cache: await caches.open("appwarden:lock")
|
|
318
|
+
},
|
|
319
|
+
keyName
|
|
320
|
+
);
|
|
321
|
+
if (isResetCacheRequest(request)) {
|
|
322
|
+
await handleResetCache(keyName, provider, edgeCache, request);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const isHTMLRequest = response.headers.get("Content-Type")?.includes("text/html");
|
|
326
|
+
if (isHTMLRequest) {
|
|
327
|
+
const innerContext = {
|
|
328
|
+
keyName,
|
|
329
|
+
request,
|
|
330
|
+
edgeCache,
|
|
331
|
+
requestUrl,
|
|
332
|
+
provider,
|
|
333
|
+
debug: input.debug,
|
|
334
|
+
lockPageSlug: input.lockPageSlug,
|
|
335
|
+
appwardenApiToken: input.appwardenApiToken,
|
|
336
|
+
waitUntil: (fn) => context.waitUntil(fn)
|
|
337
|
+
};
|
|
338
|
+
await maybeQuarantine(innerContext, {
|
|
339
|
+
onLocked: async () => {
|
|
340
|
+
context.response = await renderLockPage(innerContext);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} catch (e) {
|
|
345
|
+
const message = "Appwarden encountered an unknown error. Please contact Appwarden support at https://appwarden.io/join-community.";
|
|
346
|
+
console.error(
|
|
347
|
+
printMessage(
|
|
348
|
+
e instanceof Error ? `${message} - ${e.message}` : message
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export {
|
|
355
|
+
insertErrorLogs,
|
|
356
|
+
useAppwarden,
|
|
357
|
+
useContentSecurityPolicy
|
|
358
|
+
};
|