@notionx/core 0.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.
Files changed (208) hide show
  1. package/dist/admin/index.d.ts +137 -0
  2. package/dist/admin/index.js +206 -0
  3. package/dist/admin/index.js.map +1 -0
  4. package/dist/admin/pages/index.d.ts +324 -0
  5. package/dist/admin/pages/index.js +827 -0
  6. package/dist/admin/pages/index.js.map +1 -0
  7. package/dist/auth/auth-pages/forgot-password.d.ts +20 -0
  8. package/dist/auth/auth-pages/forgot-password.js +70 -0
  9. package/dist/auth/auth-pages/forgot-password.js.map +1 -0
  10. package/dist/auth/auth-pages/index.d.ts +6 -0
  11. package/dist/auth/auth-pages/index.js +342 -0
  12. package/dist/auth/auth-pages/index.js.map +1 -0
  13. package/dist/auth/auth-pages/login.d.ts +30 -0
  14. package/dist/auth/auth-pages/login.js +125 -0
  15. package/dist/auth/auth-pages/login.js.map +1 -0
  16. package/dist/auth/auth-pages/register.d.ts +17 -0
  17. package/dist/auth/auth-pages/register.js +81 -0
  18. package/dist/auth/auth-pages/register.js.map +1 -0
  19. package/dist/auth/auth-pages/reset-password.d.ts +18 -0
  20. package/dist/auth/auth-pages/reset-password.js +72 -0
  21. package/dist/auth/auth-pages/reset-password.js.map +1 -0
  22. package/dist/auth/index.d.ts +72 -0
  23. package/dist/auth/index.js +1011 -0
  24. package/dist/auth/index.js.map +1 -0
  25. package/dist/auth/passwords.d.ts +6 -0
  26. package/dist/auth/passwords.js +79 -0
  27. package/dist/auth/passwords.js.map +1 -0
  28. package/dist/auth/rate-limit.d.ts +28 -0
  29. package/dist/auth/rate-limit.js +245 -0
  30. package/dist/auth/rate-limit.js.map +1 -0
  31. package/dist/auth/routes/google-callback.d.ts +6 -0
  32. package/dist/auth/routes/google-callback.js +404 -0
  33. package/dist/auth/routes/google-callback.js.map +1 -0
  34. package/dist/auth/routes/google.d.ts +6 -0
  35. package/dist/auth/routes/google.js +250 -0
  36. package/dist/auth/routes/google.js.map +1 -0
  37. package/dist/auth/routes/index.d.ts +22 -0
  38. package/dist/auth/routes/index.js +619 -0
  39. package/dist/auth/routes/index.js.map +1 -0
  40. package/dist/auth/routes/verify-email.d.ts +6 -0
  41. package/dist/auth/routes/verify-email.js +317 -0
  42. package/dist/auth/routes/verify-email.js.map +1 -0
  43. package/dist/auth/routes/viewer.d.ts +6 -0
  44. package/dist/auth/routes/viewer.js +372 -0
  45. package/dist/auth/routes/viewer.js.map +1 -0
  46. package/dist/auth/session.d.ts +9 -0
  47. package/dist/auth/session.js +1 -0
  48. package/dist/auth/session.js.map +1 -0
  49. package/dist/auth/turnstile.d.ts +20 -0
  50. package/dist/auth/turnstile.js +301 -0
  51. package/dist/auth/turnstile.js.map +1 -0
  52. package/dist/auth/user-session.d.ts +42 -0
  53. package/dist/auth/user-session.js +419 -0
  54. package/dist/auth/user-session.js.map +1 -0
  55. package/dist/auth/users.d.ts +112 -0
  56. package/dist/auth/users.js +558 -0
  57. package/dist/auth/users.js.map +1 -0
  58. package/dist/bootstrap-CN2g76M6.d.ts +67 -0
  59. package/dist/cache/index.d.ts +6 -0
  60. package/dist/cache/index.js +47 -0
  61. package/dist/cache/index.js.map +1 -0
  62. package/dist/content/admin-summary.d.ts +24 -0
  63. package/dist/content/admin-summary.js +36 -0
  64. package/dist/content/admin-summary.js.map +1 -0
  65. package/dist/content/index.d.ts +9 -0
  66. package/dist/content/index.js +473 -0
  67. package/dist/content/index.js.map +1 -0
  68. package/dist/content/models.d.ts +69 -0
  69. package/dist/content/models.js +24 -0
  70. package/dist/content/models.js.map +1 -0
  71. package/dist/content/prewarm.d.ts +28 -0
  72. package/dist/content/prewarm.js +56 -0
  73. package/dist/content/prewarm.js.map +1 -0
  74. package/dist/content/revalidate.d.ts +37 -0
  75. package/dist/content/revalidate.js +170 -0
  76. package/dist/content/revalidate.js.map +1 -0
  77. package/dist/content/search-index.d.ts +54 -0
  78. package/dist/content/search-index.js +172 -0
  79. package/dist/content/search-index.js.map +1 -0
  80. package/dist/content/search.d.ts +8 -0
  81. package/dist/content/search.js +57 -0
  82. package/dist/content/search.js.map +1 -0
  83. package/dist/doctor/cli.d.ts +1 -0
  84. package/dist/doctor/cli.js +360 -0
  85. package/dist/doctor/cli.js.map +1 -0
  86. package/dist/doctor/index.d.ts +139 -0
  87. package/dist/doctor/index.js +289 -0
  88. package/dist/doctor/index.js.map +1 -0
  89. package/dist/email/index.d.ts +38 -0
  90. package/dist/email/index.js +126 -0
  91. package/dist/email/index.js.map +1 -0
  92. package/dist/env-C5qu-0R-.d.ts +35 -0
  93. package/dist/hooks/index.d.ts +2 -0
  94. package/dist/hooks/index.js +1 -0
  95. package/dist/hooks/index.js.map +1 -0
  96. package/dist/i18n/index.d.ts +26 -0
  97. package/dist/i18n/index.js +73 -0
  98. package/dist/i18n/index.js.map +1 -0
  99. package/dist/index.d.ts +8 -0
  100. package/dist/index.js +1281 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/internal/admin/index.d.ts +75 -0
  103. package/dist/internal/admin/index.js +365 -0
  104. package/dist/internal/admin/index.js.map +1 -0
  105. package/dist/media/index.d.ts +24 -0
  106. package/dist/media/index.js +86 -0
  107. package/dist/media/index.js.map +1 -0
  108. package/dist/media/routes/index.d.ts +1 -0
  109. package/dist/media/routes/index.js +585 -0
  110. package/dist/media/routes/index.js.map +1 -0
  111. package/dist/media/routes/notion-media.d.ts +19 -0
  112. package/dist/media/routes/notion-media.js +588 -0
  113. package/dist/media/routes/notion-media.js.map +1 -0
  114. package/dist/middleware.d.ts +95 -0
  115. package/dist/middleware.js +79 -0
  116. package/dist/middleware.js.map +1 -0
  117. package/dist/notion/block-text.d.ts +5 -0
  118. package/dist/notion/block-text.js +37 -0
  119. package/dist/notion/block-text.js.map +1 -0
  120. package/dist/notion/blocks.d.ts +24 -0
  121. package/dist/notion/blocks.js +46 -0
  122. package/dist/notion/blocks.js.map +1 -0
  123. package/dist/notion/client.d.ts +7 -0
  124. package/dist/notion/client.js +13 -0
  125. package/dist/notion/client.js.map +1 -0
  126. package/dist/notion/config.d.ts +25 -0
  127. package/dist/notion/config.js +147 -0
  128. package/dist/notion/config.js.map +1 -0
  129. package/dist/notion/content-cache.d.ts +45 -0
  130. package/dist/notion/content-cache.js +166 -0
  131. package/dist/notion/content-cache.js.map +1 -0
  132. package/dist/notion/generic-source.d.ts +61 -0
  133. package/dist/notion/generic-source.js +408 -0
  134. package/dist/notion/generic-source.js.map +1 -0
  135. package/dist/notion/index.d.ts +13 -0
  136. package/dist/notion/index.js +1278 -0
  137. package/dist/notion/index.js.map +1 -0
  138. package/dist/notion/mappers.d.ts +1 -0
  139. package/dist/notion/mappers.js +152 -0
  140. package/dist/notion/mappers.js.map +1 -0
  141. package/dist/notion/media.d.ts +22 -0
  142. package/dist/notion/media.js +209 -0
  143. package/dist/notion/media.js.map +1 -0
  144. package/dist/notion/property-mappers.d.ts +24 -0
  145. package/dist/notion/property-mappers.js +152 -0
  146. package/dist/notion/property-mappers.js.map +1 -0
  147. package/dist/notion/routes/index.d.ts +8 -0
  148. package/dist/notion/routes/index.js +428 -0
  149. package/dist/notion/routes/index.js.map +1 -0
  150. package/dist/notion/routes/webhook.d.ts +98 -0
  151. package/dist/notion/routes/webhook.js +428 -0
  152. package/dist/notion/routes/webhook.js.map +1 -0
  153. package/dist/notion/types.d.ts +152 -0
  154. package/dist/notion/types.js +1 -0
  155. package/dist/notion/types.js.map +1 -0
  156. package/dist/notion/webhook.d.ts +83 -0
  157. package/dist/notion/webhook.js +490 -0
  158. package/dist/notion/webhook.js.map +1 -0
  159. package/dist/platform/capabilities.d.ts +34 -0
  160. package/dist/platform/capabilities.js +42 -0
  161. package/dist/platform/capabilities.js.map +1 -0
  162. package/dist/platform/current.d.ts +13 -0
  163. package/dist/platform/current.js +181 -0
  164. package/dist/platform/current.js.map +1 -0
  165. package/dist/platform/index.d.ts +5 -0
  166. package/dist/platform/index.js +269 -0
  167. package/dist/platform/index.js.map +1 -0
  168. package/dist/platform/runtime.d.ts +118 -0
  169. package/dist/platform/runtime.js +160 -0
  170. package/dist/platform/runtime.js.map +1 -0
  171. package/dist/platform/selection.d.ts +10 -0
  172. package/dist/platform/selection.js +22 -0
  173. package/dist/platform/selection.js.map +1 -0
  174. package/dist/storage/index.d.ts +17 -0
  175. package/dist/storage/index.js +218 -0
  176. package/dist/storage/index.js.map +1 -0
  177. package/dist/storage/routes/cdn.d.ts +19 -0
  178. package/dist/storage/routes/cdn.js +289 -0
  179. package/dist/storage/routes/cdn.js.map +1 -0
  180. package/dist/storage/routes/files.d.ts +27 -0
  181. package/dist/storage/routes/files.js +216 -0
  182. package/dist/storage/routes/files.js.map +1 -0
  183. package/dist/storage/routes/index.d.ts +2 -0
  184. package/dist/storage/routes/index.js +352 -0
  185. package/dist/storage/routes/index.js.map +1 -0
  186. package/dist/types-BsAcZSNX.d.ts +94 -0
  187. package/dist/types.d.ts +78 -0
  188. package/dist/types.js +1 -0
  189. package/dist/types.js.map +1 -0
  190. package/dist/util/index.d.ts +18 -0
  191. package/dist/util/index.js +48 -0
  192. package/dist/util/index.js.map +1 -0
  193. package/dist/worker/index.d.ts +6 -0
  194. package/dist/worker/index.js +1026 -0
  195. package/dist/worker/index.js.map +1 -0
  196. package/dist/worker/routes/content-prewarm.d.ts +34 -0
  197. package/dist/worker/routes/content-prewarm.js +38 -0
  198. package/dist/worker/routes/content-prewarm.js.map +1 -0
  199. package/dist/worker/routes/content-revalidate.d.ts +81 -0
  200. package/dist/worker/routes/content-revalidate.js +64 -0
  201. package/dist/worker/routes/content-revalidate.js.map +1 -0
  202. package/dist/worker/routes/health.d.ts +14 -0
  203. package/dist/worker/routes/health.js +278 -0
  204. package/dist/worker/routes/health.js.map +1 -0
  205. package/dist/worker/routes/index.d.ts +6 -0
  206. package/dist/worker/routes/index.js +373 -0
  207. package/dist/worker/routes/index.js.map +1 -0
  208. package/package.json +124 -0
