@appwarden/middleware 3.11.6 → 3.13.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
@@ -4,7 +4,7 @@
4
4
  [![GitHub](https://img.shields.io/badge/GitHub-appwarden%2Fmiddleware-181717?logo=github&logoColor=white)](https://github.com/appwarden/middleware)
5
5
  [![npm version](https://img.shields.io/npm/v/@appwarden/middleware.svg)](https://www.npmjs.com/package/@appwarden/middleware)
6
6
  [![npm provenance](https://img.shields.io/badge/npm-provenance-green)](https://docs.npmjs.com/generating-provenance-statements)
7
- ![Test Coverage](https://img.shields.io/badge/coverage-93.08%25-brightgreen)
7
+ ![Test Coverage](https://img.shields.io/badge/coverage-94.3%25-brightgreen)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
9
 
10
10
  ## Core Features
@@ -0,0 +1,445 @@
1
+ import {
2
+ APPWARDEN_HEARTBEAT_ROUTE,
3
+ HEARTBEAT_CONFIG_ERRORS_MAX_SERIALIZED_BYTES,
4
+ HEARTBEAT_CONFIG_ERROR_MAX_CODE_LENGTH,
5
+ HEARTBEAT_CONFIG_ERROR_MAX_COUNT,
6
+ HEARTBEAT_CONFIG_ERROR_MAX_MESSAGE_LENGTH,
7
+ HEARTBEAT_CONFIG_ERROR_MAX_PATH_DEPTH,
8
+ HEARTBEAT_CONFIG_ERROR_MAX_PATH_SEGMENT_LENGTH,
9
+ HEARTBEAT_CONTRACT_VERSION,
10
+ HEARTBEAT_RESPONSE_BODY_MAX_SERIALIZED_BYTES,
11
+ LOCKDOWN_TEST_EXPIRY_MS,
12
+ validateHeartbeatResponseBody
13
+ } from "./chunk-SREQAAZC.js";
14
+
15
+ // src/utils/build-lock-page-url.ts
16
+ function normalizeLockPageSlug(lockPageSlug) {
17
+ return lockPageSlug.startsWith("/") ? lockPageSlug : `/${lockPageSlug}`;
18
+ }
19
+ function buildLockPageUrl(lockPageSlug, requestUrl) {
20
+ const normalizedSlug = normalizeLockPageSlug(lockPageSlug);
21
+ return new URL(normalizedSlug, requestUrl);
22
+ }
23
+ function normalizeTrailingSlash(path) {
24
+ if (path === "/") return path;
25
+ return path.endsWith("/") ? path.slice(0, -1) : path;
26
+ }
27
+ function isOnLockPage(lockPageSlug, requestUrl) {
28
+ const normalizedSlug = normalizeTrailingSlash(
29
+ normalizeLockPageSlug(lockPageSlug)
30
+ );
31
+ const url = typeof requestUrl === "string" ? new URL(requestUrl) : requestUrl;
32
+ const normalizedPathname = normalizeTrailingSlash(url.pathname);
33
+ return normalizedPathname === normalizedSlug;
34
+ }
35
+
36
+ // src/utils/create-redirect.ts
37
+ var TEMPORARY_REDIRECT_STATUS = 302;
38
+ var createRedirect = (url) => {
39
+ return new Response(null, {
40
+ status: TEMPORARY_REDIRECT_STATUS,
41
+ headers: {
42
+ Location: url.toString()
43
+ }
44
+ });
45
+ };
46
+
47
+ // src/utils/print-message.ts
48
+ var addSlashes = (str) => str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\u0000/g, "\\0").replace(/<\/script>/gi, "<\\/script>");
49
+ var printMessage = (message) => `[@appwarden/middleware] ${addSlashes(message)}`;
50
+
51
+ // src/utils/debug.ts
52
+ var debug = (isDebug) => (...msg) => {
53
+ if (!isDebug) return;
54
+ const parts = msg.map((m) => {
55
+ let content;
56
+ if (m instanceof Error) {
57
+ content = m.stack ?? m.message;
58
+ } else if (typeof m === "object" && m !== null) {
59
+ try {
60
+ content = JSON.stringify(m);
61
+ } catch {
62
+ try {
63
+ content = String(m);
64
+ } catch {
65
+ content = "[Unserializable value]";
66
+ }
67
+ }
68
+ } else {
69
+ content = String(m);
70
+ }
71
+ return content;
72
+ });
73
+ const message = parts.join(" ");
74
+ console.log(printMessage(message));
75
+ };
76
+
77
+ // src/utils/memory-cache.ts
78
+ var MemoryCache = class {
79
+ cache = /* @__PURE__ */ new Map();
80
+ maxSize;
81
+ constructor(options) {
82
+ this.maxSize = options.maxSize;
83
+ }
84
+ get(key) {
85
+ let item;
86
+ if (this.cache.has(key)) {
87
+ item = this.cache.get(key);
88
+ this.cache.delete(key);
89
+ if (item !== void 0) {
90
+ this.cache.set(key, item);
91
+ }
92
+ }
93
+ return item;
94
+ }
95
+ put(key, value) {
96
+ if (this.cache.has(key)) {
97
+ this.cache.delete(key);
98
+ } else if (this.cache.size >= this.maxSize) {
99
+ const firstKey = this.cache.keys().next().value;
100
+ if (firstKey !== void 0) {
101
+ this.cache.delete(firstKey);
102
+ }
103
+ }
104
+ this.cache.set(key, value);
105
+ }
106
+ getValues() {
107
+ return this.cache;
108
+ }
109
+ // the default value will be expired here
110
+ static isExpired = (lockValue) => {
111
+ if (!lockValue) {
112
+ return true;
113
+ }
114
+ return Date.now() > lockValue.lastCheck + 3e4;
115
+ };
116
+ static isTestExpired = (lockValue) => {
117
+ if (!lockValue) {
118
+ return true;
119
+ }
120
+ return Date.now() > lockValue.isLockedTest + LOCKDOWN_TEST_EXPIRY_MS;
121
+ };
122
+ };
123
+
124
+ // src/version.ts
125
+ var MIDDLEWARE_VERSION = "3.12.0";
126
+
127
+ // src/utils/heartbeat.ts
128
+ var DEFAULT_HEARTBEAT_CONFIG_ERROR_CODE = "custom";
129
+ var DEFAULT_HEARTBEAT_CONFIG_ERROR_MESSAGE = "Appwarden configuration validation failed";
130
+ var HEARTBEAT_CONSTRUCTION_FAILURE_BODY = JSON.stringify({
131
+ error: "appwarden_heartbeat_construction_failed"
132
+ });
133
+ function createSanitizedMessage(code, path) {
134
+ const fieldName = path.length > 0 ? path[path.length - 1] : "field";
135
+ switch (code) {
136
+ case "invalid_type":
137
+ return `Invalid type for ${fieldName}`;
138
+ case "invalid_literal":
139
+ return `Invalid value for ${fieldName}`;
140
+ case "unrecognized_keys":
141
+ return `Unrecognized keys in ${fieldName}`;
142
+ case "invalid_union":
143
+ return `Invalid union value for ${fieldName}`;
144
+ case "invalid_enum_value":
145
+ return `Invalid enum value for ${fieldName}`;
146
+ case "invalid_arguments":
147
+ return `Invalid arguments for ${fieldName}`;
148
+ case "invalid_return_type":
149
+ return `Invalid return type for ${fieldName}`;
150
+ case "invalid_date":
151
+ return `Invalid date for ${fieldName}`;
152
+ case "invalid_string":
153
+ return `Invalid string format for ${fieldName}`;
154
+ case "too_small":
155
+ return `Value too small for ${fieldName}`;
156
+ case "too_big":
157
+ return `Value too large for ${fieldName}`;
158
+ case "invalid_intersection_types":
159
+ return `Invalid intersection types for ${fieldName}`;
160
+ case "not_multiple_of":
161
+ return `Value not a multiple of required value for ${fieldName}`;
162
+ case "not_finite":
163
+ return `Value must be finite for ${fieldName}`;
164
+ case "custom":
165
+ return `Validation failed for ${fieldName}`;
166
+ default:
167
+ return `Validation error for ${fieldName}`;
168
+ }
169
+ }
170
+ function truncateWithEllipsis(value, maxLength) {
171
+ if (value.length <= maxLength) {
172
+ return value;
173
+ }
174
+ if (maxLength <= 3) {
175
+ return value.substring(0, maxLength);
176
+ }
177
+ return value.substring(0, maxLength - 3) + "...";
178
+ }
179
+ function sanitizePathSegment(segment) {
180
+ if (typeof segment === "number" && Number.isFinite(segment)) {
181
+ return Number.isSafeInteger(segment) ? Math.max(0, segment) : Math.max(0, Math.trunc(segment));
182
+ }
183
+ return truncateWithEllipsis(
184
+ typeof segment === "string" ? segment : String(segment),
185
+ HEARTBEAT_CONFIG_ERROR_MAX_PATH_SEGMENT_LENGTH
186
+ );
187
+ }
188
+ function sanitizePath(path) {
189
+ const truncatedPath = path.slice(0, HEARTBEAT_CONFIG_ERROR_MAX_PATH_DEPTH);
190
+ return truncatedPath.map(sanitizePathSegment);
191
+ }
192
+ function truncateMessage(message) {
193
+ return truncateWithEllipsis(
194
+ message,
195
+ HEARTBEAT_CONFIG_ERROR_MAX_MESSAGE_LENGTH
196
+ );
197
+ }
198
+ function truncateCode(code) {
199
+ return truncateWithEllipsis(code, HEARTBEAT_CONFIG_ERROR_MAX_CODE_LENGTH);
200
+ }
201
+ function normalizeNonEmptyString(value, fallback, truncate) {
202
+ const normalizedValue = truncate(value.trim());
203
+ if (normalizedValue.length > 0) {
204
+ return normalizedValue;
205
+ }
206
+ return truncate(fallback);
207
+ }
208
+ function getSerializedJsonByteLength(value) {
209
+ return new TextEncoder().encode(JSON.stringify(value)).length;
210
+ }
211
+ function isConfigErrorsWithinByteBudget(configErrors) {
212
+ return getSerializedJsonByteLength(configErrors) <= HEARTBEAT_CONFIG_ERRORS_MAX_SERIALIZED_BYTES;
213
+ }
214
+ function isResponseBodyWithinByteBudget(body) {
215
+ return getSerializedJsonByteLength(body) <= HEARTBEAT_RESPONSE_BODY_MAX_SERIALIZED_BYTES;
216
+ }
217
+ function createHeartbeatConfigError(path, code, message) {
218
+ return {
219
+ path: sanitizePath(path),
220
+ code: normalizeNonEmptyString(
221
+ code,
222
+ DEFAULT_HEARTBEAT_CONFIG_ERROR_CODE,
223
+ truncateCode
224
+ ),
225
+ message: normalizeNonEmptyString(
226
+ message,
227
+ DEFAULT_HEARTBEAT_CONFIG_ERROR_MESSAGE,
228
+ truncateMessage
229
+ )
230
+ };
231
+ }
232
+ function normalizeHeartbeatConfigErrors(configErrors) {
233
+ const normalizedConfigErrors = configErrors.slice(0, HEARTBEAT_CONFIG_ERROR_MAX_COUNT).map(
234
+ (configError) => createHeartbeatConfigError(
235
+ configError.path,
236
+ configError.code,
237
+ configError.message
238
+ )
239
+ );
240
+ while (normalizedConfigErrors.length > 0 && !isConfigErrorsWithinByteBudget(normalizedConfigErrors)) {
241
+ normalizedConfigErrors.pop();
242
+ }
243
+ return normalizedConfigErrors;
244
+ }
245
+ function sanitizeConfigErrors(error) {
246
+ if (!error) {
247
+ return [];
248
+ }
249
+ const errors = [];
250
+ const issues = error.issues.slice(0, HEARTBEAT_CONFIG_ERROR_MAX_COUNT);
251
+ for (const issue of issues) {
252
+ const sanitizedPath = sanitizePath(issue.path);
253
+ const message = truncateMessage(
254
+ createSanitizedMessage(issue.code, sanitizedPath)
255
+ );
256
+ errors.push({
257
+ path: sanitizedPath,
258
+ code: issue.code,
259
+ message
260
+ });
261
+ }
262
+ return errors;
263
+ }
264
+ function createHeartbeatResponseBody(service, configErrors = []) {
265
+ const normalizedConfigErrors = normalizeHeartbeatConfigErrors(configErrors);
266
+ const body = {
267
+ app: "appwarden",
268
+ kind: "heartbeat",
269
+ status: "ok",
270
+ contractVersion: HEARTBEAT_CONTRACT_VERSION,
271
+ service,
272
+ version: MIDDLEWARE_VERSION,
273
+ configErrors: normalizedConfigErrors
274
+ };
275
+ while (body.configErrors.length > 0 && !isResponseBodyWithinByteBudget(body)) {
276
+ body.configErrors.pop();
277
+ }
278
+ return validateHeartbeatResponseBody(body);
279
+ }
280
+ function createHeartbeatConstructionFailureResponse() {
281
+ return new Response(HEARTBEAT_CONSTRUCTION_FAILURE_BODY, {
282
+ status: 500,
283
+ headers: {
284
+ "content-type": "application/json",
285
+ "cache-control": "no-store"
286
+ }
287
+ });
288
+ }
289
+ function createHeartbeatResponse(service, configErrors = []) {
290
+ try {
291
+ const body = createHeartbeatResponseBody(service, configErrors);
292
+ return new Response(JSON.stringify(body), {
293
+ status: 200,
294
+ headers: {
295
+ "content-type": "application/json",
296
+ "cache-control": "no-store",
297
+ "x-appwarden-heartbeat": "1",
298
+ "x-appwarden-contract-version": String(HEARTBEAT_CONTRACT_VERSION),
299
+ "x-appwarden-service": service,
300
+ "x-appwarden-version": MIDDLEWARE_VERSION
301
+ }
302
+ });
303
+ } catch {
304
+ return createHeartbeatConstructionFailureResponse();
305
+ }
306
+ }
307
+ function isHeartbeatRoute(url) {
308
+ return url.pathname === APPWARDEN_HEARTBEAT_ROUTE;
309
+ }
310
+ function isHeartbeatRequest(request, url) {
311
+ return request.method === "GET" && isHeartbeatRoute(url);
312
+ }
313
+ function handleHeartbeatRequest(request, service, configErrors = []) {
314
+ return createHeartbeatResponse(service, configErrors);
315
+ }
316
+
317
+ // src/utils/request-checks.ts
318
+ function isHTMLResponse(response) {
319
+ return response.headers.get("Content-Type")?.includes("text/html") ?? false;
320
+ }
321
+ function isHTMLRequest(request) {
322
+ const accept = request.headers.get("accept");
323
+ if (!accept) {
324
+ return false;
325
+ }
326
+ const normalizedAccept = accept.toLowerCase();
327
+ const isWildcardOnlyAccept = (value) => {
328
+ const mediaRanges2 = value.split(",");
329
+ let hasNonEmptyRange = false;
330
+ for (const range of mediaRanges2) {
331
+ const [typeSubtype] = range.split(";");
332
+ const trimmed = typeSubtype.trim();
333
+ if (!trimmed) {
334
+ continue;
335
+ }
336
+ hasNonEmptyRange = true;
337
+ if (trimmed !== "*/*" && trimmed !== "*") {
338
+ return false;
339
+ }
340
+ }
341
+ return hasNonEmptyRange;
342
+ };
343
+ if (isWildcardOnlyAccept(normalizedAccept)) {
344
+ return false;
345
+ }
346
+ const mediaRanges = normalizedAccept.split(",");
347
+ for (const range of mediaRanges) {
348
+ const [typeSubtype] = range.split(";");
349
+ const token = typeSubtype.trim();
350
+ if (token === "text/html") {
351
+ return true;
352
+ }
353
+ }
354
+ return false;
355
+ }
356
+
357
+ // src/utils/cloudflare/csp-keywords.ts
358
+ var CSP_KEYWORDS = [
359
+ "self",
360
+ "none",
361
+ "unsafe-inline",
362
+ "unsafe-eval",
363
+ "unsafe-hashes",
364
+ "strict-dynamic",
365
+ "report-sample",
366
+ "unsafe-allow-redirects",
367
+ "wasm-unsafe-eval",
368
+ "trusted-types-eval",
369
+ "report-sha256",
370
+ "report-sha384",
371
+ "report-sha512",
372
+ "unsafe-webtransport-hashes"
373
+ ];
374
+ var CSP_KEYWORDS_SET = new Set(CSP_KEYWORDS);
375
+ var isCSPKeyword = (value) => {
376
+ return CSP_KEYWORDS_SET.has(value.toLowerCase());
377
+ };
378
+ var isQuoted = (value) => {
379
+ return value.startsWith("'") && value.endsWith("'");
380
+ };
381
+ var autoQuoteCSPKeyword = (value) => {
382
+ const trimmed = value.trim();
383
+ if (isQuoted(trimmed)) {
384
+ return trimmed;
385
+ }
386
+ if (isCSPKeyword(trimmed)) {
387
+ return `'${trimmed}'`;
388
+ }
389
+ return trimmed;
390
+ };
391
+ var autoQuoteCSPDirectiveValue = (value) => {
392
+ return value.trim().split(/\s+/).filter(Boolean).map(autoQuoteCSPKeyword).join(" ");
393
+ };
394
+ var autoQuoteCSPDirectiveArray = (values) => {
395
+ return values.map(autoQuoteCSPKeyword);
396
+ };
397
+
398
+ // src/utils/cloudflare/make-csp-header.ts
399
+ var addNonce = (value, cspNonce) => value.replace("{{nonce}}", `'nonce-${cspNonce}'`);
400
+ var makeCSPHeader = (cspNonce, directives, mode) => {
401
+ const namesSeen = /* @__PURE__ */ new Set(), result = [];
402
+ Object.entries(directives ?? {}).forEach(([originalName, value]) => {
403
+ const name = originalName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
404
+ if (namesSeen.has(name)) {
405
+ throw new Error(`${originalName} is specified more than once`);
406
+ }
407
+ namesSeen.add(name);
408
+ let directiveValue;
409
+ if (Array.isArray(value)) {
410
+ directiveValue = autoQuoteCSPDirectiveArray(value).join(" ");
411
+ } else if (value === true) {
412
+ directiveValue = "";
413
+ } else if (typeof value === "string") {
414
+ directiveValue = autoQuoteCSPDirectiveValue(value);
415
+ } else {
416
+ return;
417
+ }
418
+ if (directiveValue) {
419
+ result.push(`${name} ${addNonce(directiveValue, cspNonce)}`);
420
+ } else {
421
+ result.push(name);
422
+ }
423
+ });
424
+ return [
425
+ mode === "enforced" ? "Content-Security-Policy" : "Content-Security-Policy-Report-Only",
426
+ result.join("; ")
427
+ ];
428
+ };
429
+
430
+ export {
431
+ buildLockPageUrl,
432
+ isOnLockPage,
433
+ TEMPORARY_REDIRECT_STATUS,
434
+ createRedirect,
435
+ printMessage,
436
+ debug,
437
+ MemoryCache,
438
+ createHeartbeatConfigError,
439
+ sanitizeConfigErrors,
440
+ isHeartbeatRequest,
441
+ handleHeartbeatRequest,
442
+ isHTMLResponse,
443
+ isHTMLRequest,
444
+ makeCSPHeader
445
+ };
@@ -1,11 +1,9 @@
1
- import {
2
- UseCSPInputSchema
3
- } from "./chunk-ZTVJBORU.js";
4
1
  import {
5
2
  AppwardenApiHostnameSchema,
6
3
  AppwardenApiTokenSchema,
7
- BooleanSchema
8
- } from "./chunk-WEM7GS4M.js";
4
+ BooleanSchema,
5
+ UseCSPInputSchema
6
+ } from "./chunk-SREQAAZC.js";
9
7
 
10
8
  // src/schemas/use-appwarden.ts
11
9
  import { z } from "zod";