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