@emdash-cms/cloudflare 0.0.1

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.
Files changed (67) hide show
  1. package/dist/auth/index.d.mts +81 -0
  2. package/dist/auth/index.mjs +147 -0
  3. package/dist/cache/config.d.mts +52 -0
  4. package/dist/cache/config.mjs +55 -0
  5. package/dist/cache/runtime.d.mts +40 -0
  6. package/dist/cache/runtime.mjs +191 -0
  7. package/dist/d1-introspector-bZf0_ylK.mjs +57 -0
  8. package/dist/db/d1.d.mts +43 -0
  9. package/dist/db/d1.mjs +74 -0
  10. package/dist/db/do.d.mts +96 -0
  11. package/dist/db/do.mjs +489 -0
  12. package/dist/db/playground-middleware.d.mts +20 -0
  13. package/dist/db/playground-middleware.mjs +533 -0
  14. package/dist/db/playground.d.mts +39 -0
  15. package/dist/db/playground.mjs +26 -0
  16. package/dist/do-class-DY2Ba2RJ.mjs +174 -0
  17. package/dist/do-class-x5Xh_G62.d.mts +73 -0
  18. package/dist/do-dialect-BhFcRSFQ.mjs +58 -0
  19. package/dist/do-playground-routes-CmwFeGwJ.mjs +49 -0
  20. package/dist/do-types-CY0G0oyh.d.mts +14 -0
  21. package/dist/images-4RT9Ag8_.d.mts +76 -0
  22. package/dist/index.d.mts +200 -0
  23. package/dist/index.mjs +214 -0
  24. package/dist/media/images-runtime.d.mts +10 -0
  25. package/dist/media/images-runtime.mjs +215 -0
  26. package/dist/media/stream-runtime.d.mts +10 -0
  27. package/dist/media/stream-runtime.mjs +218 -0
  28. package/dist/plugins/index.d.mts +32 -0
  29. package/dist/plugins/index.mjs +163 -0
  30. package/dist/sandbox/index.d.mts +255 -0
  31. package/dist/sandbox/index.mjs +945 -0
  32. package/dist/storage/r2.d.mts +31 -0
  33. package/dist/storage/r2.mjs +116 -0
  34. package/dist/stream-DdbcvKi0.d.mts +78 -0
  35. package/package.json +109 -0
  36. package/src/auth/cloudflare-access.ts +303 -0
  37. package/src/auth/index.ts +16 -0
  38. package/src/cache/config.ts +81 -0
  39. package/src/cache/runtime.ts +328 -0
  40. package/src/cloudflare.d.ts +31 -0
  41. package/src/db/d1-introspector.ts +120 -0
  42. package/src/db/d1.ts +112 -0
  43. package/src/db/do-class.ts +275 -0
  44. package/src/db/do-dialect.ts +125 -0
  45. package/src/db/do-playground-routes.ts +65 -0
  46. package/src/db/do-preview-routes.ts +48 -0
  47. package/src/db/do-preview-sign.ts +100 -0
  48. package/src/db/do-preview.ts +268 -0
  49. package/src/db/do-types.ts +12 -0
  50. package/src/db/do.ts +62 -0
  51. package/src/db/playground-middleware.ts +340 -0
  52. package/src/db/playground-toolbar.ts +341 -0
  53. package/src/db/playground.ts +49 -0
  54. package/src/db/preview-toolbar.ts +220 -0
  55. package/src/index.ts +285 -0
  56. package/src/media/images-runtime.ts +353 -0
  57. package/src/media/images.ts +114 -0
  58. package/src/media/stream-runtime.ts +392 -0
  59. package/src/media/stream.ts +118 -0
  60. package/src/plugins/index.ts +7 -0
  61. package/src/plugins/vectorize-search.ts +393 -0
  62. package/src/sandbox/bridge.ts +1008 -0
  63. package/src/sandbox/index.ts +13 -0
  64. package/src/sandbox/runner.ts +357 -0
  65. package/src/sandbox/types.ts +181 -0
  66. package/src/sandbox/wrapper.ts +238 -0
  67. package/src/storage/r2.ts +200 -0
