@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
@@ -0,0 +1,352 @@
1
+ // src/storage/routes/files.ts
2
+ import { NextResponse } from "next/server";
3
+
4
+ // src/util/env.ts
5
+ import { env } from "cloudflare:workers";
6
+ var workerEnv = env;
7
+
8
+ // src/platform/runtime.ts
9
+ function cacheRequestForKey(key) {
10
+ return new Request(key, { method: "GET" });
11
+ }
12
+ function createCloudflarePublicCacheAdapter(cache) {
13
+ return {
14
+ kind: "cloudflare-cache",
15
+ async match(key) {
16
+ return await cache.match(cacheRequestForKey(key)) ?? null;
17
+ },
18
+ put(key, response) {
19
+ return cache.put(cacheRequestForKey(key), response);
20
+ },
21
+ delete(key) {
22
+ return cache.delete(cacheRequestForKey(key));
23
+ }
24
+ };
25
+ }
26
+ function createCloudflareKeyValueCacheAdapter(namespace) {
27
+ return {
28
+ kind: "workers-kv",
29
+ async get(key, options) {
30
+ return await namespace.get(key, {
31
+ type: "json",
32
+ cacheTtl: options?.cacheTtl
33
+ });
34
+ },
35
+ async put(key, value, options) {
36
+ await namespace.put(key, JSON.stringify(value), {
37
+ expirationTtl: options?.expirationTtl,
38
+ metadata: options?.metadata
39
+ });
40
+ },
41
+ delete(key) {
42
+ return namespace.delete(key);
43
+ },
44
+ async list(options) {
45
+ const result = await namespace.list({
46
+ prefix: options?.prefix,
47
+ limit: options?.limit,
48
+ cursor: options?.cursor
49
+ });
50
+ return {
51
+ keys: result.keys.map((key) => ({ name: key.name })),
52
+ cursor: result.list_complete ? void 0 : result.cursor,
53
+ listComplete: result.list_complete
54
+ };
55
+ }
56
+ };
57
+ }
58
+ function r2ObjectToStoredObject(object) {
59
+ return {
60
+ body: object.body,
61
+ size: object.size,
62
+ etag: object.etag,
63
+ contentType: object.httpMetadata?.contentType
64
+ };
65
+ }
66
+ function createCloudflareRuntimePlatform(env2, options) {
67
+ const database = env2.DB ? {
68
+ kind: "d1",
69
+ prepare(query) {
70
+ return env2.DB.prepare(query);
71
+ },
72
+ async batch(statements) {
73
+ return await env2.DB.batch(
74
+ statements
75
+ );
76
+ }
77
+ } : null;
78
+ const objectStorage = env2.ASSETS_BUCKET ? {
79
+ kind: "r2",
80
+ async get(key) {
81
+ const object = await env2.ASSETS_BUCKET?.get(key);
82
+ return object ? r2ObjectToStoredObject(object) : null;
83
+ },
84
+ async put(key, value, options2) {
85
+ await env2.ASSETS_BUCKET?.put(key, value, {
86
+ httpMetadata: {
87
+ contentType: options2?.contentType,
88
+ cacheControl: options2?.cacheControl
89
+ },
90
+ customMetadata: options2?.metadata
91
+ });
92
+ },
93
+ async delete(key) {
94
+ await env2.ASSETS_BUCKET?.delete(key);
95
+ },
96
+ async list(options2) {
97
+ const listed = await env2.ASSETS_BUCKET?.list({
98
+ prefix: options2?.prefix,
99
+ limit: options2?.limit
100
+ });
101
+ return listed?.objects.map((object) => ({
102
+ key: object.key,
103
+ size: object.size,
104
+ uploaded: object.uploaded
105
+ })) ?? [];
106
+ }
107
+ } : null;
108
+ const imageTransformer = env2.IMAGES ? {
109
+ kind: "cloudflare-images",
110
+ async transform(body, options2) {
111
+ const result = await env2.IMAGES.input(body).transform(options2.width ? { width: options2.width } : {}).output({
112
+ format: options2.format,
113
+ quality: options2.quality
114
+ });
115
+ return {
116
+ body: result.image(),
117
+ contentType: result.contentType(),
118
+ response: () => result.response()
119
+ };
120
+ }
121
+ } : null;
122
+ const keyValueCache = env2.CONTENT_CACHE ? createCloudflareKeyValueCacheAdapter(env2.CONTENT_CACHE) : null;
123
+ return {
124
+ id: "cloudflare-workers",
125
+ database,
126
+ objectStorage,
127
+ imageTransformer,
128
+ keyValueCache,
129
+ publicCache: options?.publicCache ? createCloudflarePublicCacheAdapter(options.publicCache) : null
130
+ };
131
+ }
132
+
133
+ // src/platform/cloudflare-runtime.ts
134
+ function getDefaultCloudflareCache() {
135
+ const globalWithCaches = globalThis;
136
+ return globalWithCaches.caches?.default ?? null;
137
+ }
138
+ function getRuntimePlatform() {
139
+ return createCloudflareRuntimePlatform(workerEnv, {
140
+ publicCache: getDefaultCloudflareCache()
141
+ });
142
+ }
143
+
144
+ // src/platform/current.ts
145
+ function getRuntimePlatform2() {
146
+ return getRuntimePlatform();
147
+ }
148
+
149
+ // src/storage/routes/files.ts
150
+ var filesRoute = {
151
+ /**
152
+ * Next.js handler for `app/api/files/[...key]/route.ts`. Receives the
153
+ * catch-all key from the route params.
154
+ */
155
+ async GET(_request, props) {
156
+ const { key } = await props.params;
157
+ return filesRoute.handle(new Request(buildInternalUrl(_request, key)));
158
+ },
159
+ /**
160
+ * Worker-friendly handler. Extracts the catch-all key from the URL
161
+ * pathname (`/api/files/<key>`).
162
+ */
163
+ async handle(request) {
164
+ const key = readKeyFromUrl(request.url);
165
+ if (!key) {
166
+ return NextResponse.json({ error: "Invalid key" }, { status: 400 });
167
+ }
168
+ if (key.includes("..") || key.startsWith("/")) {
169
+ return NextResponse.json({ error: "Invalid key" }, { status: 400 });
170
+ }
171
+ const storage = getRuntimePlatform2().objectStorage;
172
+ if (!storage) {
173
+ return NextResponse.json(
174
+ { error: "Object storage not configured" },
175
+ { status: 503 }
176
+ );
177
+ }
178
+ const object = await storage.get(key);
179
+ if (!object) {
180
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
181
+ }
182
+ const headers = new Headers();
183
+ if (object.contentType) {
184
+ headers.set("Content-Type", object.contentType);
185
+ }
186
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
187
+ if (object.etag) headers.set("ETag", object.etag);
188
+ headers.set("Content-Length", String(object.size));
189
+ return new Response(object.body, { headers });
190
+ }
191
+ };
192
+ async function filesRouteHandle(request) {
193
+ return filesRoute.handle(request);
194
+ }
195
+ function buildInternalUrl(request, keyParts) {
196
+ const url = new URL(request.url);
197
+ url.pathname = `/api/files/${keyParts.map(encodeURIComponent).join("/")}`;
198
+ return url.toString();
199
+ }
200
+ function readKeyFromUrl(rawUrl) {
201
+ const url = new URL(rawUrl);
202
+ const prefix = "/api/files/";
203
+ if (!url.pathname.startsWith(prefix)) return null;
204
+ const encoded = url.pathname.slice(prefix.length);
205
+ if (!encoded) return null;
206
+ return decodeURIComponent(encoded);
207
+ }
208
+ var GET = filesRoute.GET;
209
+
210
+ // src/storage/routes/cdn.ts
211
+ import { NextResponse as NextResponse2 } from "next/server";
212
+ var DEFAULT_WIDTH = 1200;
213
+ var MAX_WIDTH = 2400;
214
+ var DEFAULT_QUALITY = 75;
215
+ var MIN_QUALITY = 40;
216
+ var MAX_QUALITY = 85;
217
+ var cdnRoute = {
218
+ async GET(request, props) {
219
+ const { key } = await props.params;
220
+ return cdnRoute.handle(new Request(buildInternalUrl2(request, key)));
221
+ },
222
+ async handle(request) {
223
+ const url = new URL(request.url);
224
+ const key = readKeyFromPathname(url.pathname);
225
+ if (!key) {
226
+ return NextResponse2.json({ error: "Invalid key" }, { status: 400 });
227
+ }
228
+ if (key.includes("..") || key.startsWith("/")) {
229
+ return NextResponse2.json({ error: "Invalid key" }, { status: 400 });
230
+ }
231
+ const platform = getRuntimePlatform2();
232
+ const storage = platform.objectStorage;
233
+ if (!storage) {
234
+ return NextResponse2.json(
235
+ { error: "Object storage not configured" },
236
+ { status: 503 }
237
+ );
238
+ }
239
+ const object = await storage.get(key);
240
+ if (!object) {
241
+ return NextResponse2.json({ error: "Not found" }, { status: 404 });
242
+ }
243
+ const accept = request.headers.get("accept") ?? "";
244
+ const isImage = object.contentType?.startsWith("image/") ?? false;
245
+ if (!isImage) {
246
+ return streamObject(object, {
247
+ "X-Debug-Cdn-Branch": "non-image",
248
+ "X-Debug-Cdn-Key": key
249
+ });
250
+ }
251
+ let outputFormat = null;
252
+ let outputQuality = void 0;
253
+ if (accept.includes("image/avif")) {
254
+ outputFormat = "image/avif";
255
+ outputQuality = 60;
256
+ } else if (accept.includes("image/webp")) {
257
+ outputFormat = "image/webp";
258
+ outputQuality = 75;
259
+ }
260
+ const isSvg = object.contentType === "image/svg+xml";
261
+ if (!outputFormat || isSvg || !platform.imageTransformer) {
262
+ return streamObject(object, {
263
+ "X-Debug-Cdn-Branch": isSvg ? "svg-bypass" : !platform.imageTransformer ? "transformer-bypass" : "format-bypass",
264
+ "X-Debug-Cdn-Accept": accept.includes("image/avif") ? "avif" : accept.includes("image/webp") ? "webp" : "other",
265
+ "X-Debug-Cdn-Key": key
266
+ });
267
+ }
268
+ try {
269
+ const width = clampInt(
270
+ url.searchParams.get("w"),
271
+ 64,
272
+ MAX_WIDTH,
273
+ DEFAULT_WIDTH
274
+ );
275
+ const quality = clampInt(
276
+ url.searchParams.get("q"),
277
+ MIN_QUALITY,
278
+ MAX_QUALITY,
279
+ outputQuality ?? DEFAULT_QUALITY
280
+ );
281
+ const result = await platform.imageTransformer.transform(object.body, {
282
+ width,
283
+ format: outputFormat,
284
+ quality
285
+ });
286
+ return new Response(result.body, {
287
+ headers: {
288
+ "Content-Type": result.contentType,
289
+ "Cache-Control": "public, max-age=31536000, immutable",
290
+ Vary: "Accept",
291
+ "X-Debug-Cdn-Branch": "transformed",
292
+ "X-Debug-Cdn-Key": key,
293
+ "X-Optimized-Width": String(width),
294
+ "X-Optimized-Quality": String(quality),
295
+ "X-Original-Format": object.contentType ?? "unknown",
296
+ "X-Optimized-Format": outputFormat
297
+ }
298
+ });
299
+ } catch (e) {
300
+ return streamObject(object, {
301
+ "X-Debug-Cdn-Branch": "transform-error-fallback",
302
+ "X-Debug-Cdn-Key": key,
303
+ "X-Debug-Cdn-Error": e instanceof Error ? e.name : "unknown"
304
+ });
305
+ }
306
+ }
307
+ };
308
+ function buildInternalUrl2(request, keyParts) {
309
+ const url = new URL(request.url);
310
+ url.pathname = `/api/cdn/${keyParts.map(encodeURIComponent).join("/")}`;
311
+ return url.toString();
312
+ }
313
+ function readKeyFromPathname(pathname) {
314
+ const prefix = "/api/cdn/";
315
+ if (!pathname.startsWith(prefix)) return null;
316
+ const encoded = pathname.slice(prefix.length);
317
+ if (!encoded) return null;
318
+ return decodeURIComponent(encoded);
319
+ }
320
+ function clampInt(value, min, max, fallback) {
321
+ const parsed = Number.parseInt(value ?? "", 10);
322
+ if (!Number.isFinite(parsed)) {
323
+ return fallback;
324
+ }
325
+ return Math.max(min, Math.min(max, parsed));
326
+ }
327
+ function streamObject(object, extraHeaders) {
328
+ const headers = new Headers();
329
+ if (object.contentType) {
330
+ headers.set("Content-Type", object.contentType);
331
+ }
332
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
333
+ headers.set("Content-Length", String(object.size));
334
+ if (object.etag) headers.set("ETag", object.etag);
335
+ for (const [key, value] of Object.entries(extraHeaders ?? {})) {
336
+ headers.set(key, value);
337
+ }
338
+ return new Response(object.body, { headers });
339
+ }
340
+ var GET2 = cdnRoute.GET;
341
+ async function cdnRouteHandle(request) {
342
+ return cdnRoute.handle(request);
343
+ }
344
+ export {
345
+ GET2 as GET_cdn,
346
+ GET as GET_files,
347
+ cdnRoute,
348
+ cdnRouteHandle,
349
+ filesRoute,
350
+ filesRouteHandle
351
+ };
352
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/storage/routes/files.ts","../../../src/util/env.ts","../../../src/platform/runtime.ts","../../../src/platform/cloudflare-runtime.ts","../../../src/platform/current.ts","../../../src/storage/routes/cdn.ts"],"sourcesContent":["// storage/routes/files.ts\n//\n// GET /api/files/[...key] - 从对象存储取文件并代理返回\n// 不公开 bucket/container,避免任何人都能列文件\n//\n// The package exports a route object with two surfaces:\n// - `GET` is the Next.js handler signature (request, { params }).\n// - `handle` is a single `(request) => Response` form used by the\n// Cloudflare Workers bootstrap in `@notionx/core/worker`.\n//\n// The bodies are identical; only the way the object key is read differs.\n\nimport { NextResponse } from \"next/server\";\nimport { getRuntimePlatform } from \"../../platform/current\";\n\nexport const dynamic = \"force-dynamic\";\n\ntype Props = {\n params: Promise<{ key: string[] }>;\n};\n\nexport const filesRoute = {\n /**\n * Next.js handler for `app/api/files/[...key]/route.ts`. Receives the\n * catch-all key from the route params.\n */\n async GET(_request: Request, props: Props) {\n const { key } = await props.params;\n return filesRoute.handle(new Request(buildInternalUrl(_request, key)));\n },\n /**\n * Worker-friendly handler. Extracts the catch-all key from the URL\n * pathname (`/api/files/<key>`).\n */\n async handle(request: Request): Promise<Response> {\n const key = readKeyFromUrl(request.url);\n if (!key) {\n return NextResponse.json({ error: \"Invalid key\" }, { status: 400 });\n }\n\n if (key.includes(\"..\") || key.startsWith(\"/\")) {\n return NextResponse.json({ error: \"Invalid key\" }, { status: 400 });\n }\n\n const storage = getRuntimePlatform().objectStorage;\n if (!storage) {\n return NextResponse.json(\n { error: \"Object storage not configured\" },\n { status: 503 }\n );\n }\n\n const object = await storage.get(key);\n if (!object) {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n }\n\n const headers = new Headers();\n if (object.contentType) {\n headers.set(\"Content-Type\", object.contentType);\n }\n headers.set(\"Cache-Control\", \"public, max-age=31536000, immutable\");\n if (object.etag) headers.set(\"ETag\", object.etag);\n headers.set(\"Content-Length\", String(object.size));\n\n return new Response(object.body, { headers });\n },\n};\n\n/**\n * Worker-friendly single-arg handler. Used by the Cloudflare Workers\n * bootstrap in `@notionx/core/worker`. Equivalent to\n * `filesRoute.handle`.\n */\nexport async function filesRouteHandle(request: Request): Promise<Response> {\n return filesRoute.handle(request);\n}\n\nfunction buildInternalUrl(request: Request, keyParts: string[]) {\n const url = new URL(request.url);\n url.pathname = `/api/files/${keyParts.map(encodeURIComponent).join(\"/\")}`;\n return url.toString();\n}\n\nfunction readKeyFromUrl(rawUrl: string): string | null {\n const url = new URL(rawUrl);\n const prefix = \"/api/files/\";\n if (!url.pathname.startsWith(prefix)) return null;\n const encoded = url.pathname.slice(prefix.length);\n if (!encoded) return null;\n return decodeURIComponent(encoded);\n}\n\n// Re-export as top-level function aliases for callers that prefer a\n// flat signature (e.g. the Next.js app/api/.../route.ts delegates).\nexport const GET = filesRoute.GET;\n","// lib/env.ts - 集中获取 Cloudflare bindings\n// 用 cloudflare:workers 模块(workerd 内置),作为平台 adapter 的绑定入口\n\n/// <reference types=\"@cloudflare/workers-types\" />\nimport { env } from \"cloudflare:workers\";\n\nexport type AppEnv = {\n DB: D1Database;\n ASSETS: Fetcher;\n IMAGES: ImagesBinding;\n ASSETS_BUCKET?: R2Bucket;\n CONTENT_CACHE?: KVNamespace;\n ADMIN_PASSWORD: string;\n ADMIN_EMAIL?: string;\n SITE_URL?: string;\n RESEND_API_KEY?: string;\n RESEND_FROM?: string;\n // Google OAuth 仍然兼容 Cloudflare Secret 作为兜底。\n // 实际生效值以 app_settings.google_client_id / google_client_secret 为准。\n GOOGLE_CLIENT_ID?: string;\n GOOGLE_CLIENT_SECRET?: string;\n /** Turnstile site key fallback when not stored in app_settings */\n TURNSTILE_SITE_KEY?: string;\n /** Turnstile secret — set via `wrangler secret put TURNSTILE_SECRET_KEY` */\n TURNSTILE_SECRET_KEY?: string;\n /** Notion integration token for the blog data source */\n NOTION_TOKEN?: string;\n /** Notion data source ID used by dataSources.query */\n NOTION_DATA_SOURCE_ID?: string;\n /** Notion data source ID for the public movie catalog */\n NOTION_MOVIES_DATA_SOURCE_ID?: string;\n /** Notion data source ID for localized movie copy */\n NOTION_MOVIE_TRANSLATIONS_DATA_SOURCE_ID?: string;\n /** Optional Notion API base URL for tests or proxies */\n NOTION_API_BASE_URL?: string;\n /** Optional Notion edit URL for admin handoff screens */\n NOTION_EDIT_BASE_URL?: string;\n /** Optional webhook verification token for Notion invalidation */\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n};\n\n// 强制类型:vinext 把 env 类型放在 env.d.ts(interface VinextEnv extends Env),\n// 但 TS server 经常解析不到。运行时一定有 DB,类型断言保证编译通过。\nexport const workerEnv = env as unknown as AppEnv;\n","import type { AppEnv } from \"../util/env\";\n\nexport type PlatformBindingEnv = Pick<\n AppEnv,\n \"ASSETS_BUCKET\" | \"CONTENT_CACHE\" | \"DB\" | \"IMAGES\"\n>;\n\nexport type StoredObject = {\n body: ReadableStream;\n size: number;\n etag?: string;\n contentType?: string;\n};\n\nexport type ObjectStoragePutOptions = {\n contentType?: string;\n cacheControl?: string;\n metadata?: Record<string, string>;\n};\n\nexport type ObjectStorageListItem = {\n key: string;\n size: number;\n uploaded: Date;\n};\n\nexport type ObjectStorageAdapter = {\n kind: \"r2\";\n get(key: string): Promise<StoredObject | null>;\n put(\n key: string,\n value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob,\n options?: ObjectStoragePutOptions\n ): Promise<void>;\n delete(key: string): Promise<void>;\n list(\n options?: { prefix?: string; limit?: number }\n ): Promise<ObjectStorageListItem[]>;\n};\n\nexport type ImageTransformOptions = {\n width?: number;\n format: \"image/avif\" | \"image/webp\";\n quality: number;\n};\n\nexport type ImageTransformResult = {\n body: ReadableStream;\n contentType: string;\n response(): Response;\n};\n\nexport type ImageTransformerAdapter = {\n kind: \"cloudflare-images\" | \"external\";\n transform(\n body: ReadableStream,\n options: ImageTransformOptions\n ): Promise<ImageTransformResult>;\n};\n\nexport type PublicCacheAdapter = {\n kind: \"cloudflare-cache\" | \"noop\" | \"external\";\n match(key: string): Promise<Response | null>;\n put(key: string, response: Response): Promise<void>;\n delete(key: string): Promise<boolean>;\n};\n\nexport type KeyValueCacheGetOptions = {\n cacheTtl?: number;\n};\n\nexport type KeyValueCachePutOptions = {\n expirationTtl?: number;\n metadata?: Record<string, string | number | boolean | null>;\n};\n\nexport type KeyValueCacheListOptions = {\n prefix?: string;\n limit?: number;\n cursor?: string;\n};\n\nexport type KeyValueCacheListResult = {\n keys: Array<{ name: string }>;\n cursor?: string;\n listComplete: boolean;\n};\n\nexport type KeyValueCacheAdapter = {\n kind: \"workers-kv\" | \"noop\" | \"external\";\n get<T = unknown>(\n key: string,\n options?: KeyValueCacheGetOptions\n ): Promise<T | null>;\n put<T = unknown>(\n key: string,\n value: T,\n options?: KeyValueCachePutOptions\n ): Promise<void>;\n delete(key: string): Promise<void>;\n list(options?: KeyValueCacheListOptions): Promise<KeyValueCacheListResult>;\n};\n\nexport type SqlValue = string | number | boolean | null;\n\nexport type SqlResult<T = Record<string, unknown>> = {\n results?: T[];\n success?: boolean;\n meta?: {\n changes?: number;\n duration?: number;\n last_row_id?: number;\n rows_read?: number;\n rows_written?: number;\n [key: string]: unknown;\n };\n};\n\nexport type SqlPreparedStatement = {\n bind(...values: SqlValue[]): SqlPreparedStatement;\n all<T = Record<string, unknown>>(): Promise<SqlResult<T>>;\n first<T = Record<string, unknown>>(columnName?: string): Promise<T | null>;\n run<T = Record<string, unknown>>(): Promise<SqlResult<T>>;\n};\n\nexport type SqlDatabaseAdapter = {\n kind: \"d1\";\n prepare(query: string): SqlPreparedStatement;\n batch<T = Record<string, unknown>>(\n statements: SqlPreparedStatement[]\n ): Promise<SqlResult<T>[]>;\n};\n\nexport type RuntimePlatform = {\n id: \"cloudflare-workers\";\n database: SqlDatabaseAdapter | null;\n objectStorage: ObjectStorageAdapter | null;\n imageTransformer: ImageTransformerAdapter | null;\n publicCache: PublicCacheAdapter | null;\n keyValueCache: KeyValueCacheAdapter | null;\n};\n\ntype CloudflareCacheLike = Pick<Cache, \"match\" | \"put\" | \"delete\">;\ntype CloudflareKvLike = Pick<KVNamespace, \"get\" | \"put\" | \"delete\" | \"list\">;\n\nfunction cacheRequestForKey(key: string) {\n return new Request(key, { method: \"GET\" });\n}\n\nexport function createCloudflarePublicCacheAdapter(\n cache: CloudflareCacheLike\n): PublicCacheAdapter {\n return {\n kind: \"cloudflare-cache\",\n async match(key) {\n return (await cache.match(cacheRequestForKey(key))) ?? null;\n },\n put(key, response) {\n return cache.put(cacheRequestForKey(key), response);\n },\n delete(key) {\n return cache.delete(cacheRequestForKey(key));\n },\n };\n}\n\nexport function createNoopPublicCacheAdapter(kind: \"noop\" = \"noop\"): PublicCacheAdapter {\n return {\n kind,\n async match() {\n return null;\n },\n async put() {},\n async delete() {\n return false;\n },\n };\n}\n\nexport function createCloudflareKeyValueCacheAdapter(\n namespace: CloudflareKvLike\n): KeyValueCacheAdapter {\n return {\n kind: \"workers-kv\",\n async get<T = unknown>(\n key: string,\n options?: KeyValueCacheGetOptions\n ): Promise<T | null> {\n return (await namespace.get(key, {\n type: \"json\",\n cacheTtl: options?.cacheTtl,\n })) as T | null;\n },\n async put(key, value, options) {\n await namespace.put(key, JSON.stringify(value), {\n expirationTtl: options?.expirationTtl,\n metadata: options?.metadata,\n });\n },\n delete(key) {\n return namespace.delete(key);\n },\n async list(options) {\n const result = await namespace.list({\n prefix: options?.prefix,\n limit: options?.limit,\n cursor: options?.cursor,\n });\n return {\n keys: result.keys.map((key) => ({ name: key.name })),\n cursor: result.list_complete ? undefined : result.cursor,\n listComplete: result.list_complete,\n };\n },\n };\n}\n\nexport function createNoopKeyValueCacheAdapter(\n kind: \"noop\" = \"noop\"\n): KeyValueCacheAdapter {\n return {\n kind,\n async get() {\n return null;\n },\n async put() {},\n async delete() {},\n async list() {\n return { keys: [], listComplete: true };\n },\n };\n}\n\nfunction r2ObjectToStoredObject(object: R2ObjectBody): StoredObject {\n return {\n body: object.body,\n size: object.size,\n etag: object.etag,\n contentType: object.httpMetadata?.contentType,\n };\n}\n\nexport function createCloudflareRuntimePlatform(\n env: PlatformBindingEnv,\n options?: { publicCache?: CloudflareCacheLike | null }\n): RuntimePlatform {\n const database: SqlDatabaseAdapter | null = env.DB\n ? ({\n kind: \"d1\",\n prepare(query: string) {\n return env.DB.prepare(query) as unknown as SqlPreparedStatement;\n },\n async batch(statements: SqlPreparedStatement[]) {\n return (await env.DB.batch(\n statements as unknown as D1PreparedStatement[]\n )) as unknown as SqlResult<Record<string, unknown>>[];\n },\n } as unknown as SqlDatabaseAdapter)\n : null;\n\n const objectStorage: ObjectStorageAdapter | null = env.ASSETS_BUCKET\n ? {\n kind: \"r2\",\n async get(key) {\n const object = await env.ASSETS_BUCKET?.get(key);\n return object ? r2ObjectToStoredObject(object) : null;\n },\n async put(key, value, options) {\n await env.ASSETS_BUCKET?.put(key, value, {\n httpMetadata: {\n contentType: options?.contentType,\n cacheControl: options?.cacheControl,\n },\n customMetadata: options?.metadata,\n });\n },\n async delete(key) {\n await env.ASSETS_BUCKET?.delete(key);\n },\n async list(options) {\n const listed = await env.ASSETS_BUCKET?.list({\n prefix: options?.prefix,\n limit: options?.limit,\n });\n return (\n listed?.objects.map((object) => ({\n key: object.key,\n size: object.size,\n uploaded: object.uploaded,\n })) ?? []\n );\n },\n }\n : null;\n\n const imageTransformer: ImageTransformerAdapter | null = env.IMAGES\n ? {\n kind: \"cloudflare-images\",\n async transform(body, options) {\n const result = await env.IMAGES.input(body)\n .transform(options.width ? { width: options.width } : {})\n .output({\n format: options.format,\n quality: options.quality,\n });\n return {\n body: result.image(),\n contentType: result.contentType(),\n response: () => result.response(),\n };\n },\n }\n : null;\n\n const keyValueCache: KeyValueCacheAdapter | null = env.CONTENT_CACHE\n ? createCloudflareKeyValueCacheAdapter(env.CONTENT_CACHE)\n : null;\n\n return {\n id: \"cloudflare-workers\",\n database,\n objectStorage,\n imageTransformer,\n keyValueCache,\n publicCache: options?.publicCache\n ? createCloudflarePublicCacheAdapter(options.publicCache)\n : null,\n };\n}\n","import { workerEnv } from \"../util/env\";\nimport {\n createCloudflarePublicCacheAdapter,\n createCloudflareRuntimePlatform,\n} from \"./runtime\";\n\nfunction getDefaultCloudflareCache() {\n const globalWithCaches = globalThis as typeof globalThis & {\n caches?: CacheStorage & { default?: Cache };\n };\n return globalWithCaches.caches?.default ?? null;\n}\n\nexport function getRuntimePlatform() {\n return createCloudflareRuntimePlatform(workerEnv, {\n publicCache: getDefaultCloudflareCache(),\n });\n}\n\nexport function getDatabase() {\n const database = getRuntimePlatform().database;\n if (!database) {\n throw new Error(\"SQL database binding not configured\");\n }\n return database;\n}\n\nexport function getPublicCache() {\n const cache = getDefaultCloudflareCache();\n if (!cache) {\n throw new Error(\"Cloudflare cache binding not configured\");\n }\n return createCloudflarePublicCacheAdapter(cache);\n}\n","import {\n getPublicCache as getCloudflarePublicCache,\n getRuntimePlatform as getCloudflareRuntimePlatform,\n} from \"./cloudflare-runtime\";\nimport { currentRuntimeId } from \"./selection\";\n\nexport function getRuntimePlatform() {\n return getCloudflareRuntimePlatform();\n}\n\nexport function getDatabase() {\n const platform = getRuntimePlatform();\n const database = platform.database;\n if (!database) {\n throw new Error(`SQL database adapter not configured for ${platform.id}`);\n }\n return database;\n}\n\nexport function getPublicCache() {\n return getCloudflarePublicCache();\n}\n\nexport function getKeyValueCache() {\n return getRuntimePlatform().keyValueCache;\n}\n\nexport const runtimeSelection = {\n currentRuntimeId,\n};\n","// storage/routes/cdn.ts\n//\n// GET /api/cdn/[...key] - 从对象存储取原图,调用运行时图片转换转 WebP/AVIF 后返回\n//\n// 用 catch-all 参数兼容两种 URL:\n// 1. /api/cdn/uploads/2026-06-06/file.jpg\n// 2. /api/cdn/uploads%2F2026-06-06%2Ffile.jpg\n//\n// Like the other storage route, the package exports a route object that\n// exposes both a Next.js handler and a worker-friendly single-arg\n// handler. The body is otherwise identical to the original starter\n// implementation.\n\nimport { NextResponse } from \"next/server\";\nimport {\n type StoredObject,\n} from \"../../platform/runtime\";\nimport { getRuntimePlatform } from \"../../platform/current\";\n\nexport const dynamic = \"force-dynamic\";\n\nconst DEFAULT_WIDTH = 1200;\nconst MAX_WIDTH = 2400;\nconst DEFAULT_QUALITY = 75;\nconst MIN_QUALITY = 40;\nconst MAX_QUALITY = 85;\n\ntype Props = {\n params: Promise<{ key: string[] }>;\n};\n\nexport const cdnRoute = {\n async GET(request: Request, props: Props) {\n const { key } = await props.params;\n return cdnRoute.handle(new Request(buildInternalUrl(request, key)));\n },\n async handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const key = readKeyFromPathname(url.pathname);\n if (!key) {\n return NextResponse.json({ error: \"Invalid key\" }, { status: 400 });\n }\n\n if (key.includes(\"..\") || key.startsWith(\"/\")) {\n return NextResponse.json({ error: \"Invalid key\" }, { status: 400 });\n }\n\n const platform = getRuntimePlatform();\n const storage = platform.objectStorage;\n if (!storage) {\n return NextResponse.json(\n { error: \"Object storage not configured\" },\n { status: 503 }\n );\n }\n\n const object = await storage.get(key);\n if (!object) {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n }\n\n const accept = request.headers.get(\"accept\") ?? \"\";\n const isImage = object.contentType?.startsWith(\"image/\") ?? false;\n\n if (!isImage) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": \"non-image\",\n \"X-Debug-Cdn-Key\": key,\n });\n }\n\n let outputFormat: \"image/avif\" | \"image/webp\" | null = null;\n let outputQuality: number | undefined = undefined;\n\n if (accept.includes(\"image/avif\")) {\n outputFormat = \"image/avif\";\n outputQuality = 60;\n } else if (accept.includes(\"image/webp\")) {\n outputFormat = \"image/webp\";\n outputQuality = 75;\n }\n\n const isSvg = object.contentType === \"image/svg+xml\";\n if (!outputFormat || isSvg || !platform.imageTransformer) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": isSvg\n ? \"svg-bypass\"\n : !platform.imageTransformer\n ? \"transformer-bypass\"\n : \"format-bypass\",\n \"X-Debug-Cdn-Accept\": accept.includes(\"image/avif\")\n ? \"avif\"\n : accept.includes(\"image/webp\")\n ? \"webp\"\n : \"other\",\n \"X-Debug-Cdn-Key\": key,\n });\n }\n\n try {\n const width = clampInt(\n url.searchParams.get(\"w\"),\n 64,\n MAX_WIDTH,\n DEFAULT_WIDTH\n );\n const quality = clampInt(\n url.searchParams.get(\"q\"),\n MIN_QUALITY,\n MAX_QUALITY,\n outputQuality ?? DEFAULT_QUALITY\n );\n\n const result = await platform.imageTransformer.transform(object.body, {\n width,\n format: outputFormat,\n quality,\n });\n\n return new Response(result.body, {\n headers: {\n \"Content-Type\": result.contentType,\n \"Cache-Control\": \"public, max-age=31536000, immutable\",\n Vary: \"Accept\",\n \"X-Debug-Cdn-Branch\": \"transformed\",\n \"X-Debug-Cdn-Key\": key,\n \"X-Optimized-Width\": String(width),\n \"X-Optimized-Quality\": String(quality),\n \"X-Original-Format\": object.contentType ?? \"unknown\",\n \"X-Optimized-Format\": outputFormat,\n },\n });\n } catch (e) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": \"transform-error-fallback\",\n \"X-Debug-Cdn-Key\": key,\n \"X-Debug-Cdn-Error\": e instanceof Error ? e.name : \"unknown\",\n });\n }\n },\n};\n\nfunction buildInternalUrl(request: Request, keyParts: string[]) {\n const url = new URL(request.url);\n url.pathname = `/api/cdn/${keyParts.map(encodeURIComponent).join(\"/\")}`;\n return url.toString();\n}\n\nfunction readKeyFromPathname(pathname: string): string | null {\n const prefix = \"/api/cdn/\";\n if (!pathname.startsWith(prefix)) return null;\n const encoded = pathname.slice(prefix.length);\n if (!encoded) return null;\n return decodeURIComponent(encoded);\n}\n\nfunction clampInt(\n value: string | null,\n min: number,\n max: number,\n fallback: number\n) {\n const parsed = Number.parseInt(value ?? \"\", 10);\n if (!Number.isFinite(parsed)) {\n return fallback;\n }\n\n return Math.max(min, Math.min(max, parsed));\n}\n\nfunction streamObject(\n object: StoredObject,\n extraHeaders?: Record<string, string>\n): Response {\n const headers = new Headers();\n if (object.contentType) {\n headers.set(\"Content-Type\", object.contentType);\n }\n headers.set(\"Cache-Control\", \"public, max-age=31536000, immutable\");\n headers.set(\"Content-Length\", String(object.size));\n if (object.etag) headers.set(\"ETag\", object.etag);\n for (const [key, value] of Object.entries(extraHeaders ?? {})) {\n headers.set(key, value);\n }\n return new Response(object.body, { headers });\n}\n\n// Top-level aliases for callers that prefer a flat signature (e.g. the\n// Next.js `app/api/.../route.ts` delegates).\nexport const GET = cdnRoute.GET;\n\n/**\n * Worker-friendly single-arg handler. Used by the Cloudflare Workers\n * bootstrap in `@notionx/core/worker`. Equivalent to\n * `cdnRoute.handle`.\n */\nexport async function cdnRouteHandle(request: Request): Promise<Response> {\n return cdnRoute.handle(request);\n}\n"],"mappings":";AAYA,SAAS,oBAAoB;;;ACR7B,SAAS,WAAW;AAuCb,IAAM,YAAY;;;ACsGzB,SAAS,mBAAmB,KAAa;AACvC,SAAO,IAAI,QAAQ,KAAK,EAAE,QAAQ,MAAM,CAAC;AAC3C;AAEO,SAAS,mCACd,OACoB;AACpB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,MAAM,KAAK;AACf,aAAQ,MAAM,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAM;AAAA,IACzD;AAAA,IACA,IAAI,KAAK,UAAU;AACjB,aAAO,MAAM,IAAI,mBAAmB,GAAG,GAAG,QAAQ;AAAA,IACpD;AAAA,IACA,OAAO,KAAK;AACV,aAAO,MAAM,OAAO,mBAAmB,GAAG,CAAC;AAAA,IAC7C;AAAA,EACF;AACF;AAeO,SAAS,qCACd,WACsB;AACtB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,IACJ,KACA,SACmB;AACnB,aAAQ,MAAM,UAAU,IAAI,KAAK;AAAA,QAC/B,MAAM;AAAA,QACN,UAAU,SAAS;AAAA,MACrB,CAAC;AAAA,IACH;AAAA,IACA,MAAM,IAAI,KAAK,OAAO,SAAS;AAC7B,YAAM,UAAU,IAAI,KAAK,KAAK,UAAU,KAAK,GAAG;AAAA,QAC9C,eAAe,SAAS;AAAA,QACxB,UAAU,SAAS;AAAA,MACrB,CAAC;AAAA,IACH;AAAA,IACA,OAAO,KAAK;AACV,aAAO,UAAU,OAAO,GAAG;AAAA,IAC7B;AAAA,IACA,MAAM,KAAK,SAAS;AAClB,YAAM,SAAS,MAAM,UAAU,KAAK;AAAA,QAClC,QAAQ,SAAS;AAAA,QACjB,OAAO,SAAS;AAAA,QAChB,QAAQ,SAAS;AAAA,MACnB,CAAC;AACD,aAAO;AAAA,QACL,MAAM,OAAO,KAAK,IAAI,CAAC,SAAS,EAAE,MAAM,IAAI,KAAK,EAAE;AAAA,QACnD,QAAQ,OAAO,gBAAgB,SAAY,OAAO;AAAA,QAClD,cAAc,OAAO;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF;AAkBA,SAAS,uBAAuB,QAAoC;AAClE,SAAO;AAAA,IACL,MAAM,OAAO;AAAA,IACb,MAAM,OAAO;AAAA,IACb,MAAM,OAAO;AAAA,IACb,aAAa,OAAO,cAAc;AAAA,EACpC;AACF;AAEO,SAAS,gCACdA,MACA,SACiB;AACjB,QAAM,WAAsCA,KAAI,KAC3C;AAAA,IACC,MAAM;AAAA,IACN,QAAQ,OAAe;AACrB,aAAOA,KAAI,GAAG,QAAQ,KAAK;AAAA,IAC7B;AAAA,IACA,MAAM,MAAM,YAAoC;AAC9C,aAAQ,MAAMA,KAAI,GAAG;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF,IACA;AAEJ,QAAM,gBAA6CA,KAAI,gBACnD;AAAA,IACE,MAAM;AAAA,IACN,MAAM,IAAI,KAAK;AACb,YAAM,SAAS,MAAMA,KAAI,eAAe,IAAI,GAAG;AAC/C,aAAO,SAAS,uBAAuB,MAAM,IAAI;AAAA,IACnD;AAAA,IACA,MAAM,IAAI,KAAK,OAAOC,UAAS;AAC7B,YAAMD,KAAI,eAAe,IAAI,KAAK,OAAO;AAAA,QACvC,cAAc;AAAA,UACZ,aAAaC,UAAS;AAAA,UACtB,cAAcA,UAAS;AAAA,QACzB;AAAA,QACA,gBAAgBA,UAAS;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,IACA,MAAM,OAAO,KAAK;AAChB,YAAMD,KAAI,eAAe,OAAO,GAAG;AAAA,IACrC;AAAA,IACA,MAAM,KAAKC,UAAS;AAClB,YAAM,SAAS,MAAMD,KAAI,eAAe,KAAK;AAAA,QAC3C,QAAQC,UAAS;AAAA,QACjB,OAAOA,UAAS;AAAA,MAClB,CAAC;AACD,aACE,QAAQ,QAAQ,IAAI,CAAC,YAAY;AAAA,QAC/B,KAAK,OAAO;AAAA,QACZ,MAAM,OAAO;AAAA,QACb,UAAU,OAAO;AAAA,MACnB,EAAE,KAAK,CAAC;AAAA,IAEZ;AAAA,EACF,IACA;AAEJ,QAAM,mBAAmDD,KAAI,SACzD;AAAA,IACE,MAAM;AAAA,IACN,MAAM,UAAU,MAAMC,UAAS;AAC7B,YAAM,SAAS,MAAMD,KAAI,OAAO,MAAM,IAAI,EACvC,UAAUC,SAAQ,QAAQ,EAAE,OAAOA,SAAQ,MAAM,IAAI,CAAC,CAAC,EACvD,OAAO;AAAA,QACN,QAAQA,SAAQ;AAAA,QAChB,SAASA,SAAQ;AAAA,MACnB,CAAC;AACH,aAAO;AAAA,QACL,MAAM,OAAO,MAAM;AAAA,QACnB,aAAa,OAAO,YAAY;AAAA,QAChC,UAAU,MAAM,OAAO,SAAS;AAAA,MAClC;AAAA,IACF;AAAA,EACF,IACA;AAEJ,QAAM,gBAA6CD,KAAI,gBACnD,qCAAqCA,KAAI,aAAa,IACtD;AAEJ,SAAO;AAAA,IACL,IAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,SAAS,cAClB,mCAAmC,QAAQ,WAAW,IACtD;AAAA,EACN;AACF;;;AClUA,SAAS,4BAA4B;AACnC,QAAM,mBAAmB;AAGzB,SAAO,iBAAiB,QAAQ,WAAW;AAC7C;AAEO,SAAS,qBAAqB;AACnC,SAAO,gCAAgC,WAAW;AAAA,IAChD,aAAa,0BAA0B;AAAA,EACzC,CAAC;AACH;;;ACXO,SAASE,sBAAqB;AACnC,SAAO,mBAA6B;AACtC;;;AJaO,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,EAKxB,MAAM,IAAI,UAAmB,OAAc;AACzC,UAAM,EAAE,IAAI,IAAI,MAAM,MAAM;AAC5B,WAAO,WAAW,OAAO,IAAI,QAAQ,iBAAiB,UAAU,GAAG,CAAC,CAAC;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAO,SAAqC;AAChD,UAAM,MAAM,eAAe,QAAQ,GAAG;AACtC,QAAI,CAAC,KAAK;AACR,aAAO,aAAa,KAAK,EAAE,OAAO,cAAc,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpE;AAEA,QAAI,IAAI,SAAS,IAAI,KAAK,IAAI,WAAW,GAAG,GAAG;AAC7C,aAAO,aAAa,KAAK,EAAE,OAAO,cAAc,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpE;AAEA,UAAM,UAAUC,oBAAmB,EAAE;AACrC,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,gCAAgC;AAAA,QACzC,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,QAAQ,IAAI,GAAG;AACpC,QAAI,CAAC,QAAQ;AACX,aAAO,aAAa,KAAK,EAAE,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClE;AAEA,UAAM,UAAU,IAAI,QAAQ;AAC5B,QAAI,OAAO,aAAa;AACtB,cAAQ,IAAI,gBAAgB,OAAO,WAAW;AAAA,IAChD;AACA,YAAQ,IAAI,iBAAiB,qCAAqC;AAClE,QAAI,OAAO,KAAM,SAAQ,IAAI,QAAQ,OAAO,IAAI;AAChD,YAAQ,IAAI,kBAAkB,OAAO,OAAO,IAAI,CAAC;AAEjD,WAAO,IAAI,SAAS,OAAO,MAAM,EAAE,QAAQ,CAAC;AAAA,EAC9C;AACF;AAOA,eAAsB,iBAAiB,SAAqC;AAC1E,SAAO,WAAW,OAAO,OAAO;AAClC;AAEA,SAAS,iBAAiB,SAAkB,UAAoB;AAC9D,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,MAAI,WAAW,cAAc,SAAS,IAAI,kBAAkB,EAAE,KAAK,GAAG,CAAC;AACvE,SAAO,IAAI,SAAS;AACtB;AAEA,SAAS,eAAe,QAA+B;AACrD,QAAM,MAAM,IAAI,IAAI,MAAM;AAC1B,QAAM,SAAS;AACf,MAAI,CAAC,IAAI,SAAS,WAAW,MAAM,EAAG,QAAO;AAC7C,QAAM,UAAU,IAAI,SAAS,MAAM,OAAO,MAAM;AAChD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,mBAAmB,OAAO;AACnC;AAIO,IAAM,MAAM,WAAW;;;AKlF9B,SAAS,gBAAAC,qBAAoB;AAQ7B,IAAM,gBAAgB;AACtB,IAAM,YAAY;AAClB,IAAM,kBAAkB;AACxB,IAAM,cAAc;AACpB,IAAM,cAAc;AAMb,IAAM,WAAW;AAAA,EACtB,MAAM,IAAI,SAAkB,OAAc;AACxC,UAAM,EAAE,IAAI,IAAI,MAAM,MAAM;AAC5B,WAAO,SAAS,OAAO,IAAI,QAAQC,kBAAiB,SAAS,GAAG,CAAC,CAAC;AAAA,EACpE;AAAA,EACA,MAAM,OAAO,SAAqC;AAChD,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,UAAM,MAAM,oBAAoB,IAAI,QAAQ;AAC5C,QAAI,CAAC,KAAK;AACR,aAAOC,cAAa,KAAK,EAAE,OAAO,cAAc,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpE;AAEA,QAAI,IAAI,SAAS,IAAI,KAAK,IAAI,WAAW,GAAG,GAAG;AAC7C,aAAOA,cAAa,KAAK,EAAE,OAAO,cAAc,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpE;AAEA,UAAM,WAAWC,oBAAmB;AACpC,UAAM,UAAU,SAAS;AACzB,QAAI,CAAC,SAAS;AACZ,aAAOD,cAAa;AAAA,QAClB,EAAE,OAAO,gCAAgC;AAAA,QACzC,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,QAAQ,IAAI,GAAG;AACpC,QAAI,CAAC,QAAQ;AACX,aAAOA,cAAa,KAAK,EAAE,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClE;AAEA,UAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AAChD,UAAM,UAAU,OAAO,aAAa,WAAW,QAAQ,KAAK;AAE5D,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,QAAQ;AAAA,QAC1B,sBAAsB;AAAA,QACtB,mBAAmB;AAAA,MACrB,CAAC;AAAA,IACH;AAEA,QAAI,eAAmD;AACvD,QAAI,gBAAoC;AAExC,QAAI,OAAO,SAAS,YAAY,GAAG;AACjC,qBAAe;AACf,sBAAgB;AAAA,IAClB,WAAW,OAAO,SAAS,YAAY,GAAG;AACxC,qBAAe;AACf,sBAAgB;AAAA,IAClB;AAEA,UAAM,QAAQ,OAAO,gBAAgB;AACrC,QAAI,CAAC,gBAAgB,SAAS,CAAC,SAAS,kBAAkB;AACxD,aAAO,aAAa,QAAQ;AAAA,QAC1B,sBAAsB,QAClB,eACA,CAAC,SAAS,mBACR,uBACA;AAAA,QACN,sBAAsB,OAAO,SAAS,YAAY,IAC9C,SACA,OAAO,SAAS,YAAY,IAC1B,SACA;AAAA,QACN,mBAAmB;AAAA,MACrB,CAAC;AAAA,IACH;AAEA,QAAI;AACF,YAAM,QAAQ;AAAA,QACZ,IAAI,aAAa,IAAI,GAAG;AAAA,QACxB;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,YAAM,UAAU;AAAA,QACd,IAAI,aAAa,IAAI,GAAG;AAAA,QACxB;AAAA,QACA;AAAA,QACA,iBAAiB;AAAA,MACnB;AAEA,YAAM,SAAS,MAAM,SAAS,iBAAiB,UAAU,OAAO,MAAM;AAAA,QACpE;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,MACF,CAAC;AAED,aAAO,IAAI,SAAS,OAAO,MAAM;AAAA,QAC/B,SAAS;AAAA,UACP,gBAAgB,OAAO;AAAA,UACvB,iBAAiB;AAAA,UACjB,MAAM;AAAA,UACN,sBAAsB;AAAA,UACtB,mBAAmB;AAAA,UACnB,qBAAqB,OAAO,KAAK;AAAA,UACjC,uBAAuB,OAAO,OAAO;AAAA,UACrC,qBAAqB,OAAO,eAAe;AAAA,UAC3C,sBAAsB;AAAA,QACxB;AAAA,MACF,CAAC;AAAA,IACH,SAAS,GAAG;AACV,aAAO,aAAa,QAAQ;AAAA,QAC1B,sBAAsB;AAAA,QACtB,mBAAmB;AAAA,QACnB,qBAAqB,aAAa,QAAQ,EAAE,OAAO;AAAA,MACrD,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,SAASD,kBAAiB,SAAkB,UAAoB;AAC9D,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,MAAI,WAAW,YAAY,SAAS,IAAI,kBAAkB,EAAE,KAAK,GAAG,CAAC;AACrE,SAAO,IAAI,SAAS;AACtB;AAEA,SAAS,oBAAoB,UAAiC;AAC5D,QAAM,SAAS;AACf,MAAI,CAAC,SAAS,WAAW,MAAM,EAAG,QAAO;AACzC,QAAM,UAAU,SAAS,MAAM,OAAO,MAAM;AAC5C,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,mBAAmB,OAAO;AACnC;AAEA,SAAS,SACP,OACA,KACA,KACA,UACA;AACA,QAAM,SAAS,OAAO,SAAS,SAAS,IAAI,EAAE;AAC9C,MAAI,CAAC,OAAO,SAAS,MAAM,GAAG;AAC5B,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC;AAC5C;AAEA,SAAS,aACP,QACA,cACU;AACV,QAAM,UAAU,IAAI,QAAQ;AAC5B,MAAI,OAAO,aAAa;AACtB,YAAQ,IAAI,gBAAgB,OAAO,WAAW;AAAA,EAChD;AACA,UAAQ,IAAI,iBAAiB,qCAAqC;AAClE,UAAQ,IAAI,kBAAkB,OAAO,OAAO,IAAI,CAAC;AACjD,MAAI,OAAO,KAAM,SAAQ,IAAI,QAAQ,OAAO,IAAI;AAChD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,gBAAgB,CAAC,CAAC,GAAG;AAC7D,YAAQ,IAAI,KAAK,KAAK;AAAA,EACxB;AACA,SAAO,IAAI,SAAS,OAAO,MAAM,EAAE,QAAQ,CAAC;AAC9C;AAIO,IAAMG,OAAM,SAAS;AAO5B,eAAsB,eAAe,SAAqC;AACxE,SAAO,SAAS,OAAO,OAAO;AAChC;","names":["env","options","getRuntimePlatform","getRuntimePlatform","NextResponse","buildInternalUrl","NextResponse","getRuntimePlatform","GET"]}
@@ -0,0 +1,94 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ /** A render-prop that draws a Turnstile widget for the given action. */
4
+ type TurnstileSlot = (props: {
5
+ action: string;
6
+ }) => ReactNode;
7
+ /** A server action that the page form submits to. */
8
+ type FormAction = (formData: FormData) => Promise<void> | void;
9
+ /** Bundle of UI primitives used by every auth page. */
10
+ interface AuthPageUI {
11
+ Button: (props: {
12
+ type?: "button" | "submit" | "reset";
13
+ asChild?: boolean;
14
+ variant?: "default" | "outline" | "secondary" | "ghost" | "destructive";
15
+ size?: "default" | "sm" | "lg" | "icon";
16
+ className?: string;
17
+ children?: ReactNode;
18
+ }) => ReactNode;
19
+ Input: (props: {
20
+ id?: string;
21
+ name: string;
22
+ type?: string;
23
+ placeholder?: string;
24
+ defaultValue?: string;
25
+ required?: boolean;
26
+ autoFocus?: boolean;
27
+ className?: string;
28
+ }) => ReactNode;
29
+ Label: (props: {
30
+ htmlFor: string;
31
+ children: ReactNode;
32
+ className?: string;
33
+ }) => ReactNode;
34
+ Card: (props: {
35
+ className?: string;
36
+ children: ReactNode;
37
+ }) => ReactNode;
38
+ CardHeader: (props: {
39
+ className?: string;
40
+ children: ReactNode;
41
+ }) => ReactNode;
42
+ CardTitle: (props: {
43
+ className?: string;
44
+ children: ReactNode;
45
+ }) => ReactNode;
46
+ CardDescription: (props: {
47
+ className?: string;
48
+ children: ReactNode;
49
+ }) => ReactNode;
50
+ CardContent: (props: {
51
+ className?: string;
52
+ children: ReactNode;
53
+ }) => ReactNode;
54
+ Separator: (props: {
55
+ className?: string;
56
+ }) => ReactNode;
57
+ Turnstile?: TurnstileSlot;
58
+ }
59
+ /** Common props every auth page receives. */
60
+ interface AuthPageContext {
61
+ ui: AuthPageUI;
62
+ /**
63
+ * Optional mapping from action name to server action. Pages fall back
64
+ * to a no-op form handler when the action is missing so dev environments
65
+ * without configured actions still render.
66
+ */
67
+ actions?: {
68
+ emailLogin?: FormAction;
69
+ register?: FormAction;
70
+ forgotPassword?: FormAction;
71
+ resetPassword?: FormAction;
72
+ resendVerification?: FormAction;
73
+ };
74
+ /**
75
+ * Optional helper that checks whether the user is already
76
+ * authenticated. When provided, the page redirects to
77
+ * `redirectWhenAuthenticated` (default `/admin`) on truthy return.
78
+ */
79
+ isAuthenticated?: () => Promise<boolean>;
80
+ redirectWhenAuthenticated?: string;
81
+ /** Get the current OAuth user, used to show "current session" hints. */
82
+ getCurrentUser?: () => Promise<{
83
+ email: string;
84
+ } | null>;
85
+ /** Get the Google OAuth config so the "Sign in with Google" button
86
+ * can be hidden when not configured. */
87
+ getGoogleOAuthConfig?: () => Promise<{
88
+ enabled: boolean;
89
+ clientId: string;
90
+ clientSecret: string;
91
+ } | null>;
92
+ }
93
+
94
+ export type { AuthPageContext as A, FormAction as F, TurnstileSlot as T, AuthPageUI as a };
@@ -0,0 +1,78 @@
1
+ export { ContentModelDefinition, ContentSource } from './content/models.js';
2
+ import './notion/types.js';
3
+
4
+ /**
5
+ * Names of the D1 tables that back the auth feature. Snake_case keys here
6
+ * mirror the migration SQL; consumers do not need to redefine them.
7
+ */
8
+ interface AuthTables {
9
+ users: string;
10
+ sessions: string;
11
+ passwordResets: string;
12
+ emailVerifications: string;
13
+ authRateLimits: string;
14
+ }
15
+ interface AuthSessionCookieConfig {
16
+ name: string;
17
+ maxAge: number;
18
+ secure: boolean;
19
+ }
20
+ interface AuthRolesConfig {
21
+ default: string;
22
+ vip: string;
23
+ admin: string;
24
+ }
25
+ interface AuthTurnstileConfig {
26
+ siteKeyEnv: string;
27
+ secretKeyEnv: string;
28
+ }
29
+ interface AuthEmailConfig {
30
+ provider: "resend" | "smtp" | "none";
31
+ fromEnv: string;
32
+ apiKeyEnv: string;
33
+ }
34
+ interface AuthOAuthGoogleConfig {
35
+ clientIdEnv: string;
36
+ clientSecretEnv: string;
37
+ }
38
+ interface AuthOAuthConfig {
39
+ google: AuthOAuthGoogleConfig;
40
+ }
41
+ interface AuthPasswordConfig {
42
+ minLength: number;
43
+ }
44
+ /**
45
+ * Configuration object passed to `createAuth`. Decouples the auth helpers
46
+ * from module-level state — the runtime resolves the database binding and
47
+ * environment variables through this object.
48
+ */
49
+ interface AuthConfig {
50
+ databaseBinding: string;
51
+ tables: AuthTables;
52
+ sessionCookie: AuthSessionCookieConfig;
53
+ roles: AuthRolesConfig;
54
+ turnstile?: AuthTurnstileConfig;
55
+ email?: AuthEmailConfig;
56
+ oauth?: AuthOAuthConfig;
57
+ password?: AuthPasswordConfig;
58
+ }
59
+
60
+ type AdminExtension = unknown;
61
+ /**
62
+ * A single entry in the admin sidebar nav. `labelKey` is resolved at
63
+ * render time against the i18n message catalog; `icon` is the lucide
64
+ * icon name; `requireRole` is an optional guard so an item disappears
65
+ * for viewers who do not have the named role.
66
+ */
67
+ interface AdminNavItem {
68
+ href: string;
69
+ labelKey: string;
70
+ icon?: string;
71
+ order?: number;
72
+ requireRole?: string;
73
+ external?: boolean;
74
+ }
75
+ type WorkerOptions = unknown;
76
+ type FoundationConfig = unknown;
77
+
78
+ export type { AdminExtension, AdminNavItem, AuthConfig, AuthEmailConfig, AuthOAuthConfig, AuthOAuthGoogleConfig, AuthPasswordConfig, AuthRolesConfig, AuthSessionCookieConfig, AuthTables, AuthTurnstileConfig, FoundationConfig, WorkerOptions };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,18 @@
1
+ export { A as AppEnv, w as workerEnv } from '../env-C5qu-0R-.js';
2
+ import { ClassValue } from 'clsx';
3
+
4
+ declare function getEnv(primary: string, ...fallbacks: string[]): string | undefined;
5
+
6
+ /** 站点公网 URL,用于邮件验证/重置密码链接。 */
7
+ declare function getSiteUrl(): string;
8
+
9
+ /** 从 Cloudflare / 反向代理请求头解析客户端 IP。 */
10
+ declare function getClientIp(): Promise<string | null>;
11
+
12
+ /**
13
+ * shadcn/ui 标准 cn() helper:合并 className 并解决 Tailwind 冲突
14
+ * 例子:cn("px-2 py-1", condition && "bg-red-500", "px-4") → "py-1 bg-red-500 px-4"
15
+ */
16
+ declare function cn(...inputs: ClassValue[]): string;
17
+
18
+ export { cn, getClientIp, getEnv, getSiteUrl };
@@ -0,0 +1,48 @@
1
+ // src/util/get-env.ts
2
+ function getEnv(primary, ...fallbacks) {
3
+ if (process.env[primary]) return process.env[primary];
4
+ for (const name of fallbacks) {
5
+ if (process.env[name]) return process.env[name];
6
+ }
7
+ return void 0;
8
+ }
9
+
10
+ // src/util/env.ts
11
+ import { env } from "cloudflare:workers";
12
+ var workerEnv = env;
13
+
14
+ // src/util/site-url.ts
15
+ var DEFAULT_SITE_URL = "https://moviebluebook.uk";
16
+ function getSiteUrl() {
17
+ return workerEnv.SITE_URL || DEFAULT_SITE_URL;
18
+ }
19
+
20
+ // src/util/request-ip.ts
21
+ import { headers } from "next/headers";
22
+ async function getClientIp() {
23
+ const h = await headers();
24
+ const cf = h.get("cf-connecting-ip")?.trim();
25
+ if (cf) return cf;
26
+ const forwarded = h.get("x-forwarded-for")?.trim();
27
+ if (forwarded) {
28
+ const first = forwarded.split(",")[0]?.trim();
29
+ if (first) return first;
30
+ }
31
+ const realIp = h.get("x-real-ip")?.trim();
32
+ return realIp || null;
33
+ }
34
+
35
+ // src/util/utils.ts
36
+ import { clsx } from "clsx";
37
+ import { twMerge } from "tailwind-merge";
38
+ function cn(...inputs) {
39
+ return twMerge(clsx(inputs));
40
+ }
41
+ export {
42
+ cn,
43
+ getClientIp,
44
+ getEnv,
45
+ getSiteUrl,
46
+ workerEnv
47
+ };
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/util/get-env.ts","../../src/util/env.ts","../../src/util/site-url.ts","../../src/util/request-ip.ts","../../src/util/utils.ts"],"sourcesContent":["export function getEnv(primary: string, ...fallbacks: string[]): string | undefined {\n if (process.env[primary]) return process.env[primary];\n for (const name of fallbacks) {\n if (process.env[name]) return process.env[name];\n }\n return undefined;\n}\n","// lib/env.ts - 集中获取 Cloudflare bindings\n// 用 cloudflare:workers 模块(workerd 内置),作为平台 adapter 的绑定入口\n\n/// <reference types=\"@cloudflare/workers-types\" />\nimport { env } from \"cloudflare:workers\";\n\nexport type AppEnv = {\n DB: D1Database;\n ASSETS: Fetcher;\n IMAGES: ImagesBinding;\n ASSETS_BUCKET?: R2Bucket;\n CONTENT_CACHE?: KVNamespace;\n ADMIN_PASSWORD: string;\n ADMIN_EMAIL?: string;\n SITE_URL?: string;\n RESEND_API_KEY?: string;\n RESEND_FROM?: string;\n // Google OAuth 仍然兼容 Cloudflare Secret 作为兜底。\n // 实际生效值以 app_settings.google_client_id / google_client_secret 为准。\n GOOGLE_CLIENT_ID?: string;\n GOOGLE_CLIENT_SECRET?: string;\n /** Turnstile site key fallback when not stored in app_settings */\n TURNSTILE_SITE_KEY?: string;\n /** Turnstile secret — set via `wrangler secret put TURNSTILE_SECRET_KEY` */\n TURNSTILE_SECRET_KEY?: string;\n /** Notion integration token for the blog data source */\n NOTION_TOKEN?: string;\n /** Notion data source ID used by dataSources.query */\n NOTION_DATA_SOURCE_ID?: string;\n /** Notion data source ID for the public movie catalog */\n NOTION_MOVIES_DATA_SOURCE_ID?: string;\n /** Notion data source ID for localized movie copy */\n NOTION_MOVIE_TRANSLATIONS_DATA_SOURCE_ID?: string;\n /** Optional Notion API base URL for tests or proxies */\n NOTION_API_BASE_URL?: string;\n /** Optional Notion edit URL for admin handoff screens */\n NOTION_EDIT_BASE_URL?: string;\n /** Optional webhook verification token for Notion invalidation */\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n};\n\n// 强制类型:vinext 把 env 类型放在 env.d.ts(interface VinextEnv extends Env),\n// 但 TS server 经常解析不到。运行时一定有 DB,类型断言保证编译通过。\nexport const workerEnv = env as unknown as AppEnv;\n","import { workerEnv } from \"./env\";\n\nconst DEFAULT_SITE_URL = \"https://moviebluebook.uk\";\n\n/** 站点公网 URL,用于邮件验证/重置密码链接。 */\nexport function getSiteUrl(): string {\n return workerEnv.SITE_URL || DEFAULT_SITE_URL;\n}\n","import { headers } from \"next/headers\";\n\n/** 从 Cloudflare / 反向代理请求头解析客户端 IP。 */\nexport async function getClientIp(): Promise<string | null> {\n const h = await headers();\n const cf = h.get(\"cf-connecting-ip\")?.trim();\n if (cf) return cf;\n\n const forwarded = h.get(\"x-forwarded-for\")?.trim();\n if (forwarded) {\n const first = forwarded.split(\",\")[0]?.trim();\n if (first) return first;\n }\n\n const realIp = h.get(\"x-real-ip\")?.trim();\n return realIp || null;\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * shadcn/ui 标准 cn() helper:合并 className 并解决 Tailwind 冲突\n * 例子:cn(\"px-2 py-1\", condition && \"bg-red-500\", \"px-4\") → \"py-1 bg-red-500 px-4\"\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";AAAO,SAAS,OAAO,YAAoB,WAAyC;AAClF,MAAI,QAAQ,IAAI,OAAO,EAAG,QAAO,QAAQ,IAAI,OAAO;AACpD,aAAW,QAAQ,WAAW;AAC5B,QAAI,QAAQ,IAAI,IAAI,EAAG,QAAO,QAAQ,IAAI,IAAI;AAAA,EAChD;AACA,SAAO;AACT;;;ACFA,SAAS,WAAW;AAuCb,IAAM,YAAY;;;ACzCzB,IAAM,mBAAmB;AAGlB,SAAS,aAAqB;AACnC,SAAO,UAAU,YAAY;AAC/B;;;ACPA,SAAS,eAAe;AAGxB,eAAsB,cAAsC;AAC1D,QAAM,IAAI,MAAM,QAAQ;AACxB,QAAM,KAAK,EAAE,IAAI,kBAAkB,GAAG,KAAK;AAC3C,MAAI,GAAI,QAAO;AAEf,QAAM,YAAY,EAAE,IAAI,iBAAiB,GAAG,KAAK;AACjD,MAAI,WAAW;AACb,UAAM,QAAQ,UAAU,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAC5C,QAAI,MAAO,QAAO;AAAA,EACpB;AAEA,QAAM,SAAS,EAAE,IAAI,WAAW,GAAG,KAAK;AACxC,SAAO,UAAU;AACnB;;;AChBA,SAAS,YAA6B;AACtC,SAAS,eAAe;AAMjB,SAAS,MAAM,QAAsB;AAC1C,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;","names":[]}