package/dist/index.js ADDED
@@ -0,0 +1,1281 @@
1
+ // src/content/models.ts
2
+ var registry = [];
3
+ function defineContentSource(model) {
4
+ const existing = registry.findIndex((s) => s.id === model.id);
5
+ if (existing >= 0) registry[existing] = model;
6
+ else registry.push(model);
7
+ return model;
8
+ }
9
+
10
+ // src/middleware.ts
11
+ var requestContext = /* @__PURE__ */ new WeakMap();
12
+ function defaultIsProtectedPath(request) {
13
+ const url = new URL(request.url);
14
+ if (url.pathname.startsWith("/api/admin/")) return true;
15
+ if (url.pathname.startsWith("/admin")) {
16
+ return request.method !== "GET" && request.method !== "HEAD";
17
+ }
18
+ return false;
19
+ }
20
+ function readSessionCookie(request, name) {
21
+ const header = request.headers.get("cookie");
22
+ if (!header) return null;
23
+ for (const part of header.split(";")) {
24
+ const [rawKey, ...rest] = part.split("=");
25
+ if (!rawKey) continue;
26
+ if (rawKey.trim() !== name) continue;
27
+ return rest.join("=").trim() || null;
28
+ }
29
+ return null;
30
+ }
31
+ async function resolveFoundationViewer(request, options) {
32
+ const sessionId = readSessionCookie(
33
+ request,
34
+ options.authConfig.sessionCookie.name
35
+ );
36
+ if (!sessionId || !options.sessionLookup) {
37
+ return { viewer: null, sessionId: sessionId ?? null };
38
+ }
39
+ const env2 = request.env ?? null;
40
+ const lookup = await options.sessionLookup(sessionId, env2);
41
+ if (!lookup) {
42
+ return { viewer: null, sessionId };
43
+ }
44
+ return {
45
+ viewer: {
46
+ userId: lookup.userId,
47
+ role: lookup.role,
48
+ email: lookup.email ?? null
49
+ },
50
+ sessionId
51
+ };
52
+ }
53
+ async function nextionMiddleware(request, env2, options) {
54
+ const context = await resolveFoundationViewer(request, options);
55
+ requestContext.set(request, context);
56
+ const isProtected = options.isProtectedPath ?? defaultIsProtectedPath;
57
+ if (!isProtected(request)) return null;
58
+ if (context.viewer) return null;
59
+ return new Response(
60
+ JSON.stringify({ ok: false, error: "Unauthorized" }),
61
+ {
62
+ status: 401,
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ "Cache-Control": "no-store",
66
+ "X-Foundation-Gate": "admin"
67
+ }
68
+ }
69
+ );
70
+ }
71
+
72
+ // src/storage/routes/files.ts
73
+ import { NextResponse } from "next/server";
74
+
75
+ // src/util/env.ts
76
+ import { env } from "cloudflare:workers";
77
+ var workerEnv = env;
78
+
79
+ // src/platform/runtime.ts
80
+ function cacheRequestForKey(key) {
81
+ return new Request(key, { method: "GET" });
82
+ }
83
+ function createCloudflarePublicCacheAdapter(cache) {
84
+ return {
85
+ kind: "cloudflare-cache",
86
+ async match(key) {
87
+ return await cache.match(cacheRequestForKey(key)) ?? null;
88
+ },
89
+ put(key, response) {
90
+ return cache.put(cacheRequestForKey(key), response);
91
+ },
92
+ delete(key) {
93
+ return cache.delete(cacheRequestForKey(key));
94
+ }
95
+ };
96
+ }
97
+ function createCloudflareKeyValueCacheAdapter(namespace) {
98
+ return {
99
+ kind: "workers-kv",
100
+ async get(key, options) {
101
+ return await namespace.get(key, {
102
+ type: "json",
103
+ cacheTtl: options?.cacheTtl
104
+ });
105
+ },
106
+ async put(key, value, options) {
107
+ await namespace.put(key, JSON.stringify(value), {
108
+ expirationTtl: options?.expirationTtl,
109
+ metadata: options?.metadata
110
+ });
111
+ },
112
+ delete(key) {
113
+ return namespace.delete(key);
114
+ },
115
+ async list(options) {
116
+ const result = await namespace.list({
117
+ prefix: options?.prefix,
118
+ limit: options?.limit,
119
+ cursor: options?.cursor
120
+ });
121
+ return {
122
+ keys: result.keys.map((key) => ({ name: key.name })),
123
+ cursor: result.list_complete ? void 0 : result.cursor,
124
+ listComplete: result.list_complete
125
+ };
126
+ }
127
+ };
128
+ }
129
+ function r2ObjectToStoredObject(object) {
130
+ return {
131
+ body: object.body,
132
+ size: object.size,
133
+ etag: object.etag,
134
+ contentType: object.httpMetadata?.contentType
135
+ };
136
+ }
137
+ function createCloudflareRuntimePlatform(env2, options) {
138
+ const database = env2.DB ? {
139
+ kind: "d1",
140
+ prepare(query) {
141
+ return env2.DB.prepare(query);
142
+ },
143
+ async batch(statements) {
144
+ return await env2.DB.batch(
145
+ statements
146
+ );
147
+ }
148
+ } : null;
149
+ const objectStorage = env2.ASSETS_BUCKET ? {
150
+ kind: "r2",
151
+ async get(key) {
152
+ const object = await env2.ASSETS_BUCKET?.get(key);
153
+ return object ? r2ObjectToStoredObject(object) : null;
154
+ },
155
+ async put(key, value, options2) {
156
+ await env2.ASSETS_BUCKET?.put(key, value, {
157
+ httpMetadata: {
158
+ contentType: options2?.contentType,
159
+ cacheControl: options2?.cacheControl
160
+ },
161
+ customMetadata: options2?.metadata
162
+ });
163
+ },
164
+ async delete(key) {
165
+ await env2.ASSETS_BUCKET?.delete(key);
166
+ },
167
+ async list(options2) {
168
+ const listed = await env2.ASSETS_BUCKET?.list({
169
+ prefix: options2?.prefix,
170
+ limit: options2?.limit
171
+ });
172
+ return listed?.objects.map((object) => ({
173
+ key: object.key,
174
+ size: object.size,
175
+ uploaded: object.uploaded
176
+ })) ?? [];
177
+ }
178
+ } : null;
179
+ const imageTransformer = env2.IMAGES ? {
180
+ kind: "cloudflare-images",
181
+ async transform(body, options2) {
182
+ const result = await env2.IMAGES.input(body).transform(options2.width ? { width: options2.width } : {}).output({
183
+ format: options2.format,
184
+ quality: options2.quality
185
+ });
186
+ return {
187
+ body: result.image(),
188
+ contentType: result.contentType(),
189
+ response: () => result.response()
190
+ };
191
+ }
192
+ } : null;
193
+ const keyValueCache = env2.CONTENT_CACHE ? createCloudflareKeyValueCacheAdapter(env2.CONTENT_CACHE) : null;
194
+ return {
195
+ id: "cloudflare-workers",
196
+ database,
197
+ objectStorage,
198
+ imageTransformer,
199
+ keyValueCache,
200
+ publicCache: options?.publicCache ? createCloudflarePublicCacheAdapter(options.publicCache) : null
201
+ };
202
+ }
203
+
204
+ // src/platform/cloudflare-runtime.ts
205
+ function getDefaultCloudflareCache() {
206
+ const globalWithCaches = globalThis;
207
+ return globalWithCaches.caches?.default ?? null;
208
+ }
209
+ function getRuntimePlatform() {
210
+ return createCloudflareRuntimePlatform(workerEnv, {
211
+ publicCache: getDefaultCloudflareCache()
212
+ });
213
+ }
214
+ function getPublicCache() {
215
+ const cache = getDefaultCloudflareCache();
216
+ if (!cache) {
217
+ throw new Error("Cloudflare cache binding not configured");
218
+ }
219
+ return createCloudflarePublicCacheAdapter(cache);
220
+ }
221
+
222
+ // src/platform/selection.ts
223
+ function currentRuntimeId() {
224
+ return "cloudflare-workers";
225
+ }
226
+
227
+ // src/platform/current.ts
228
+ function getRuntimePlatform2() {
229
+ return getRuntimePlatform();
230
+ }
231
+ function getDatabase() {
232
+ const platform = getRuntimePlatform2();
233
+ const database = platform.database;
234
+ if (!database) {
235
+ throw new Error(`SQL database adapter not configured for ${platform.id}`);
236
+ }
237
+ return database;
238
+ }
239
+ function getPublicCache2() {
240
+ return getPublicCache();
241
+ }
242
+
243
+ // src/storage/routes/files.ts
244
+ var filesRoute = {
245
+ /**
246
+ * Next.js handler for `app/api/files/[...key]/route.ts`. Receives the
247
+ * catch-all key from the route params.
248
+ */
249
+ async GET(_request, props) {
250
+ const { key } = await props.params;
251
+ return filesRoute.handle(new Request(buildInternalUrl(_request, key)));
252
+ },
253
+ /**
254
+ * Worker-friendly handler. Extracts the catch-all key from the URL
255
+ * pathname (`/api/files/<key>`).
256
+ */
257
+ async handle(request) {
258
+ const key = readKeyFromUrl(request.url);
259
+ if (!key) {
260
+ return NextResponse.json({ error: "Invalid key" }, { status: 400 });
261
+ }
262
+ if (key.includes("..") || key.startsWith("/")) {
263
+ return NextResponse.json({ error: "Invalid key" }, { status: 400 });
264
+ }
265
+ const storage = getRuntimePlatform2().objectStorage;
266
+ if (!storage) {
267
+ return NextResponse.json(
268
+ { error: "Object storage not configured" },
269
+ { status: 503 }
270
+ );
271
+ }
272
+ const object = await storage.get(key);
273
+ if (!object) {
274
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
275
+ }
276
+ const headers = new Headers();
277
+ if (object.contentType) {
278
+ headers.set("Content-Type", object.contentType);
279
+ }
280
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
281
+ if (object.etag) headers.set("ETag", object.etag);
282
+ headers.set("Content-Length", String(object.size));
283
+ return new Response(object.body, { headers });
284
+ }
285
+ };
286
+ function buildInternalUrl(request, keyParts) {
287
+ const url = new URL(request.url);
288
+ url.pathname = `/api/files/${keyParts.map(encodeURIComponent).join("/")}`;
289
+ return url.toString();
290
+ }
291
+ function readKeyFromUrl(rawUrl) {
292
+ const url = new URL(rawUrl);
293
+ const prefix = "/api/files/";
294
+ if (!url.pathname.startsWith(prefix)) return null;
295
+ const encoded = url.pathname.slice(prefix.length);
296
+ if (!encoded) return null;
297
+ return decodeURIComponent(encoded);
298
+ }
299
+ var GET = filesRoute.GET;
300
+
301
+ // src/storage/routes/cdn.ts
302
+ import { NextResponse as NextResponse2 } from "next/server";
303
+ var DEFAULT_WIDTH = 1200;
304
+ var MAX_WIDTH = 2400;
305
+ var DEFAULT_QUALITY = 75;
306
+ var MIN_QUALITY = 40;
307
+ var MAX_QUALITY = 85;
308
+ var cdnRoute = {
309
+ async GET(request, props) {
310
+ const { key } = await props.params;
311
+ return cdnRoute.handle(new Request(buildInternalUrl2(request, key)));
312
+ },
313
+ async handle(request) {
314
+ const url = new URL(request.url);
315
+ const key = readKeyFromPathname(url.pathname);
316
+ if (!key) {
317
+ return NextResponse2.json({ error: "Invalid key" }, { status: 400 });
318
+ }
319
+ if (key.includes("..") || key.startsWith("/")) {
320
+ return NextResponse2.json({ error: "Invalid key" }, { status: 400 });
321
+ }
322
+ const platform = getRuntimePlatform2();
323
+ const storage = platform.objectStorage;
324
+ if (!storage) {
325
+ return NextResponse2.json(
326
+ { error: "Object storage not configured" },
327
+ { status: 503 }
328
+ );
329
+ }
330
+ const object = await storage.get(key);
331
+ if (!object) {
332
+ return NextResponse2.json({ error: "Not found" }, { status: 404 });
333
+ }
334
+ const accept = request.headers.get("accept") ?? "";
335
+ const isImage = object.contentType?.startsWith("image/") ?? false;
336
+ if (!isImage) {
337
+ return streamObject(object, {
338
+ "X-Debug-Cdn-Branch": "non-image",
339
+ "X-Debug-Cdn-Key": key
340
+ });
341
+ }
342
+ let outputFormat = null;
343
+ let outputQuality = void 0;
344
+ if (accept.includes("image/avif")) {
345
+ outputFormat = "image/avif";
346
+ outputQuality = 60;
347
+ } else if (accept.includes("image/webp")) {
348
+ outputFormat = "image/webp";
349
+ outputQuality = 75;
350
+ }
351
+ const isSvg = object.contentType === "image/svg+xml";
352
+ if (!outputFormat || isSvg || !platform.imageTransformer) {
353
+ return streamObject(object, {
354
+ "X-Debug-Cdn-Branch": isSvg ? "svg-bypass" : !platform.imageTransformer ? "transformer-bypass" : "format-bypass",
355
+ "X-Debug-Cdn-Accept": accept.includes("image/avif") ? "avif" : accept.includes("image/webp") ? "webp" : "other",
356
+ "X-Debug-Cdn-Key": key
357
+ });
358
+ }
359
+ try {
360
+ const width = clampInt(
361
+ url.searchParams.get("w"),
362
+ 64,
363
+ MAX_WIDTH,
364
+ DEFAULT_WIDTH
365
+ );
366
+ const quality = clampInt(
367
+ url.searchParams.get("q"),
368
+ MIN_QUALITY,
369
+ MAX_QUALITY,
370
+ outputQuality ?? DEFAULT_QUALITY
371
+ );
372
+ const result = await platform.imageTransformer.transform(object.body, {
373
+ width,
374
+ format: outputFormat,
375
+ quality
376
+ });
377
+ return new Response(result.body, {
378
+ headers: {
379
+ "Content-Type": result.contentType,
380
+ "Cache-Control": "public, max-age=31536000, immutable",
381
+ Vary: "Accept",
382
+ "X-Debug-Cdn-Branch": "transformed",
383
+ "X-Debug-Cdn-Key": key,
384
+ "X-Optimized-Width": String(width),
385
+ "X-Optimized-Quality": String(quality),
386
+ "X-Original-Format": object.contentType ?? "unknown",
387
+ "X-Optimized-Format": outputFormat
388
+ }
389
+ });
390
+ } catch (e) {
391
+ return streamObject(object, {
392
+ "X-Debug-Cdn-Branch": "transform-error-fallback",
393
+ "X-Debug-Cdn-Key": key,
394
+ "X-Debug-Cdn-Error": e instanceof Error ? e.name : "unknown"
395
+ });
396
+ }
397
+ }
398
+ };
399
+ function buildInternalUrl2(request, keyParts) {
400
+ const url = new URL(request.url);
401
+ url.pathname = `/api/cdn/${keyParts.map(encodeURIComponent).join("/")}`;
402
+ return url.toString();
403
+ }
404
+ function readKeyFromPathname(pathname) {
405
+ const prefix = "/api/cdn/";
406
+ if (!pathname.startsWith(prefix)) return null;
407
+ const encoded = pathname.slice(prefix.length);
408
+ if (!encoded) return null;
409
+ return decodeURIComponent(encoded);
410
+ }
411
+ function clampInt(value, min, max, fallback) {
412
+ const parsed = Number.parseInt(value ?? "", 10);
413
+ if (!Number.isFinite(parsed)) {
414
+ return fallback;
415
+ }
416
+ return Math.max(min, Math.min(max, parsed));
417
+ }
418
+ function streamObject(object, extraHeaders) {
419
+ const headers = new Headers();
420
+ if (object.contentType) {
421
+ headers.set("Content-Type", object.contentType);
422
+ }
423
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
424
+ headers.set("Content-Length", String(object.size));
425
+ if (object.etag) headers.set("ETag", object.etag);
426
+ for (const [key, value] of Object.entries(extraHeaders ?? {})) {
427
+ headers.set(key, value);
428
+ }
429
+ return new Response(object.body, { headers });
430
+ }
431
+ var GET2 = cdnRoute.GET;
432
+
433
+ // src/media/routes/notion-media.ts
434
+ import { NextResponse as NextResponse3 } from "next/server";
435
+ import { getRequestExecutionContext } from "vinext/shims/request-context";
436
+
437
+ // src/cache/cache-keys.ts
438
+ var CACHE_ORIGIN = "https://cache.local";
439
+ var CACHE_NAMESPACE = "/__public-cache/v20260609a";
440
+ var NOTION_MEDIA_R2_PREFIX = "notion-media/v1";
441
+ function normalizePath(pathname) {
442
+ if (pathname === "/") return "/";
443
+ return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
444
+ }
445
+ function publicMediaVariantForAccept(accept) {
446
+ if (accept.includes("image/avif")) return "avif";
447
+ if (accept.includes("image/webp")) return "webp";
448
+ return "source";
449
+ }
450
+ function publicMediaCacheKeyForUrl(input, variant) {
451
+ const url = new URL(
452
+ `${CACHE_NAMESPACE}${normalizePath(input.pathname)}${input.search}`,
453
+ CACHE_ORIGIN
454
+ );
455
+ url.searchParams.set("__variant", variant);
456
+ url.searchParams.sort();
457
+ return url.toString();
458
+ }
459
+ function keySegment(value) {
460
+ return encodeURIComponent(value || "none");
461
+ }
462
+ function notionMediaR2KeyForUrl(input, variant) {
463
+ if (variant === "source") return null;
464
+ const version = input.searchParams.get("v");
465
+ if (!version) return null;
466
+ const path = normalizePath(input.pathname).split("/").filter(Boolean).map(keySegment).join("/");
467
+ const width = input.searchParams.get("w") ?? "source";
468
+ const quality = input.searchParams.get("q") ?? "source";
469
+ return [
470
+ NOTION_MEDIA_R2_PREFIX,
471
+ variant,
472
+ path,
473
+ `v-${keySegment(version)}`,
474
+ `w-${keySegment(width)}`,
475
+ `q-${keySegment(quality)}.${variant}`
476
+ ].join("/");
477
+ }
478
+
479
+ // src/notion/client.ts
480
+ import { Client } from "@notionhq/client";
481
+ function createNotionClient(config) {
482
+ return new Client({
483
+ auth: config.token,
484
+ baseUrl: config.apiBaseUrl,
485
+ notionVersion: "2026-03-11"
486
+ });
487
+ }
488
+
489
+ // src/notion/config.ts
490
+ function readProcessEnv() {
491
+ const env2 = {
492
+ NOTION_TOKEN: process.env.NOTION_TOKEN,
493
+ NOTION_DATA_SOURCE_ID: process.env.NOTION_DATA_SOURCE_ID,
494
+ NOTION_MOVIES_DATA_SOURCE_ID: process.env.NOTION_MOVIES_DATA_SOURCE_ID,
495
+ NOTION_API_BASE_URL: process.env.NOTION_API_BASE_URL,
496
+ NOTION_EDIT_BASE_URL: process.env.NOTION_EDIT_BASE_URL,
497
+ NOTION_WEBHOOK_VERIFICATION_TOKEN: process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN
498
+ };
499
+ for (const [key, value] of Object.entries(process.env)) {
500
+ if (key.startsWith("NOTION_") && typeof value === "string") {
501
+ env2[key] = value;
502
+ }
503
+ }
504
+ return env2;
505
+ }
506
+ async function readWorkerEnv() {
507
+ try {
508
+ const mod = await import(
509
+ /* webpackIgnore: true */
510
+ "cloudflare:workers"
511
+ );
512
+ const env2 = {};
513
+ for (const [key, value] of Object.entries(mod.env ?? {})) {
514
+ if (key.startsWith("NOTION_") && typeof value === "string") {
515
+ env2[key] = value;
516
+ }
517
+ }
518
+ return env2;
519
+ } catch {
520
+ return {};
521
+ }
522
+ }
523
+ function readString(source, name) {
524
+ const value = String(source[name] ?? "").trim();
525
+ return value || void 0;
526
+ }
527
+ function mergeEnv(...sources) {
528
+ const merged = {};
529
+ for (const source of sources) {
530
+ for (const name of Object.keys(source)) {
531
+ if (!name.startsWith("NOTION_")) continue;
532
+ const value = readString(source, name);
533
+ if (value) merged[name] = value;
534
+ }
535
+ }
536
+ return merged;
537
+ }
538
+ async function readEnv() {
539
+ const processEnv = readProcessEnv();
540
+ return mergeEnv(await readWorkerEnv(), processEnv);
541
+ }
542
+ function readRequired(source, name) {
543
+ const value = readString(source, name);
544
+ if (!value) {
545
+ throw new Error(`Missing required Notion env: ${name}`);
546
+ }
547
+ return value;
548
+ }
549
+ async function getNotionClientConfig() {
550
+ const env2 = await readEnv();
551
+ return {
552
+ token: readRequired(env2, "NOTION_TOKEN"),
553
+ apiBaseUrl: readString(env2, "NOTION_API_BASE_URL")
554
+ };
555
+ }
556
+
557
+ // src/notion/media.ts
558
+ function normalizeNotionFileSource(input) {
559
+ const file = input;
560
+ if (!file || typeof file !== "object") return null;
561
+ if (file.type === "external") {
562
+ const url = String(file.external?.url ?? "").trim();
563
+ return url ? { type: "external", url } : null;
564
+ }
565
+ if (file.type === "file") {
566
+ const url = String(file.file?.url ?? "").trim();
567
+ if (!url) return null;
568
+ return {
569
+ type: "file",
570
+ url,
571
+ expiryTime: String(file.file?.expiry_time ?? "").trim() || null
572
+ };
573
+ }
574
+ return null;
575
+ }
576
+ function pickFirstFilesPropertyValue(property) {
577
+ const value = property;
578
+ if (!value || value.type !== "files" || !Array.isArray(value.files)) {
579
+ return null;
580
+ }
581
+ return value.files[0] ?? null;
582
+ }
583
+ function fileObjectForMediaBlock(block) {
584
+ const typed = block[block.type];
585
+ if (!typed || typeof typed !== "object") return null;
586
+ if (block.type === "image" || block.type === "video" || block.type === "file" || block.type === "pdf" || block.type === "audio") {
587
+ return typed;
588
+ }
589
+ return null;
590
+ }
591
+
592
+ // src/media/routes/notion-media.ts
593
+ var DEFAULT_WIDTH2 = 1200;
594
+ var MAX_WIDTH2 = 2400;
595
+ var DEFAULT_QUALITY2 = 75;
596
+ var MIN_QUALITY2 = 40;
597
+ var MAX_QUALITY2 = 85;
598
+ var CACHEABLE_STATUS = /* @__PURE__ */ new Set([200]);
599
+ var notionMediaRoute = {
600
+ async GET(_request, props) {
601
+ const { ref } = await props.params;
602
+ if (ref.some((part) => part === ".." || part.includes("/"))) {
603
+ return badRequest();
604
+ }
605
+ const url = new URL(_request.url);
606
+ url.pathname = buildNotionMediaPath(ref);
607
+ return notionMediaRoute.handle(new Request(url.toString(), _request));
608
+ },
609
+ async handle(request) {
610
+ const variant = publicMediaVariantForAccept(
611
+ request.headers.get("accept") ?? ""
612
+ );
613
+ return withEdgeMediaCache(request, variant, () => loadMedia(request));
614
+ }
615
+ };
616
+ function buildNotionMediaPath(ref) {
617
+ return `/api/notion/media/${ref.map(encodeURIComponent).join("/")}`;
618
+ }
619
+ function clampInt2(value, min, max, fallback) {
620
+ const parsed = Number.parseInt(value ?? "", 10);
621
+ if (!Number.isFinite(parsed)) return fallback;
622
+ return Math.max(min, Math.min(max, parsed));
623
+ }
624
+ function cacheControl(request) {
625
+ const url = new URL(request.url);
626
+ if (url.searchParams.has("v")) {
627
+ return "public, max-age=31536000, immutable";
628
+ }
629
+ return "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400";
630
+ }
631
+ function canUseMediaCache(request) {
632
+ if (request.method !== "GET") return false;
633
+ return !request.headers.has("range");
634
+ }
635
+ function mediaCacheHeaders(response, request, state, r2State) {
636
+ const headers = new Headers(response.headers);
637
+ headers.set("Cache-Control", cacheControl(request));
638
+ headers.set("X-Notion-Media-Cache", state);
639
+ if (r2State) headers.set("X-Notion-Media-R2", r2State);
640
+ return headers;
641
+ }
642
+ async function responseFromR2Cache(request, variant) {
643
+ const url = new URL(request.url);
644
+ const r2Key = notionMediaR2KeyForUrl(url, variant);
645
+ const storage = getRuntimePlatform2().objectStorage;
646
+ if (!r2Key || !storage) return null;
647
+ const object = await storage.get(r2Key);
648
+ if (!object?.body) return null;
649
+ const contentType = object.contentType ?? (variant === "avif" ? "image/avif" : "image/webp");
650
+ const headers = new Headers();
651
+ headers.set("Content-Type", contentType);
652
+ headers.set("Cache-Control", cacheControl(request));
653
+ headers.set("Vary", "Accept");
654
+ headers.set("X-Notion-Media-Branch", "r2");
655
+ headers.set("X-Notion-Media-R2", "HIT");
656
+ if (object.etag) headers.set("ETag", object.etag);
657
+ return new Response(object.body, { headers });
658
+ }
659
+ async function withEdgeMediaCache(request, variant, load) {
660
+ if (!canUseMediaCache(request)) {
661
+ const response2 = await load();
662
+ return new Response(response2.body, {
663
+ status: response2.status,
664
+ headers: mediaCacheHeaders(response2, request, "BYPASS")
665
+ });
666
+ }
667
+ const url = new URL(request.url);
668
+ const cache = getPublicCache2();
669
+ const cacheKey = publicMediaCacheKeyForUrl(url, variant);
670
+ const cached = await cache.match(cacheKey);
671
+ if (cached) {
672
+ return new Response(cached.body, {
673
+ status: cached.status,
674
+ headers: mediaCacheHeaders(cached, request, "HIT")
675
+ });
676
+ }
677
+ const r2Response = await responseFromR2Cache(request, variant);
678
+ const response = r2Response ?? await load();
679
+ const headers = mediaCacheHeaders(response, request, "MISS");
680
+ const output = new Response(response.body, {
681
+ status: response.status,
682
+ headers
683
+ });
684
+ if (CACHEABLE_STATUS.has(response.status)) {
685
+ const toCache = output.clone();
686
+ getRequestExecutionContext()?.waitUntil(cache.put(cacheKey, toCache));
687
+ }
688
+ return output;
689
+ }
690
+ function mediaRedirect(url) {
691
+ const response = NextResponse3.redirect(url, 302);
692
+ response.headers.set(
693
+ "Cache-Control",
694
+ "public, max-age=300, s-maxage=300, stale-while-revalidate=300"
695
+ );
696
+ return response;
697
+ }
698
+ function notFound() {
699
+ return NextResponse3.json({ error: "Not found" }, { status: 404 });
700
+ }
701
+ function badRequest() {
702
+ return NextResponse3.json({ error: "Invalid media ref" }, { status: 400 });
703
+ }
704
+ function forbidden() {
705
+ return NextResponse3.json({ error: "Forbidden" }, { status: 403 });
706
+ }
707
+ async function serveFileObject(input, request, options) {
708
+ const source = normalizeNotionFileSource(input);
709
+ if (!source) return notFound();
710
+ if (options?.redirectNotionHosted) return mediaRedirect(source.url);
711
+ return proxyNotionHostedFile(source.url, request);
712
+ }
713
+ async function proxyNotionHostedFile(url, request) {
714
+ const range = request.headers.get("range");
715
+ const ifRange = request.headers.get("if-range");
716
+ const upstreamHeaders = new Headers({
717
+ Accept: request.headers.get("accept") ?? "*/*"
718
+ });
719
+ if (range) upstreamHeaders.set("Range", range);
720
+ if (ifRange) upstreamHeaders.set("If-Range", ifRange);
721
+ const upstream = await fetch(url, {
722
+ headers: upstreamHeaders
723
+ });
724
+ if (!upstream.ok && upstream.status !== 416 || !upstream.body) {
725
+ return NextResponse3.json(
726
+ { error: "Unable to fetch Notion media" },
727
+ { status: upstream.status || 502 }
728
+ );
729
+ }
730
+ const contentType = upstream.headers.get("content-type") ?? "";
731
+ const isImage = contentType.startsWith("image/");
732
+ const accept = request.headers.get("accept") ?? "";
733
+ const variant = publicMediaVariantForAccept(accept);
734
+ const urlObj = new URL(request.url);
735
+ const width = clampInt2(
736
+ urlObj.searchParams.get("w"),
737
+ 64,
738
+ MAX_WIDTH2,
739
+ DEFAULT_WIDTH2
740
+ );
741
+ const quality = clampInt2(
742
+ urlObj.searchParams.get("q"),
743
+ MIN_QUALITY2,
744
+ MAX_QUALITY2,
745
+ DEFAULT_QUALITY2
746
+ );
747
+ let outputFormat = null;
748
+ if (variant === "avif") {
749
+ outputFormat = "image/avif";
750
+ } else if (variant === "webp") {
751
+ outputFormat = "image/webp";
752
+ }
753
+ const platform = getRuntimePlatform2();
754
+ const imageTransformer = platform.imageTransformer;
755
+ if (isImage && !range && outputFormat && imageTransformer) {
756
+ const r2Key = notionMediaR2KeyForUrl(urlObj, variant);
757
+ try {
758
+ const result = await imageTransformer.transform(upstream.body, {
759
+ width,
760
+ format: outputFormat,
761
+ quality
762
+ });
763
+ const transformed = result.response();
764
+ const headers2 = new Headers(transformed.headers);
765
+ headers2.set("Content-Type", result.contentType);
766
+ headers2.set("Cache-Control", cacheControl(request));
767
+ headers2.set("Vary", "Accept");
768
+ headers2.set("X-Notion-Media-Branch", "transformed");
769
+ headers2.set("X-Notion-Media-R2", r2Key ? "MISS" : "BYPASS");
770
+ headers2.set("X-Optimized-Width", String(width));
771
+ headers2.set("X-Optimized-Quality", String(quality));
772
+ if (transformed.body && r2Key && platform.objectStorage) {
773
+ const [clientBody, r2Body] = transformed.body.tee();
774
+ getRequestExecutionContext()?.waitUntil(
775
+ platform.objectStorage.put(r2Key, r2Body, {
776
+ contentType: result.contentType,
777
+ cacheControl: "public, max-age=31536000, immutable",
778
+ metadata: {
779
+ source: "notion",
780
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
781
+ width: String(width),
782
+ quality: String(quality)
783
+ }
784
+ })
785
+ );
786
+ return new Response(clientBody, { headers: headers2 });
787
+ }
788
+ return new Response(transformed.body, { headers: headers2 });
789
+ } catch {
790
+ }
791
+ }
792
+ const headers = new Headers();
793
+ for (const header of [
794
+ "accept-ranges",
795
+ "content-disposition",
796
+ "content-encoding",
797
+ "content-length",
798
+ "content-range",
799
+ "content-type",
800
+ "etag",
801
+ "last-modified"
802
+ ]) {
803
+ const value = upstream.headers.get(header);
804
+ if (value) headers.set(header, value);
805
+ }
806
+ if (contentType && !headers.has("Content-Type")) {
807
+ headers.set("Content-Type", contentType);
808
+ }
809
+ headers.set("Cache-Control", cacheControl(request));
810
+ headers.set("X-Notion-Media-Branch", "proxied");
811
+ return new Response(upstream.body, { status: upstream.status, headers });
812
+ }
813
+ async function loadMedia(request) {
814
+ const url = new URL(request.url);
815
+ const ref = readRefFromPathname(url.pathname);
816
+ if (!ref) return badRequest();
817
+ const client = createNotionClient(await getNotionClientConfig());
818
+ if (ref[0] === "page" && ref[1] && ref[2] === "cover") {
819
+ const page = await client.pages.retrieve({
820
+ page_id: ref[1]
821
+ });
822
+ return serveFileObject(page.cover, request);
823
+ }
824
+ if (ref[0] === "page" && ref[1] && ref[2] === "property" && ref[3]) {
825
+ const page = await client.pages.retrieve({
826
+ page_id: ref[1]
827
+ });
828
+ const propertyName = decodeURIComponent(ref.slice(3).join("/"));
829
+ return serveFileObject(
830
+ pickFirstFilesPropertyValue(page.properties?.[propertyName]),
831
+ request
832
+ );
833
+ }
834
+ if (ref[0] === "block" && ref[1]) {
835
+ const block = await client.blocks.retrieve({
836
+ block_id: ref[1]
837
+ });
838
+ if (block.type === "video") {
839
+ return forbidden();
840
+ }
841
+ return serveFileObject(fileObjectForMediaBlock(block), request, {
842
+ redirectNotionHosted: block.type === "audio" || block.type === "pdf" || block.type === "file"
843
+ });
844
+ }
845
+ return badRequest();
846
+ }
847
+ function readRefFromPathname(pathname) {
848
+ const prefix = "/api/notion/media/";
849
+ if (!pathname.startsWith(prefix)) return null;
850
+ const encoded = pathname.slice(prefix.length);
851
+ if (!encoded) return null;
852
+ return encoded.split("/").map((part) => decodeURIComponent(part));
853
+ }
854
+ var GET3 = notionMediaRoute.GET;
855
+
856
+ // src/worker/routes/health.ts
857
+ import { NextResponse as NextResponse4 } from "next/server";
858
+
859
+ // src/internal/admin/schema-guard.ts
860
+ var REQUIRED_SCHEMA_CHECKS = [
861
+ {
862
+ key: "app_settings.turnstile_enabled",
863
+ sql: "SELECT turnstile_enabled FROM app_settings LIMIT 1"
864
+ },
865
+ {
866
+ key: "users.session_rev",
867
+ sql: "SELECT session_rev FROM users LIMIT 1"
868
+ },
869
+ {
870
+ key: "auth_rate_limits",
871
+ sql: "SELECT 1 FROM auth_rate_limits LIMIT 1"
872
+ }
873
+ ];
874
+ function isSchemaDriftError(error) {
875
+ const message = error instanceof Error ? error.message : String(error ?? "");
876
+ return message.includes("no such column") || message.includes("no such table");
877
+ }
878
+ async function runSchemaHealthChecks(db) {
879
+ const missing = [];
880
+ const errors = [];
881
+ for (const check of REQUIRED_SCHEMA_CHECKS) {
882
+ try {
883
+ await db.prepare(check.sql).first();
884
+ } catch (error) {
885
+ if (isSchemaDriftError(error)) {
886
+ missing.push(check.key);
887
+ } else {
888
+ const message = error instanceof Error ? error.message : String(error);
889
+ errors.push(`${check.key}: ${message}`);
890
+ }
891
+ }
892
+ }
893
+ return {
894
+ ok: missing.length === 0 && errors.length === 0,
895
+ missing,
896
+ errors
897
+ };
898
+ }
899
+
900
+ // src/worker/routes/health.ts
901
+ async function probeDatabase() {
902
+ const database = getDatabase();
903
+ try {
904
+ const result = await database.prepare(
905
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='posts' LIMIT 1"
906
+ ).first();
907
+ return {
908
+ ok: true,
909
+ error: null,
910
+ postsTableExists: Boolean(result?.name)
911
+ };
912
+ } catch (e) {
913
+ return {
914
+ ok: false,
915
+ error: e instanceof Error ? e.message : String(e),
916
+ postsTableExists: false
917
+ };
918
+ }
919
+ }
920
+ var healthRoute = {
921
+ async GET() {
922
+ return healthRoute.handle(new Request("https://health.local/api/health"));
923
+ },
924
+ async handle(_request) {
925
+ const start = Date.now();
926
+ const probe = await probeDatabase();
927
+ const d1Ok = probe.ok;
928
+ const d1Error = probe.error;
929
+ let schemaOk = false;
930
+ let schemaError = null;
931
+ let schemaMissing = [];
932
+ try {
933
+ const schema = await runSchemaHealthChecks(getDatabase());
934
+ schemaOk = schema.ok;
935
+ schemaMissing = schema.missing;
936
+ if (schema.errors.length > 0) {
937
+ schemaError = schema.errors.join("; ");
938
+ } else if (schema.missing.length > 0) {
939
+ schemaError = `missing required schema: ${schema.missing.join(", ")}`;
940
+ }
941
+ } catch (e) {
942
+ schemaError = e instanceof Error ? e.message : String(e);
943
+ }
944
+ const allHealthy = d1Ok && schemaOk;
945
+ return NextResponse4.json(
946
+ {
947
+ status: allHealthy ? "ok" : "degraded",
948
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
949
+ uptime_ms: Date.now() - start,
950
+ checks: {
951
+ d1: d1Ok ? "ok" : "error",
952
+ d1_error: d1Error,
953
+ schema: schemaOk ? "ok" : "error",
954
+ schema_error: schemaError,
955
+ schema_missing: schemaMissing
956
+ },
957
+ version: "1.0.0"
958
+ },
959
+ {
960
+ status: allHealthy ? 200 : 503,
961
+ headers: {
962
+ "Cache-Control": "no-store",
963
+ "Access-Control-Allow-Origin": "*"
964
+ }
965
+ }
966
+ );
967
+ }
968
+ };
969
+ var GET4 = healthRoute.GET;
970
+ async function healthRouteHandle(request) {
971
+ return healthRoute.handle(request);
972
+ }
973
+
974
+ // src/worker/bootstrap.ts
975
+ function pathMatches(pathname, match) {
976
+ if (match.endsWith("/")) return pathname.startsWith(match);
977
+ return pathname === match || pathname.startsWith(`${match}/`);
978
+ }
979
+ function buildStaticRoutes() {
980
+ return [
981
+ {
982
+ match: (req) => new URL(req.url).pathname === "/api/health",
983
+ handle: healthRouteHandle
984
+ },
985
+ {
986
+ match: (req) => pathMatches(new URL(req.url).pathname, "/api/notion/media/"),
987
+ handle: notionMediaRoute.handle
988
+ },
989
+ {
990
+ match: (req) => pathMatches(new URL(req.url).pathname, "/api/files/"),
991
+ handle: filesRoute.handle
992
+ },
993
+ {
994
+ match: (req) => pathMatches(new URL(req.url).pathname, "/api/cdn/"),
995
+ handle: cdnRoute.handle
996
+ }
997
+ ];
998
+ }
999
+ function createNextionWorker(options) {
1000
+ const sources = options.sources;
1001
+ const auth = { databaseBinding: options.authConfig.databaseBinding };
1002
+ const routes = buildStaticRoutes();
1003
+ if (options.extraRoutes) {
1004
+ for (const [path, load] of Object.entries(options.extraRoutes)) {
1005
+ const modPromise = load();
1006
+ routes.push({
1007
+ match: (req) => new URL(req.url).pathname === path,
1008
+ handle: async (req) => {
1009
+ const mod = await modPromise;
1010
+ return mod.default(req, options, sources, auth);
1011
+ }
1012
+ });
1013
+ }
1014
+ }
1015
+ const middlewareOptions = {
1016
+ authConfig: options.authConfig,
1017
+ sessionLookup: options.sessionLookup
1018
+ };
1019
+ return {
1020
+ async fetch(request, env2, ctx) {
1021
+ void ctx;
1022
+ const gateResponse = await nextionMiddleware(
1023
+ request,
1024
+ env2,
1025
+ middlewareOptions
1026
+ );
1027
+ if (gateResponse) return gateResponse;
1028
+ for (const route of routes) {
1029
+ if (route.match(request)) return route.handle(request);
1030
+ }
1031
+ return null;
1032
+ }
1033
+ };
1034
+ }
1035
+
1036
+ // src/platform/capabilities.ts
1037
+ var cloudflareWorkersAdapter = {
1038
+ id: "cloudflare-workers",
1039
+ label: "Cloudflare Workers + D1",
1040
+ status: "active",
1041
+ services: {
1042
+ compute: "Cloudflare Workers via vinext",
1043
+ relationalStorage: "D1 through the runtime SQL adapter",
1044
+ objectStorage: "R2",
1045
+ imageOptimization: "Cloudflare Images",
1046
+ cache: "vinext CDN/data adapters and caches.default for media",
1047
+ authStorage: "D1 users and signed cookies"
1048
+ },
1049
+ capabilities: [
1050
+ "server-rendering",
1051
+ "edge-cache",
1052
+ "relational-storage",
1053
+ "object-storage",
1054
+ "image-optimization",
1055
+ "secrets",
1056
+ "observability"
1057
+ ]
1058
+ };
1059
+ var runtimeAdapters = [cloudflareWorkersAdapter];
1060
+ function getRuntimeAdapter(id) {
1061
+ return runtimeAdapters.find((adapter) => adapter.id === id);
1062
+ }
1063
+
1064
+ // src/doctor/doctor.ts
1065
+ function envValue(env2, name) {
1066
+ const value = String(env2[name] ?? "").trim();
1067
+ return value || void 0;
1068
+ }
1069
+ function hasEnv(env2, name) {
1070
+ return Boolean(envValue(env2, name));
1071
+ }
1072
+ function hasD1Binding(config, binding) {
1073
+ return Boolean(config?.d1_databases?.some((item) => item.binding === binding));
1074
+ }
1075
+ function hasR2Binding(config, binding) {
1076
+ return Boolean(config?.r2_buckets?.some((item) => item.binding === binding));
1077
+ }
1078
+ function hasImagesBinding(config, binding) {
1079
+ const images = config?.images;
1080
+ if (Array.isArray(images)) {
1081
+ return images.some((item) => item.binding === binding);
1082
+ }
1083
+ return images?.binding === binding;
1084
+ }
1085
+ function statusSummary(status) {
1086
+ if (status === "ok") return "ready";
1087
+ if (status === "warn") return "usable with warnings";
1088
+ return "missing required configuration";
1089
+ }
1090
+ function overallStatus(checks, models) {
1091
+ if (checks.some((check) => check.status === "missing") || models.some((model) => model.dataSourceStatus === "missing")) {
1092
+ return "missing";
1093
+ }
1094
+ if (checks.some((check) => check.status === "warn") || models.some((model) => model.dataSourceStatus === "warn")) {
1095
+ return "warn";
1096
+ }
1097
+ return "ok";
1098
+ }
1099
+ function cloudflareChecks(config) {
1100
+ return [
1101
+ {
1102
+ id: "runtime.database",
1103
+ label: "SQL database",
1104
+ status: hasD1Binding(config, "DB") ? "ok" : "missing",
1105
+ required: true,
1106
+ detail: hasD1Binding(config, "DB") ? "wrangler D1 binding DB is declared" : "wrangler D1 binding DB is missing",
1107
+ action: "Add a DB binding under d1_databases in wrangler.jsonc."
1108
+ },
1109
+ {
1110
+ id: "runtime.objectStorage",
1111
+ label: "Object storage",
1112
+ status: hasR2Binding(config, "ASSETS_BUCKET") ? "ok" : "warn",
1113
+ required: false,
1114
+ detail: hasR2Binding(config, "ASSETS_BUCKET") ? "wrangler R2 binding ASSETS_BUCKET is declared" : "uploads and persistent media cache need ASSETS_BUCKET",
1115
+ action: "Add an ASSETS_BUCKET R2 binding for uploads and media cache."
1116
+ },
1117
+ {
1118
+ id: "runtime.imageTransformer",
1119
+ label: "Image transformation",
1120
+ status: hasImagesBinding(config, "IMAGES") ? "ok" : "warn",
1121
+ required: false,
1122
+ detail: hasImagesBinding(config, "IMAGES") ? "wrangler Images binding IMAGES is declared" : "Notion media optimization will fall back without IMAGES",
1123
+ action: "Add a Cloudflare Images binding named IMAGES."
1124
+ },
1125
+ {
1126
+ id: "runtime.publicCache",
1127
+ label: "Public cache",
1128
+ status: "ok",
1129
+ required: true,
1130
+ detail: "vinext CDN adapter handles page cache; caches.default remains for media"
1131
+ },
1132
+ {
1133
+ id: "runtime.observability",
1134
+ label: "Observability",
1135
+ status: config?.observability?.enabled ? "ok" : "warn",
1136
+ required: false,
1137
+ detail: config?.observability?.enabled ? "wrangler observability is enabled" : "wrangler observability is not enabled",
1138
+ action: "Enable observability in wrangler.jsonc for production debugging."
1139
+ }
1140
+ ];
1141
+ }
1142
+ function notionChecks(env2) {
1143
+ return [
1144
+ {
1145
+ id: "notion.token",
1146
+ label: "Notion token",
1147
+ status: hasEnv(env2, "NOTION_TOKEN") ? "ok" : "missing",
1148
+ required: true,
1149
+ detail: hasEnv(env2, "NOTION_TOKEN") ? "NOTION_TOKEN is configured" : "NOTION_TOKEN is missing",
1150
+ action: "Set NOTION_TOKEN to an internal integration token."
1151
+ },
1152
+ {
1153
+ id: "notion.webhook",
1154
+ label: "Notion webhook verification",
1155
+ status: hasEnv(env2, "NOTION_WEBHOOK_VERIFICATION_TOKEN") ? "ok" : "warn",
1156
+ required: false,
1157
+ detail: hasEnv(env2, "NOTION_WEBHOOK_VERIFICATION_TOKEN") ? "NOTION_WEBHOOK_VERIFICATION_TOKEN is configured" : "instant content invalidation needs NOTION_WEBHOOK_VERIFICATION_TOKEN",
1158
+ action: "Set NOTION_WEBHOOK_VERIFICATION_TOKEN after creating the Notion webhook."
1159
+ }
1160
+ ];
1161
+ }
1162
+ function modelDoctorStatus(env2, model) {
1163
+ const hasConfiguredEnv = hasEnv(env2, model.source.dataSourceEnv);
1164
+ const hasDefault = Boolean(model.source.defaultDataSourceId);
1165
+ const dataSourceSource = hasConfiguredEnv ? "env" : hasDefault ? "default" : "missing";
1166
+ const dataSourceStatus = dataSourceSource === "missing" ? "missing" : "ok";
1167
+ return {
1168
+ dataSourceStatus,
1169
+ dataSourceSource
1170
+ };
1171
+ }
1172
+ function modelChecks(env2, models) {
1173
+ return models.map((model) => ({
1174
+ id: model.id,
1175
+ public: model.visibility.public,
1176
+ admin: model.visibility.admin,
1177
+ listPath: model.routes.listPath,
1178
+ detailPath: model.routes.detailPath,
1179
+ publicApiPath: model.routes.publicApiPath,
1180
+ dataSourceEnv: model.source.dataSourceEnv,
1181
+ ...modelDoctorStatus(env2, model)
1182
+ }));
1183
+ }
1184
+ function uniqueActions(checks) {
1185
+ return Array.from(
1186
+ new Set(
1187
+ checks.filter((check) => check.status !== "ok" && check.action).map((check) => check.action)
1188
+ )
1189
+ );
1190
+ }
1191
+ function omitResolvedActions(check) {
1192
+ if (check.status !== "ok") return check;
1193
+ return {
1194
+ ...check,
1195
+ action: void 0
1196
+ };
1197
+ }
1198
+ function buildNextionDoctorReport(options = {}) {
1199
+ const env2 = options.env ?? process.env;
1200
+ const runtimeId = options.runtimeId ?? currentRuntimeId();
1201
+ const adapter = getRuntimeAdapter(runtimeId);
1202
+ const checks = [
1203
+ ...cloudflareChecks(options.wranglerConfig),
1204
+ ...notionChecks(env2)
1205
+ ].map(omitResolvedActions);
1206
+ const models = modelChecks(env2, options.models ?? []);
1207
+ const status = overallStatus(checks, models);
1208
+ const modelActions = models.filter((model) => model.dataSourceStatus === "missing").map(
1209
+ (model) => `Set ${model.dataSourceEnv} for the ${model.id} content model or add a model defaultDataSourceId.`
1210
+ );
1211
+ return {
1212
+ overall: {
1213
+ status,
1214
+ summary: statusSummary(status)
1215
+ },
1216
+ runtime: {
1217
+ id: runtimeId,
1218
+ label: adapter?.label ?? runtimeId,
1219
+ adapterStatus: adapter?.status ?? "planned"
1220
+ },
1221
+ checks,
1222
+ models,
1223
+ nextSteps: [...uniqueActions(checks), ...modelActions]
1224
+ };
1225
+ }
1226
+
1227
+ // src/doctor/index.ts
1228
+ function deriveWranglerConfig(runtime) {
1229
+ return {
1230
+ d1_databases: runtime.getBinding("DB") ? [{ binding: "DB" }] : [],
1231
+ r2_buckets: runtime.getBinding("ASSETS_BUCKET") ? [{ binding: "ASSETS_BUCKET" }] : [],
1232
+ images: runtime.getBinding("IMAGES") ? { binding: "IMAGES" } : void 0
1233
+ };
1234
+ }
1235
+ function checkToFindingCode(checkId) {
1236
+ if (checkId === "runtime.database") return "missing-db-binding";
1237
+ if (checkId === "runtime.objectStorage") return "missing-r2-binding";
1238
+ if (checkId === "runtime.imageTransformer") return "missing-images-binding";
1239
+ if (checkId === "runtime.observability") return "observability-disabled";
1240
+ if (checkId === "notion.token") return "missing-notion-token";
1241
+ if (checkId === "notion.webhook") return "missing-notion-webhook";
1242
+ return checkId;
1243
+ }
1244
+ function checkToFinding(check) {
1245
+ const severity = check.status === "missing" ? "error" : check.status === "warn" ? "warning" : "info";
1246
+ return {
1247
+ code: checkToFindingCode(check.id),
1248
+ message: check.action ?? check.detail,
1249
+ severity
1250
+ };
1251
+ }
1252
+ function modelToFinding(model) {
1253
+ if (model.dataSourceStatus !== "missing") return null;
1254
+ return {
1255
+ code: `missing-data-source:${model.id}`,
1256
+ message: `Set ${model.dataSourceEnv} for the ${model.id} content model or add a model defaultDataSourceId.`,
1257
+ severity: "error"
1258
+ };
1259
+ }
1260
+ function runNextionDoctor(options) {
1261
+ const wranglerConfig = options.wranglerConfig ?? deriveWranglerConfig(options.runtime);
1262
+ const report = buildNextionDoctorReport({
1263
+ env: options.env,
1264
+ wranglerConfig,
1265
+ // Phase 6 will map `sources` (ContentSource[]) to the detailed model
1266
+ // shape used by `buildNextionDoctorReport`. For now, treat the
1267
+ // empty array as the default and rely on binding/env checks.
1268
+ models: []
1269
+ });
1270
+ const findings = [
1271
+ ...report.checks.filter((check) => check.status !== "ok").map(checkToFinding),
1272
+ ...report.models.map(modelToFinding).filter((finding) => finding !== null)
1273
+ ];
1274
+ return { ...report, findings };
1275
+ }
1276
+ export {
1277
+ createNextionWorker,
1278
+ defineContentSource,
1279
+ runNextionDoctor
1280
+ };
1281
+ //# sourceMappingURL=index.js.map