@appwarden/middleware 1.5.1 → 2.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/README.md +17 -48
- package/chunk-5DEXVBY6.js +232 -0
- package/chunk-6PUA5YXP.js +27 -0
- package/{chunk-FDIKUQ3E.js → chunk-7UTT3M2S.js} +11 -3
- package/{chunk-L6RSRHOF.js → chunk-B5IE7V77.js} +36 -34
- package/{chunk-TP5CHUTK.js → chunk-MOTPEQEU.js} +3 -2
- package/chunk-N6AUTMZO.js +22 -0
- package/cloudflare/astro.d.ts +72 -0
- package/cloudflare/astro.js +90 -0
- package/cloudflare/nextjs.d.ts +60 -0
- package/cloudflare/nextjs.js +77 -0
- package/cloudflare/react-router.d.ts +63 -0
- package/cloudflare/react-router.js +83 -0
- package/cloudflare/tanstack-start.d.ts +71 -0
- package/cloudflare/tanstack-start.js +86 -0
- package/cloudflare.d.ts +14 -31
- package/cloudflare.js +28 -219
- package/index.d.ts +2 -4
- package/index.js +2 -4
- package/package.json +30 -4
- package/{use-content-security-policy-D4ccKJVd.d.ts → use-content-security-policy-DjRTjIpm.d.ts} +98 -2
- package/vercel.d.ts +45 -7
- package/vercel.js +62 -151
- package/cloudflare-0sboFQGC.d.ts +0 -100
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @appwarden/middleware
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
[](https://www.npmjs.com/package/@appwarden/middleware)
|
|
5
5
|
[](https://docs.npmjs.com/generating-provenance-statements)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
- **Instant Quarantine**: Immediately redirects all visitors to a maintenance page when activated
|
|
15
15
|
- **Discord Integration**: Trigger lockdowns via Discord commands (`/quarantine lock your.app.io`)
|
|
16
|
-
- **Nonce-based Content Security Policy
|
|
16
|
+
- **Nonce-based Content Security Policy (Cloudflare only)**: Deploy a nonce-based Content Security Policy to supercharge your website security
|
|
17
17
|
- **Minimal Runtime Overhead**: Negligible performance impact by using `event.waitUntil` for status checks
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
@@ -22,57 +22,26 @@ Compatible with websites powered by [Cloudflare](https://developers.cloudflare.c
|
|
|
22
22
|
|
|
23
23
|
For detailed usage instructions, please refer to our [documentation](https://appwarden.io/docs).
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
We recommend using the [`@appwarden/build-cloudflare-action`](https://github.com/appwarden/build-cloudflare-action) Github Action to deploy automatically on Cloudflare.
|
|
28
|
-
|
|
29
|
-
> Read the docs [to get started](https://appwarden.io/docs/guides/cloudflare-integration)
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
import {
|
|
33
|
-
withAppwarden,
|
|
34
|
-
useContentSecurityPolicy,
|
|
35
|
-
} from "@appwarden/middleware/cloudflare"
|
|
36
|
-
|
|
37
|
-
export default {
|
|
38
|
-
fetch: withAppwarden((context) => ({
|
|
39
|
-
debug: context.env.DEBUG,
|
|
40
|
-
lockPageSlug: context.env.LOCK_PAGE_SLUG,
|
|
41
|
-
appwardenApiToken: context.env.APPWARDEN_API_TOKEN,
|
|
42
|
-
middleware: {
|
|
43
|
-
before: [
|
|
44
|
-
useContentSecurityPolicy({
|
|
45
|
-
mode: "enforced",
|
|
46
|
-
directives: {
|
|
47
|
-
"script-src": ["self", "{{nonce}}"],
|
|
48
|
-
"style-src": ["self", "{{nonce}}"],
|
|
49
|
-
},
|
|
50
|
-
}),
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
})),
|
|
54
|
-
}
|
|
55
|
-
```
|
|
25
|
+
## Supported Frameworks
|
|
56
26
|
|
|
57
|
-
###
|
|
27
|
+
### On Cloudflare
|
|
58
28
|
|
|
59
|
-
|
|
29
|
+
Cloudflare has two deployment options: [pages.dev](https://pages.dev) and [Workers static assets](https://developers.cloudflare.com/workers/static-assets/). We support both.
|
|
60
30
|
|
|
61
|
-
|
|
62
|
-
import { withAppwarden } from "@appwarden/middleware/vercel"
|
|
31
|
+
#### On pages.dev
|
|
63
32
|
|
|
64
|
-
|
|
65
|
-
cacheUrl: process.env.EDGE_CONFIG_URL || process.env.UPSTASH_URL,
|
|
66
|
-
appwardenApiToken: process.env.APPWARDEN_API_TOKEN,
|
|
67
|
-
vercelApiToken: process.env.VERCEL_API_TOKEN,
|
|
68
|
-
lockPageSlug: "/maintenance",
|
|
69
|
-
})
|
|
33
|
+
- [All websites on pages.dev](https://appwarden.io/docs/guides/cloudflare-integration)
|
|
70
34
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
35
|
+
#### On Workers static assets
|
|
36
|
+
|
|
37
|
+
- [Astro](https://appwarden.io/docs/guides/astro-cloudflare)
|
|
38
|
+
- [React Router](https://appwarden.io/docs/guides/react-router-cloudflare)
|
|
39
|
+
- [TanStack Start](https://appwarden.io/docs/guides/tanstack-start-cloudflare)
|
|
40
|
+
- [Next.js](https://appwarden.io/docs/guides/nextjs-cloudflare)
|
|
41
|
+
|
|
42
|
+
### On Vercel
|
|
43
|
+
|
|
44
|
+
- [All websites on Vercel](https://appwarden.io/docs/guides/vercel-integration)
|
|
76
45
|
|
|
77
46
|
## Contributing
|
|
78
47
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LockValue,
|
|
3
|
+
MemoryCache
|
|
4
|
+
} from "./chunk-B5IE7V77.js";
|
|
5
|
+
import {
|
|
6
|
+
APPWARDEN_CACHE_KEY,
|
|
7
|
+
APPWARDEN_TEST_ROUTE,
|
|
8
|
+
debug,
|
|
9
|
+
printMessage
|
|
10
|
+
} from "./chunk-7UTT3M2S.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
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getErrors
|
|
3
|
+
} from "./chunk-B5IE7V77.js";
|
|
4
|
+
import {
|
|
5
|
+
printMessage
|
|
6
|
+
} from "./chunk-7UTT3M2S.js";
|
|
7
|
+
|
|
8
|
+
// src/utils/validate-config.ts
|
|
9
|
+
function validateConfig(config, schema) {
|
|
10
|
+
const result = schema.safeParse(config);
|
|
11
|
+
const hasErrors = !result.success;
|
|
12
|
+
if (hasErrors) {
|
|
13
|
+
const mappedErrors = getErrors(result.error);
|
|
14
|
+
if (mappedErrors.length > 0) {
|
|
15
|
+
for (const error of mappedErrors) {
|
|
16
|
+
console.error(printMessage(error));
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
console.error(printMessage(result.error.message));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return hasErrors;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
validateConfig
|
|
27
|
+
};
|
|
@@ -3,7 +3,6 @@ var LOCKDOWN_TEST_EXPIRY_MS = 5 * 60 * 1e3;
|
|
|
3
3
|
var errors = { badCacheConnection: "BAD_CACHE_CONNECTION" };
|
|
4
4
|
var globalErrors = [errors.badCacheConnection];
|
|
5
5
|
var APPWARDEN_TEST_ROUTE = "/_appwarden/test";
|
|
6
|
-
var APPWARDEN_USER_AGENT = "Appwarden-Monitor";
|
|
7
6
|
var APPWARDEN_CACHE_KEY = "appwarden-lock";
|
|
8
7
|
|
|
9
8
|
// src/utils/debug.ts
|
|
@@ -17,13 +16,22 @@ var debug = (...msg) => {
|
|
|
17
16
|
var addSlashes = (str) => str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\u0000/g, "\\0").replace(/<\/script>/gi, "<\\/script>");
|
|
18
17
|
var printMessage = (message) => `[@appwarden/middleware] ${addSlashes(message)}`;
|
|
19
18
|
|
|
19
|
+
// src/utils/request-checks.ts
|
|
20
|
+
function isHTMLResponse(response) {
|
|
21
|
+
return response.headers.get("Content-Type")?.includes("text/html") ?? false;
|
|
22
|
+
}
|
|
23
|
+
function isHTMLRequest(request) {
|
|
24
|
+
return request.headers.get("accept")?.includes("text/html") ?? false;
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
export {
|
|
21
28
|
LOCKDOWN_TEST_EXPIRY_MS,
|
|
22
29
|
errors,
|
|
23
30
|
globalErrors,
|
|
24
31
|
APPWARDEN_TEST_ROUTE,
|
|
25
|
-
APPWARDEN_USER_AGENT,
|
|
26
32
|
APPWARDEN_CACHE_KEY,
|
|
27
33
|
debug,
|
|
28
|
-
printMessage
|
|
34
|
+
printMessage,
|
|
35
|
+
isHTMLResponse,
|
|
36
|
+
isHTMLRequest
|
|
29
37
|
};
|
|
@@ -1,39 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LOCKDOWN_TEST_EXPIRY_MS
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
|
|
5
|
-
// src/utils/errors.ts
|
|
6
|
-
var errorsMap = {
|
|
7
|
-
mode: '`CSP_MODE` must be one of "disabled", "report-only", or "enforced"',
|
|
8
|
-
directives: {
|
|
9
|
-
["DirectivesRequired" /* DirectivesRequired */]: '`CSP_DIRECTIVES` must be provided when `CSP_MODE` is "report-only" or "enforced"',
|
|
10
|
-
["DirectivesBadParse" /* DirectivesBadParse */]: "Failed to parse `CSP_DIRECTIVES`. Is it a valid JSON string?"
|
|
11
|
-
},
|
|
12
|
-
appwardenApiToken: "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/guides/api-token-management."
|
|
13
|
-
};
|
|
14
|
-
var getErrors = (error) => {
|
|
15
|
-
const matches = [];
|
|
16
|
-
const errors = [...Object.entries(error.flatten().fieldErrors)];
|
|
17
|
-
for (const issue of error.issues) {
|
|
18
|
-
errors.push(
|
|
19
|
-
...Object.entries(
|
|
20
|
-
"returnTypeError" in issue ? issue.returnTypeError.flatten().fieldErrors : {}
|
|
21
|
-
)
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
for (const [field, maybeSchemaErrorKey] of errors) {
|
|
25
|
-
let match = errorsMap[field];
|
|
26
|
-
if (match) {
|
|
27
|
-
if (match instanceof Object) {
|
|
28
|
-
if (maybeSchemaErrorKey) {
|
|
29
|
-
match = match[maybeSchemaErrorKey[0]];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
matches.push(match);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return matches;
|
|
36
|
-
};
|
|
3
|
+
} from "./chunk-7UTT3M2S.js";
|
|
37
4
|
|
|
38
5
|
// src/utils/memory-cache.ts
|
|
39
6
|
var MemoryCache = class {
|
|
@@ -82,6 +49,39 @@ var MemoryCache = class {
|
|
|
82
49
|
};
|
|
83
50
|
};
|
|
84
51
|
|
|
52
|
+
// src/utils/errors.ts
|
|
53
|
+
var errorsMap = {
|
|
54
|
+
mode: '`CSP_MODE` must be one of "disabled", "report-only", or "enforced"',
|
|
55
|
+
directives: {
|
|
56
|
+
["DirectivesRequired" /* DirectivesRequired */]: '`CSP_DIRECTIVES` must be provided when `CSP_MODE` is "report-only" or "enforced"',
|
|
57
|
+
["DirectivesBadParse" /* DirectivesBadParse */]: "Failed to parse `CSP_DIRECTIVES`. Is it a valid JSON string?"
|
|
58
|
+
},
|
|
59
|
+
appwardenApiToken: "Please provide a valid `appwardenApiToken`. Learn more at https://appwarden.com/docs/guides/api-token-management."
|
|
60
|
+
};
|
|
61
|
+
var getErrors = (error) => {
|
|
62
|
+
const matches = [];
|
|
63
|
+
const errors = [...Object.entries(error.flatten().fieldErrors)];
|
|
64
|
+
for (const issue of error.issues) {
|
|
65
|
+
errors.push(
|
|
66
|
+
...Object.entries(
|
|
67
|
+
"returnTypeError" in issue ? issue.returnTypeError.flatten().fieldErrors : {}
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
for (const [field, maybeSchemaErrorKey] of errors) {
|
|
72
|
+
let match = errorsMap[field];
|
|
73
|
+
if (match) {
|
|
74
|
+
if (match instanceof Object) {
|
|
75
|
+
if (maybeSchemaErrorKey) {
|
|
76
|
+
match = match[maybeSchemaErrorKey[0]];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
matches.push(match);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return matches;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
85
|
// src/schemas/helpers.ts
|
|
86
86
|
import { z } from "zod";
|
|
87
87
|
var BoolOrStringSchema = z.union([z.string(), z.boolean()]).optional();
|
|
@@ -93,6 +93,7 @@ var BooleanSchema = BoolOrStringSchema.transform((val) => {
|
|
|
93
93
|
}
|
|
94
94
|
throw new Error("Invalid value");
|
|
95
95
|
});
|
|
96
|
+
var AppwardenApiTokenSchema = z.string().refine((val) => !!val, { message: "appwardenApiToken is required" });
|
|
96
97
|
var LockValue = z.object({
|
|
97
98
|
isLocked: z.number(),
|
|
98
99
|
isLockedTest: z.number(),
|
|
@@ -104,5 +105,6 @@ export {
|
|
|
104
105
|
getErrors,
|
|
105
106
|
MemoryCache,
|
|
106
107
|
BooleanSchema,
|
|
108
|
+
AppwardenApiTokenSchema,
|
|
107
109
|
LockValue
|
|
108
110
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
debug,
|
|
3
|
+
isHTMLResponse,
|
|
3
4
|
printMessage
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-7UTT3M2S.js";
|
|
5
6
|
|
|
6
7
|
// src/schemas/use-content-security-policy.ts
|
|
7
8
|
import { z as z2 } from "zod";
|
|
@@ -126,7 +127,7 @@ var useContentSecurityPolicy = (input) => {
|
|
|
126
127
|
debug(printMessage("csp is disabled"));
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
|
-
if (response.headers.has("Content-Type") && !response
|
|
130
|
+
if (response.headers.has("Content-Type") && !isHTMLResponse(response)) {
|
|
130
131
|
return;
|
|
131
132
|
}
|
|
132
133
|
const cspNonce = crypto.randomUUID();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/utils/build-lock-page-url.ts
|
|
2
|
+
function buildLockPageUrl(lockPageSlug, requestUrl) {
|
|
3
|
+
const normalizedSlug = lockPageSlug.startsWith("/") ? lockPageSlug : `/${lockPageSlug}`;
|
|
4
|
+
return new URL(normalizedSlug, requestUrl);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/utils/create-redirect.ts
|
|
8
|
+
var TEMPORARY_REDIRECT_STATUS = 302;
|
|
9
|
+
var createRedirect = (url) => {
|
|
10
|
+
return new Response(null, {
|
|
11
|
+
status: TEMPORARY_REDIRECT_STATUS,
|
|
12
|
+
headers: {
|
|
13
|
+
Location: url.toString()
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
buildLockPageUrl,
|
|
20
|
+
TEMPORARY_REDIRECT_STATUS,
|
|
21
|
+
createRedirect
|
|
22
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare runtime context provided by Astro on Cloudflare Workers.
|
|
3
|
+
* This is the shape of `context.locals.runtime` when using @astrojs/cloudflare adapter.
|
|
4
|
+
*/
|
|
5
|
+
interface AstroCloudflareRuntime {
|
|
6
|
+
env: CloudflareEnv;
|
|
7
|
+
ctx: ExecutionContext;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for the Appwarden middleware.
|
|
11
|
+
*/
|
|
12
|
+
interface AstroAppwardenConfig {
|
|
13
|
+
/** The slug/path of the lock page to redirect to when the site is locked */
|
|
14
|
+
lockPageSlug: string;
|
|
15
|
+
/** The Appwarden API token for authentication */
|
|
16
|
+
appwardenApiToken: string;
|
|
17
|
+
/** Optional custom API hostname (defaults to https://api.appwarden.io) */
|
|
18
|
+
appwardenApiHostname?: string;
|
|
19
|
+
/** Enable debug logging */
|
|
20
|
+
debug?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Configuration function that receives the Cloudflare runtime and returns the config.
|
|
24
|
+
* This allows dynamic configuration based on environment variables.
|
|
25
|
+
*/
|
|
26
|
+
type AstroConfigFn = (runtime: AstroCloudflareRuntime) => AstroAppwardenConfig;
|
|
27
|
+
/**
|
|
28
|
+
* Astro middleware context type.
|
|
29
|
+
* This matches Astro's APIContext shape for middleware.
|
|
30
|
+
*/
|
|
31
|
+
interface AstroMiddlewareContext {
|
|
32
|
+
/** The incoming request */
|
|
33
|
+
request: Request;
|
|
34
|
+
/** Object for storing request-specific data */
|
|
35
|
+
locals: {
|
|
36
|
+
runtime?: AstroCloudflareRuntime;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
};
|
|
39
|
+
/** Helper to create redirect responses */
|
|
40
|
+
redirect: (path: string, status?: number) => Response;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Astro middleware function signature.
|
|
44
|
+
* This matches the onRequest export type in Astro's middleware system.
|
|
45
|
+
*/
|
|
46
|
+
type AstroMiddlewareFunction = (context: AstroMiddlewareContext, next: () => Promise<Response>) => Promise<Response>;
|
|
47
|
+
/**
|
|
48
|
+
* Creates an Appwarden middleware function for Astro.
|
|
49
|
+
*
|
|
50
|
+
* This middleware checks if the site is locked and redirects to the lock page if so.
|
|
51
|
+
* It should be used with Astro's `sequence()` function or exported directly as `onRequest`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* // src/middleware.ts
|
|
56
|
+
* import { sequence } from "astro:middleware"
|
|
57
|
+
* import { createAppwardenMiddleware } from "@appwarden/middleware/astro"
|
|
58
|
+
*
|
|
59
|
+
* const appwarden = createAppwardenMiddleware(({ env }) => ({
|
|
60
|
+
* lockPageSlug: env.APPWARDEN_LOCK_PAGE_SLUG,
|
|
61
|
+
* appwardenApiToken: env.APPWARDEN_API_TOKEN,
|
|
62
|
+
* }))
|
|
63
|
+
*
|
|
64
|
+
* export const onRequest = sequence(appwarden)
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @param configFn - A function that receives the Cloudflare runtime and returns the config
|
|
68
|
+
* @returns An Astro middleware function
|
|
69
|
+
*/
|
|
70
|
+
declare function createAppwardenMiddleware(configFn: AstroConfigFn): AstroMiddlewareFunction;
|
|
71
|
+
|
|
72
|
+
export { type AstroAppwardenConfig, type AstroCloudflareRuntime, type AstroConfigFn, type AstroMiddlewareContext, type AstroMiddlewareFunction, createAppwardenMiddleware };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TEMPORARY_REDIRECT_STATUS,
|
|
3
|
+
buildLockPageUrl,
|
|
4
|
+
createRedirect
|
|
5
|
+
} from "../chunk-N6AUTMZO.js";
|
|
6
|
+
import {
|
|
7
|
+
validateConfig
|
|
8
|
+
} from "../chunk-6PUA5YXP.js";
|
|
9
|
+
import {
|
|
10
|
+
checkLockStatus
|
|
11
|
+
} from "../chunk-5DEXVBY6.js";
|
|
12
|
+
import {
|
|
13
|
+
AppwardenApiTokenSchema,
|
|
14
|
+
BooleanSchema
|
|
15
|
+
} from "../chunk-B5IE7V77.js";
|
|
16
|
+
import {
|
|
17
|
+
isHTMLRequest,
|
|
18
|
+
printMessage
|
|
19
|
+
} from "../chunk-7UTT3M2S.js";
|
|
20
|
+
|
|
21
|
+
// src/schemas/astro-cloudflare.ts
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
var AstroCloudflareConfigSchema = z.object({
|
|
24
|
+
/** The slug/path of the lock page to redirect to when the site is locked */
|
|
25
|
+
lockPageSlug: z.string(),
|
|
26
|
+
/** The Appwarden API token for authentication */
|
|
27
|
+
appwardenApiToken: AppwardenApiTokenSchema,
|
|
28
|
+
/** Optional custom API hostname (defaults to https://api.appwarden.io) */
|
|
29
|
+
appwardenApiHostname: z.string().optional(),
|
|
30
|
+
/** Enable debug logging */
|
|
31
|
+
debug: BooleanSchema.default(false)
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// src/adapters/astro-cloudflare.ts
|
|
35
|
+
function createAppwardenMiddleware(configFn) {
|
|
36
|
+
return async (context, next) => {
|
|
37
|
+
const { request, locals } = context;
|
|
38
|
+
try {
|
|
39
|
+
const runtime = locals.runtime;
|
|
40
|
+
if (!runtime) {
|
|
41
|
+
console.error(
|
|
42
|
+
printMessage(
|
|
43
|
+
"Cloudflare runtime not found. Ensure @astrojs/cloudflare adapter is configured."
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
return next();
|
|
47
|
+
}
|
|
48
|
+
if (!isHTMLRequest(request)) {
|
|
49
|
+
return next();
|
|
50
|
+
}
|
|
51
|
+
const config = configFn(runtime);
|
|
52
|
+
const hasError = validateConfig(config, AstroCloudflareConfigSchema);
|
|
53
|
+
if (hasError) {
|
|
54
|
+
return next();
|
|
55
|
+
}
|
|
56
|
+
const result = await checkLockStatus({
|
|
57
|
+
request,
|
|
58
|
+
appwardenApiToken: config.appwardenApiToken,
|
|
59
|
+
appwardenApiHostname: config.appwardenApiHostname,
|
|
60
|
+
debug: config.debug,
|
|
61
|
+
lockPageSlug: config.lockPageSlug,
|
|
62
|
+
waitUntil: (fn) => runtime.ctx.waitUntil(fn)
|
|
63
|
+
});
|
|
64
|
+
if (result.isLocked) {
|
|
65
|
+
const lockPageUrl = buildLockPageUrl(config.lockPageSlug, request.url);
|
|
66
|
+
if (context.redirect) {
|
|
67
|
+
return context.redirect(
|
|
68
|
+
lockPageUrl.toString(),
|
|
69
|
+
TEMPORARY_REDIRECT_STATUS
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return createRedirect(lockPageUrl);
|
|
73
|
+
}
|
|
74
|
+
return next();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof Response) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
console.error(
|
|
80
|
+
printMessage(
|
|
81
|
+
`Unhandled error: ${error instanceof Error ? error.message : String(error)}`
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
return next();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
createAppwardenMiddleware
|
|
90
|
+
};
|