@@ -0,0 +1,81 @@
1
+ import { AuthResult } from "emdash";
2
+ import { JWTPayload } from "jose";
3
+
4
+ //#region src/auth/cloudflare-access.d.ts
5
+ /**
6
+ * Configuration for Cloudflare Access authentication
7
+ *
8
+ * Note: This interface is duplicated in ../index.ts for config-time usage.
9
+ * Keep them in sync.
10
+ */
11
+ interface AccessConfig {
12
+ /**
13
+ * Your Cloudflare Access team domain
14
+ * @example "myteam.cloudflareaccess.com"
15
+ */
16
+ teamDomain: string;
17
+ /**
18
+ * Application Audience (AUD) tag from Access application settings.
19
+ * For Cloudflare Workers, use `audienceEnvVar` instead to read at runtime.
20
+ */
21
+ audience?: string;
22
+ /**
23
+ * Environment variable name containing the audience tag.
24
+ * Read at runtime from environment.
25
+ * @default "CF_ACCESS_AUDIENCE"
26
+ */
27
+ audienceEnvVar?: string;
28
+ /**
29
+ * Role level for users not matching any group in roleMapping
30
+ * @default 30 (Editor)
31
+ */
32
+ defaultRole?: number;
33
+ /**
34
+ * Map IdP group names to EmDash role levels
35
+ */
36
+ roleMapping?: Record<string, number>;
37
+ }
38
+ /**
39
+ * Cloudflare Access JWT payload extends standard JWT with email claim
40
+ */
41
+ interface AccessJwtPayload extends JWTPayload {
42
+ /** User's email address (Access-specific claim) */
43
+ email: string;
44
+ }
45
+ /**
46
+ * Group from IdP (returned by get-identity endpoint)
47
+ */
48
+ interface AccessGroup {
49
+ id: string;
50
+ name: string;
51
+ email?: string;
52
+ }
53
+ /**
54
+ * Full identity from Access get-identity endpoint
55
+ */
56
+ interface AccessIdentity {
57
+ /** Unique identity ID */
58
+ id: string;
59
+ /** User's display name (may be undefined if IdP doesn't provide it) */
60
+ name?: string;
61
+ /** User's email address */
62
+ email: string;
63
+ /** Groups from IdP */
64
+ groups: AccessGroup[];
65
+ /** Identity provider info */
66
+ idp: {
67
+ id: string;
68
+ type: string;
69
+ };
70
+ /** Custom OIDC claims from IdP */
71
+ oidc_fields?: Record<string, unknown>;
72
+ /** SAML attributes from IdP */
73
+ saml_attributes?: Record<string, unknown>;
74
+ /** User's country (from geo) */
75
+ geo?: {
76
+ country: string;
77
+ };
78
+ }
79
+ declare function authenticate(request: Request, config: unknown): Promise<AuthResult>;
80
+ //#endregion
81
+ export { type AccessConfig, type AccessGroup, type AccessIdentity, type AccessJwtPayload, authenticate };
@@ -0,0 +1,147 @@
1
+ import { createRemoteJWKSet, jwtVerify } from "jose";
2
+
3
+ //#region src/auth/cloudflare-access.ts
4
+ /**
5
+ * Cloudflare Access Authentication - RUNTIME MODULE
6
+ *
7
+ * When EmDash is deployed behind Cloudflare Access, this module handles
8
+ * JWT validation and user provisioning from Access identity.
9
+ *
10
+ * Uses jose for JWT verification - works in all runtimes.
11
+ *
12
+ * This is loaded at runtime via the auth provider system.
13
+ * Do not import at config time.
14
+ */
15
+ const jwksCache = /* @__PURE__ */ new Map();
16
+ /** Regex to extract CF_Authorization cookie value */
17
+ const CF_AUTHORIZATION_COOKIE_REGEX = /CF_Authorization=([^;]+)/;
18
+ /**
19
+ * Get or create a JWKS client for the given team domain
20
+ */
21
+ function getJwks(teamDomain) {
22
+ let jwks = jwksCache.get(teamDomain);
23
+ if (!jwks) {
24
+ jwks = createRemoteJWKSet(new URL(`https://${teamDomain}/cdn-cgi/access/certs`));
25
+ jwksCache.set(teamDomain, jwks);
26
+ }
27
+ return jwks;
28
+ }
29
+ /** Default environment variable name for Access audience */
30
+ const DEFAULT_AUDIENCE_ENV_VAR = "CF_ACCESS_AUDIENCE";
31
+ /**
32
+ * Resolve the audience value from config.
33
+ * Supports direct value or reading from environment variable.
34
+ */
35
+ function resolveAudience(config) {
36
+ if (config.audience) return config.audience;
37
+ const envVarName = config.audienceEnvVar ?? DEFAULT_AUDIENCE_ENV_VAR;
38
+ const value = process.env[envVarName];
39
+ if (typeof value === "string" && value) return value;
40
+ throw new Error(`Environment variable "${envVarName}" not found or empty. Set it via wrangler secret, .dev.vars, or environment.`);
41
+ }
42
+ /**
43
+ * Validate a Cloudflare Access JWT using jose
44
+ *
45
+ * @param jwt The JWT string from header or cookie
46
+ * @param config Access configuration
47
+ * @returns Decoded and validated JWT payload
48
+ * @throws Error if validation fails
49
+ */
50
+ async function validateAccessJwt(jwt, config) {
51
+ const audience = resolveAudience(config);
52
+ const issuer = `https://${config.teamDomain}`;
53
+ const { payload } = await jwtVerify(jwt, getJwks(config.teamDomain), {
54
+ issuer,
55
+ audience,
56
+ clockTolerance: 60
57
+ });
58
+ return payload;
59
+ }
60
+ /**
61
+ * Extract Access JWT from request
62
+ *
63
+ * Checks header first (more reliable), then falls back to cookie.
64
+ *
65
+ * @param request The incoming request
66
+ * @returns JWT string or null if not present
67
+ */
68
+ function extractAccessJwt(request) {
69
+ const headerJwt = request.headers.get("Cf-Access-Jwt-Assertion");
70
+ if (headerJwt) return headerJwt;
71
+ return (request.headers.get("Cookie") || "").match(CF_AUTHORIZATION_COOKIE_REGEX)?.[1] || null;
72
+ }
73
+ /**
74
+ * Fetch full identity from Access (includes groups)
75
+ *
76
+ * The JWT itself only contains basic claims. To get groups and other
77
+ * IdP attributes, we need to call the get-identity endpoint.
78
+ *
79
+ * @param jwt The JWT string
80
+ * @param teamDomain The Access team domain
81
+ * @returns Full identity including groups
82
+ */
83
+ async function getAccessIdentity(jwt, teamDomain) {
84
+ const response = await fetch(`https://${teamDomain}/cdn-cgi/access/get-identity`, { headers: { Cookie: `CF_Authorization=${jwt}` } });
85
+ if (!response.ok) throw new Error(`Failed to fetch identity: ${response.status}`);
86
+ return response.json();
87
+ }
88
+ /**
89
+ * Resolve role from IdP groups using roleMapping config
90
+ *
91
+ * @param groups User's groups from IdP
92
+ * @param config Access configuration
93
+ * @returns Role level (e.g., 50 for Admin, 30 for Editor)
94
+ */
95
+ function resolveRoleFromGroups(groups, config) {
96
+ const defaultRole = config.defaultRole ?? 30;
97
+ if (!config.roleMapping) return defaultRole;
98
+ for (const group of groups) {
99
+ const role = config.roleMapping[group.name];
100
+ if (role !== void 0) return role;
101
+ }
102
+ return defaultRole;
103
+ }
104
+ /**
105
+ * Authenticate a request using Cloudflare Access
106
+ *
107
+ * This is the main entry point for Access authentication.
108
+ * It validates the JWT, fetches the full identity, and resolves the role.
109
+ *
110
+ * This function implements the AuthProviderModule.authenticate interface.
111
+ *
112
+ * @param request The incoming request
113
+ * @param config Access configuration (passed from AuthDescriptor)
114
+ * @returns Authentication result with user info and role
115
+ * @throws Error if authentication fails
116
+ */
117
+ function isAccessConfig(value) {
118
+ return value != null && typeof value === "object" && "teamDomain" in value && typeof value.teamDomain === "string";
119
+ }
120
+ async function authenticate(request, config) {
121
+ if (!isAccessConfig(config)) throw new Error("Invalid Cloudflare Access config: teamDomain is required");
122
+ const accessConfig = config;
123
+ const jwt = extractAccessJwt(request);
124
+ if (!jwt) throw new Error("No Access JWT present");
125
+ const payload = await validateAccessJwt(jwt, accessConfig);
126
+ const identity = await getAccessIdentity(jwt, accessConfig.teamDomain);
127
+ const role = resolveRoleFromGroups(identity.groups, accessConfig);
128
+ console.log("[cf-access] Identity from Access:", JSON.stringify({
129
+ email: identity.email,
130
+ name: identity.name,
131
+ groups: identity.groups?.map((g) => g.name),
132
+ idp: identity.idp
133
+ }));
134
+ return {
135
+ email: identity.email,
136
+ name: identity.name ?? identity.email.split("@")[0] ?? "Unknown",
137
+ role,
138
+ subject: payload.sub,
139
+ metadata: {
140
+ groups: identity.groups,
141
+ idp: identity.idp
142
+ }
143
+ };
144
+ }
145
+
146
+ //#endregion
147
+ export { authenticate };
@@ -0,0 +1,52 @@
1
+ import { CloudflareCacheConfig } from "./runtime.mjs";
2
+ import { CacheProviderConfig } from "astro";
3
+
4
+ //#region src/cache/config.d.ts
5
+ /**
6
+ * Cloudflare Cache API route cache provider.
7
+ *
8
+ * Uses the Workers Cache API (`cache.put()`/`cache.match()`) to cache
9
+ * rendered route responses at the edge. Invalidation uses the Cloudflare
10
+ * purge-by-tag REST API for global purge across all edge locations.
11
+ *
12
+ * This is a stopgap until CacheW provides native distributed caching
13
+ * for Workers. Worker responses can't go through the CDN cache today,
14
+ * so we use the Cache API directly. The standard `Cache-Tag` header is
15
+ * set on stored responses so the purge-by-tag API can find them.
16
+ *
17
+ * Tag-based invalidation requires a Zone ID and an API token with
18
+ * "Cache Purge" permission. These can be passed directly in the config
19
+ * or read from environment variables at runtime (default: `CF_ZONE_ID`
20
+ * and `CF_CACHE_PURGE_TOKEN`).
21
+ *
22
+ * @param config Optional configuration.
23
+ * @returns A {@link CacheProviderConfig} to pass to `experimental.cache.provider`.
24
+ *
25
+ * @example Basic usage (reads zone ID and token from env vars)
26
+ * ```ts
27
+ * import { defineConfig } from "astro/config";
28
+ * import cloudflare from "@astrojs/cloudflare";
29
+ * import { cloudflareCache } from "@emdash-cms/cloudflare";
30
+ *
31
+ * export default defineConfig({
32
+ * adapter: cloudflare(),
33
+ * experimental: {
34
+ * cache: {
35
+ * provider: cloudflareCache(),
36
+ * },
37
+ * },
38
+ * });
39
+ * ```
40
+ *
41
+ * @example With explicit config
42
+ * ```ts
43
+ * cloudflareCache({
44
+ * cacheName: "my-site",
45
+ * zoneId: "abc123...",
46
+ * apiToken: "xyz789...",
47
+ * })
48
+ * ```
49
+ */
50
+ declare function cloudflareCache(config?: CloudflareCacheConfig): CacheProviderConfig<CloudflareCacheConfig>;
51
+ //#endregion
52
+ export { type CloudflareCacheConfig, cloudflareCache };
@@ -0,0 +1,55 @@
1
+ //#region src/cache/config.ts
2
+ /**
3
+ * Cloudflare Cache API route cache provider.
4
+ *
5
+ * Uses the Workers Cache API (`cache.put()`/`cache.match()`) to cache
6
+ * rendered route responses at the edge. Invalidation uses the Cloudflare
7
+ * purge-by-tag REST API for global purge across all edge locations.
8
+ *
9
+ * This is a stopgap until CacheW provides native distributed caching
10
+ * for Workers. Worker responses can't go through the CDN cache today,
11
+ * so we use the Cache API directly. The standard `Cache-Tag` header is
12
+ * set on stored responses so the purge-by-tag API can find them.
13
+ *
14
+ * Tag-based invalidation requires a Zone ID and an API token with
15
+ * "Cache Purge" permission. These can be passed directly in the config
16
+ * or read from environment variables at runtime (default: `CF_ZONE_ID`
17
+ * and `CF_CACHE_PURGE_TOKEN`).
18
+ *
19
+ * @param config Optional configuration.
20
+ * @returns A {@link CacheProviderConfig} to pass to `experimental.cache.provider`.
21
+ *
22
+ * @example Basic usage (reads zone ID and token from env vars)
23
+ * ```ts
24
+ * import { defineConfig } from "astro/config";
25
+ * import cloudflare from "@astrojs/cloudflare";
26
+ * import { cloudflareCache } from "@emdash-cms/cloudflare";
27
+ *
28
+ * export default defineConfig({
29
+ * adapter: cloudflare(),
30
+ * experimental: {
31
+ * cache: {
32
+ * provider: cloudflareCache(),
33
+ * },
34
+ * },
35
+ * });
36
+ * ```
37
+ *
38
+ * @example With explicit config
39
+ * ```ts
40
+ * cloudflareCache({
41
+ * cacheName: "my-site",
42
+ * zoneId: "abc123...",
43
+ * apiToken: "xyz789...",
44
+ * })
45
+ * ```
46
+ */
47
+ function cloudflareCache(config = {}) {
48
+ return {
49
+ entrypoint: "@emdash-cms/cloudflare/cache",
50
+ config
51
+ };
52
+ }
53
+
54
+ //#endregion
55
+ export { cloudflareCache };
@@ -0,0 +1,40 @@
1
+ import { CacheProviderFactory } from "astro";
2
+
3
+ //#region src/cache/runtime.d.ts
4
+ interface CloudflareCacheConfig {
5
+ /**
6
+ * Name of the Cache API cache to use.
7
+ * @default "emdash"
8
+ */
9
+ cacheName?: string;
10
+ /**
11
+ * D1 bookmark cookie name. Responses whose only Set-Cookie is this
12
+ * bookmark will have it stripped before caching. Responses with any
13
+ * other Set-Cookie headers will not be cached.
14
+ * @default "__ec_d1_bookmark"
15
+ */
16
+ bookmarkCookie?: string;
17
+ /**
18
+ * Cloudflare Zone ID. Required for tag-based invalidation.
19
+ * If not provided, reads from `zoneIdEnvVar` at runtime.
20
+ */
21
+ zoneId?: string;
22
+ /**
23
+ * Environment variable name containing the Zone ID.
24
+ * @default "CF_ZONE_ID"
25
+ */
26
+ zoneIdEnvVar?: string;
27
+ /**
28
+ * Cloudflare API token with Cache Purge permission.
29
+ * If not provided, reads from `apiTokenEnvVar` at runtime.
30
+ */
31
+ apiToken?: string;
32
+ /**
33
+ * Environment variable name containing the API token.
34
+ * @default "CF_CACHE_PURGE_TOKEN"
35
+ */
36
+ apiTokenEnvVar?: string;
37
+ }
38
+ declare const factory: CacheProviderFactory<CloudflareCacheConfig>;
39
+ //#endregion
40
+ export { CloudflareCacheConfig, factory as default };
@@ -0,0 +1,191 @@
1
+ import { env, waitUntil } from "cloudflare:workers";
2
+
3
+ //#region src/cache/runtime.ts
4
+ /**
5
+ * Internal headers stored on cached responses for freshness tracking.
6
+ * These are removed before returning to the client.
7
+ */
8
+ const STORED_AT_HEADER = "X-EmDash-Stored-At";
9
+ const MAX_AGE_HEADER = "X-EmDash-Max-Age";
10
+ const SWR_HEADER = "X-EmDash-SWR";
11
+ /** Cloudflare purge API base */
12
+ const CF_API_BASE = "https://api.cloudflare.com/client/v4";
13
+ /** Matches max-age in CDN-Cache-Control */
14
+ const MAX_AGE_REGEX = /max-age=(\d+)/;
15
+ /** Matches stale-while-revalidate in CDN-Cache-Control */
16
+ const SWR_REGEX = /stale-while-revalidate=(\d+)/;
17
+ /** Internal headers to strip before returning responses to the client */
18
+ const INTERNAL_HEADERS = [
19
+ STORED_AT_HEADER,
20
+ MAX_AGE_HEADER,
21
+ SWR_HEADER
22
+ ];
23
+ /** Default D1 bookmark cookie name (from @emdash-cms/cloudflare d1 config) */
24
+ const DEFAULT_BOOKMARK_COOKIE = "__ec_d1_bookmark";
25
+ /**
26
+ * Parse CDN-Cache-Control header for max-age and stale-while-revalidate.
27
+ */
28
+ function parseCdnCacheControl(header) {
29
+ let maxAge = 0;
30
+ let swr = 0;
31
+ if (!header) return {
32
+ maxAge,
33
+ swr
34
+ };
35
+ const maxAgeMatch = MAX_AGE_REGEX.exec(header);
36
+ if (maxAgeMatch) maxAge = parseInt(maxAgeMatch[1], 10) || 0;
37
+ const swrMatch = SWR_REGEX.exec(header);
38
+ if (swrMatch) swr = parseInt(swrMatch[1], 10) || 0;
39
+ return {
40
+ maxAge,
41
+ swr
42
+ };
43
+ }
44
+ /**
45
+ * Normalize a URL for use as a cache key.
46
+ * Strips common tracking query parameters and sorts the rest.
47
+ */
48
+ function normalizeCacheKey(url) {
49
+ const normalized = new URL(url.toString());
50
+ for (const param of [
51
+ "utm_source",
52
+ "utm_medium",
53
+ "utm_campaign",
54
+ "utm_term",
55
+ "utm_content",
56
+ "fbclid",
57
+ "gclid",
58
+ "gbraid",
59
+ "wbraid",
60
+ "dclid",
61
+ "msclkid",
62
+ "twclid",
63
+ "_ga",
64
+ "_gl"
65
+ ]) normalized.searchParams.delete(param);
66
+ normalized.searchParams.sort();
67
+ return normalized.toString();
68
+ }
69
+ /**
70
+ * Read a config value, falling back to an env var.
71
+ */
72
+ function resolveEnvValue(explicit, envVarName) {
73
+ if (explicit) return explicit;
74
+ return env[envVarName];
75
+ }
76
+ /**
77
+ * Strip internal tracking headers from a response before returning to client.
78
+ */
79
+ function stripInternalHeaders(response) {
80
+ for (const header of INTERNAL_HEADERS) response.headers.delete(header);
81
+ }
82
+ /**
83
+ * Check whether all Set-Cookie headers on a response are only the D1
84
+ * bookmark cookie. Returns true if we can safely strip them for caching.
85
+ * Returns false if there are non-bookmark cookies (session, auth, etc.)
86
+ * which means the response should NOT be cached.
87
+ */
88
+ function hasOnlyBookmarkCookies(response, bookmarkCookie) {
89
+ const cookies = response.headers.getSetCookie();
90
+ if (cookies.length === 0) return true;
91
+ return cookies.every((c) => c.startsWith(`${bookmarkCookie}=`));
92
+ }
93
+ /**
94
+ * Prepare a response for storage in the Cache API.
95
+ * - Adds internal tracking headers (stored-at, max-age, swr)
96
+ * - Strips Set-Cookie (only called when cookies are safe to strip)
97
+ *
98
+ * Returns null if the response has non-bookmark Set-Cookie headers
99
+ * and should not be cached.
100
+ */
101
+ function prepareForCache(response, maxAge, swr, bookmarkCookie) {
102
+ if (!hasOnlyBookmarkCookies(response, bookmarkCookie)) return null;
103
+ const prepared = new Response(response.body, response);
104
+ prepared.headers.set(STORED_AT_HEADER, String(Date.now()));
105
+ prepared.headers.set(MAX_AGE_HEADER, String(maxAge));
106
+ prepared.headers.set(SWR_HEADER, String(swr));
107
+ prepared.headers.delete("Set-Cookie");
108
+ return prepared;
109
+ }
110
+ const factory = (config) => {
111
+ const cacheName = config?.cacheName ?? "emdash";
112
+ const bookmarkCookie = config?.bookmarkCookie ?? DEFAULT_BOOKMARK_COOKIE;
113
+ const zoneIdEnvVar = config?.zoneIdEnvVar ?? "CF_ZONE_ID";
114
+ const apiTokenEnvVar = config?.apiTokenEnvVar ?? "CF_CACHE_PURGE_TOKEN";
115
+ async function getCache() {
116
+ return caches.open(cacheName);
117
+ }
118
+ return {
119
+ name: "cloudflare-cache-api",
120
+ async onRequest(context, next) {
121
+ if (context.request.method !== "GET") return next();
122
+ if ((context.request.headers.get("Cookie") ?? "").includes("astro-session=")) return next();
123
+ const cacheKey = normalizeCacheKey(context.url);
124
+ const cache = await getCache();
125
+ const cached = await cache.match(cacheKey);
126
+ if (cached) {
127
+ const storedAt = parseInt(cached.headers.get(STORED_AT_HEADER) ?? "0", 10);
128
+ const maxAge = parseInt(cached.headers.get(MAX_AGE_HEADER) ?? "0", 10);
129
+ const swr = parseInt(cached.headers.get(SWR_HEADER) ?? "0", 10);
130
+ const ageSeconds = (Date.now() - storedAt) / 1e3;
131
+ if (ageSeconds < maxAge) {
132
+ const hit = new Response(cached.body, cached);
133
+ hit.headers.set("X-Astro-Cache", "HIT");
134
+ stripInternalHeaders(hit);
135
+ return hit;
136
+ }
137
+ if (swr > 0 && ageSeconds < maxAge + swr) {
138
+ const stale = new Response(cached.body, cached);
139
+ stale.headers.set("X-Astro-Cache", "STALE");
140
+ stripInternalHeaders(stale);
141
+ waitUntil((async () => {
142
+ try {
143
+ const fresh = await next();
144
+ const parsed = parseCdnCacheControl(fresh.headers.get("CDN-Cache-Control"));
145
+ if (parsed.maxAge > 0 && fresh.ok) {
146
+ const toStore = prepareForCache(fresh, parsed.maxAge, parsed.swr, bookmarkCookie);
147
+ if (toStore) await cache.put(cacheKey, toStore);
148
+ }
149
+ } catch {}
150
+ })());
151
+ return stale;
152
+ }
153
+ await cache.delete(cacheKey);
154
+ }
155
+ const response = await next();
156
+ const { maxAge, swr } = parseCdnCacheControl(response.headers.get("CDN-Cache-Control"));
157
+ if (maxAge > 0 && response.ok) {
158
+ const toStore = prepareForCache(response.clone(), maxAge, swr, bookmarkCookie);
159
+ if (toStore) await cache.put(cacheKey, toStore);
160
+ const miss = new Response(response.body, response);
161
+ miss.headers.set("X-Astro-Cache", "MISS");
162
+ return miss;
163
+ }
164
+ return response;
165
+ },
166
+ async invalidate(options) {
167
+ if (options.tags) {
168
+ const zoneId = resolveEnvValue(config?.zoneId, zoneIdEnvVar);
169
+ const apiToken = resolveEnvValue(config?.apiToken, apiTokenEnvVar);
170
+ if (!zoneId || !apiToken) throw new Error(`[cloudflare-cache-api] Tag-based invalidation requires a Zone ID and API token. Set the ${zoneIdEnvVar} and ${apiTokenEnvVar} environment variables, or pass zoneId/apiToken in the cloudflareCache() config.`);
171
+ const tags = Array.isArray(options.tags) ? options.tags : [options.tags];
172
+ const response = await fetch(`${CF_API_BASE}/zones/${zoneId}/purge_cache`, {
173
+ method: "POST",
174
+ headers: {
175
+ Authorization: `Bearer ${apiToken}`,
176
+ "Content-Type": "application/json"
177
+ },
178
+ body: JSON.stringify({ tags })
179
+ });
180
+ if (!response.ok) {
181
+ const body = await response.text().catch(() => "");
182
+ throw new Error(`[cloudflare-cache-api] Cache purge failed (${response.status}): ${body}`);
183
+ }
184
+ }
185
+ if (options.path) await (await getCache()).delete(options.path);
186
+ }
187
+ };
188
+ };
189
+
190
+ //#endregion
191
+ export { factory as default };
@@ -0,0 +1,57 @@
1
+ import { sql } from "kysely";
2
+
3
+ //#region src/db/d1-introspector.ts
4
+ const DEFAULT_MIGRATION_TABLE = "kysely_migration";
5
+ const DEFAULT_MIGRATION_LOCK_TABLE = "kysely_migration_lock";
6
+ const SPLIT_PARENS_PATTERN = /[(),]/;
7
+ const WHITESPACE_PATTERN = /\s+/;
8
+ const QUOTES_PATTERN = /["`]/g;
9
+ var D1Introspector = class {
10
+ #db;
11
+ constructor(db) {
12
+ this.#db = db;
13
+ }
14
+ async getSchemas() {
15
+ return [];
16
+ }
17
+ async getTables(options = {}) {
18
+ let query = this.#db.selectFrom("sqlite_master").where("type", "in", ["table", "view"]).where("name", "not like", "sqlite_%").where("name", "not like", "_cf_%").select([
19
+ "name",
20
+ "sql",
21
+ "type"
22
+ ]).orderBy("name");
23
+ if (!options.withInternalKyselyTables) query = query.where("name", "!=", DEFAULT_MIGRATION_TABLE).where("name", "!=", DEFAULT_MIGRATION_LOCK_TABLE);
24
+ const tables = await query.execute();
25
+ const result = [];
26
+ for (const table of tables) {
27
+ const tableName = table.name;
28
+ const tableType = table.type;
29
+ const tableSql = table.sql;
30
+ const columns = await sql`SELECT * FROM pragma_table_info('${sql.raw(tableName)}')`.execute(this.#db);
31
+ let autoIncrementCol = tableSql?.split(SPLIT_PARENS_PATTERN)?.find((it) => it.toLowerCase().includes("autoincrement"))?.trimStart()?.split(WHITESPACE_PATTERN)?.[0]?.replace(QUOTES_PATTERN, "");
32
+ if (!autoIncrementCol) {
33
+ const pkCols = columns.rows.filter((r) => r.pk > 0);
34
+ if (pkCols.length === 1 && pkCols[0].type.toLowerCase() === "integer") autoIncrementCol = pkCols[0].name;
35
+ }
36
+ result.push({
37
+ name: tableName,
38
+ isView: tableType === "view",
39
+ columns: columns.rows.map((col) => ({
40
+ name: col.name,
41
+ dataType: col.type,
42
+ isNullable: !col.notnull,
43
+ isAutoIncrementing: col.name === autoIncrementCol,
44
+ hasDefaultValue: col.dflt_value != null,
45
+ comment: void 0
46
+ }))
47
+ });
48
+ }
49
+ return result;
50
+ }
51
+ async getMetadata(options) {
52
+ return { tables: await this.getTables(options) };
53
+ }
54
+ };
55
+
56
+ //#endregion
57
+ export { D1Introspector as t };
@@ -0,0 +1,43 @@
1
+ import { Dialect } from "kysely";
2
+
3
+ //#region src/db/d1.d.ts
4
+ /**
5
+ * D1 configuration (runtime type — matches the config-time type in index.ts)
6
+ */
7
+ interface D1Config {
8
+ binding: string;
9
+ session?: "disabled" | "auto" | "primary-first";
10
+ bookmarkCookie?: string;
11
+ }
12
+ /**
13
+ * Create a D1 dialect from config
14
+ *
15
+ * @param config - D1 configuration with binding name
16
+ */
17
+ declare function createDialect(config: D1Config): Dialect;
18
+ /**
19
+ * Whether D1 sessions are enabled in the config.
20
+ */
21
+ declare function isSessionEnabled(config: D1Config): boolean;
22
+ /**
23
+ * Get the raw D1 binding for creating sessions.
24
+ * Returns null if sessions are disabled.
25
+ */
26
+ declare function getD1Binding(config: D1Config): D1Database | null;
27
+ /**
28
+ * Get the default session constraint for the config's session mode.
29
+ */
30
+ declare function getDefaultConstraint(config: D1Config): string;
31
+ /**
32
+ * Get the cookie name used for storing D1 session bookmarks.
33
+ */
34
+ declare function getBookmarkCookieName(config: D1Config): string;
35
+ /**
36
+ * Create a Kysely dialect from a D1 session object.
37
+ *
38
+ * D1DatabaseSession has the same `prepare()` / `batch()` interface
39
+ * as D1Database, so we pass it directly to D1Dialect.
40
+ */
41
+ declare function createSessionDialect(session: D1Database): Dialect;
42
+ //#endregion
43
+ export { createDialect, createSessionDialect, getBookmarkCookieName, getD1Binding, getDefaultConstraint, isSessionEnabled };