@notionx/core 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/auth/index.d.ts +1 -1
  2. package/dist/auth/index.js.map +1 -1
  3. package/dist/auth/rate-limit.js.map +1 -1
  4. package/dist/auth/routes/google-callback.js.map +1 -1
  5. package/dist/auth/routes/google.js.map +1 -1
  6. package/dist/auth/routes/index.js.map +1 -1
  7. package/dist/auth/routes/verify-email.js.map +1 -1
  8. package/dist/auth/routes/viewer.js.map +1 -1
  9. package/dist/auth/turnstile.js.map +1 -1
  10. package/dist/auth/user-session.d.ts +1 -1
  11. package/dist/auth/user-session.js.map +1 -1
  12. package/dist/auth/users.js.map +1 -1
  13. package/dist/content/index.d.ts +2 -2
  14. package/dist/content/index.js +8 -60
  15. package/dist/content/index.js.map +1 -1
  16. package/dist/content/revalidate.d.ts +2 -1
  17. package/dist/content/revalidate.js +5 -28
  18. package/dist/content/revalidate.js.map +1 -1
  19. package/dist/content/search-index.d.ts +1 -1
  20. package/dist/content/search-index.js.map +1 -1
  21. package/dist/content/search.d.ts +2 -5
  22. package/dist/content/search.js +3 -32
  23. package/dist/content/search.js.map +1 -1
  24. package/dist/doctor/cli.js +0 -0
  25. package/dist/email/index.js.map +1 -1
  26. package/dist/{env-C5qu-0R-.d.ts → env-hoez1e-n.d.ts} +0 -4
  27. package/dist/i18n/index.d.ts +18 -24
  28. package/dist/i18n/index.js +29 -54
  29. package/dist/i18n/index.js.map +1 -1
  30. package/dist/index.js +0 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/internal/admin/index.js.map +1 -1
  33. package/dist/media/index.js +3 -2
  34. package/dist/media/index.js.map +1 -1
  35. package/dist/media/routes/index.js +0 -1
  36. package/dist/media/routes/index.js.map +1 -1
  37. package/dist/media/routes/notion-media.js +0 -1
  38. package/dist/media/routes/notion-media.js.map +1 -1
  39. package/dist/notion/config.d.ts +1 -4
  40. package/dist/notion/config.js +1 -23
  41. package/dist/notion/config.js.map +1 -1
  42. package/dist/notion/content-cache.d.ts +1 -1
  43. package/dist/notion/generic-source.js +0 -1
  44. package/dist/notion/generic-source.js.map +1 -1
  45. package/dist/notion/index.d.ts +3 -3
  46. package/dist/notion/index.js +1 -23
  47. package/dist/notion/index.js.map +1 -1
  48. package/dist/notion/media.d.ts +1 -1
  49. package/dist/notion/media.js +1 -1
  50. package/dist/notion/media.js.map +1 -1
  51. package/dist/notion/routes/index.d.ts +1 -1
  52. package/dist/notion/routes/index.js +0 -1
  53. package/dist/notion/routes/index.js.map +1 -1
  54. package/dist/notion/routes/webhook.d.ts +1 -1
  55. package/dist/notion/routes/webhook.js +0 -1
  56. package/dist/notion/routes/webhook.js.map +1 -1
  57. package/dist/notion/types.d.ts +1 -73
  58. package/dist/notion/webhook.d.ts +1 -1
  59. package/dist/notion/webhook.js +0 -1
  60. package/dist/notion/webhook.js.map +1 -1
  61. package/dist/pages/index.js +0 -1
  62. package/dist/pages/index.js.map +1 -1
  63. package/dist/platform/current.d.ts +1 -1
  64. package/dist/platform/current.js.map +1 -1
  65. package/dist/platform/index.d.ts +1 -1
  66. package/dist/platform/index.js.map +1 -1
  67. package/dist/platform/runtime.d.ts +1 -1
  68. package/dist/storage/index.js.map +1 -1
  69. package/dist/storage/routes/cdn.js.map +1 -1
  70. package/dist/storage/routes/files.js.map +1 -1
  71. package/dist/storage/routes/index.js.map +1 -1
  72. package/dist/util/index.d.ts +1 -1
  73. package/dist/util/index.js +1 -2
  74. package/dist/util/index.js.map +1 -1
  75. package/dist/worker/index.js +0 -1
  76. package/dist/worker/index.js.map +1 -1
  77. package/dist/worker/routes/content-revalidate.d.ts +1 -1
  78. package/dist/worker/routes/health.js.map +1 -1
  79. package/dist/worker/routes/index.d.ts +1 -1
  80. package/dist/worker/routes/index.js.map +1 -1
  81. package/package.json +14 -12
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/internal/admin/settings.ts","../../../src/util/env.ts","../../../src/platform/runtime.ts","../../../src/platform/cloudflare-runtime.ts","../../../src/platform/current.ts","../../../src/internal/admin/schema-guard.ts","../../../src/internal/admin/admin.ts"],"sourcesContent":["// lib/settings.ts - 读取和更新后台系统设置(单管理员模型)\n// 数据保存在 SQL 表 app_settings,目前固定为 1 行。\n//\n// Internal to the package — not exposed via package.json exports. The\n// auth helpers (turnstile.ts, users.ts) call into the read functions;\n// admin pages in the starter import the update functions through a\n// re-export shim at `apps/moviebluebook/lib/settings.ts`.\n\nimport { cache } from \"react\";\nimport { workerEnv } from \"../../util/env\";\nimport { getDatabase } from \"../../platform/current\";\nimport {\n buildTurnstilePublicConfig,\n DEFAULT_TURNSTILE_PUBLIC_CONFIG,\n isSchemaDriftError,\n} from \"./schema-guard\";\n\nexport type AppSettings = {\n site_title: string;\n google_enabled: 0 | 1;\n google_client_id: string | null;\n google_client_secret: string | null;\n google_updated_at: string | null;\n turnstile_enabled: 0 | 1;\n turnstile_site_key: string | null;\n turnstile_updated_at: string | null;\n admin_email: string;\n updated_at: string;\n};\n\ntype Row = {\n site_title: string;\n google_enabled: number;\n google_client_id: string | null;\n google_client_secret: string | null;\n google_updated_at: string | null;\n turnstile_enabled: number;\n turnstile_site_key: string | null;\n turnstile_updated_at: string | null;\n admin_email: string;\n updated_at: string;\n};\n\nconst DEFAULT_ADMIN_EMAIL = \"zhaofilms@gmail.com\";\n\nfunction rowToSettings(r: Row): AppSettings {\n return {\n site_title: r.site_title,\n google_enabled: r.google_enabled === 1 ? 1 : 0,\n google_client_id: r.google_client_id,\n google_client_secret: r.google_client_secret,\n google_updated_at: r.google_updated_at,\n turnstile_enabled: r.turnstile_enabled === 1 ? 1 : 0,\n turnstile_site_key: r.turnstile_site_key,\n turnstile_updated_at: r.turnstile_updated_at,\n admin_email: r.admin_email,\n updated_at: r.updated_at,\n };\n}\n\nconst getAppSettingsCached = cache(async (): Promise<AppSettings> => {\n const row = await getDatabase().prepare(\n `SELECT site_title, google_enabled, google_client_id, google_client_secret,\n google_updated_at, turnstile_enabled, turnstile_site_key,\n turnstile_updated_at, admin_email, updated_at\n FROM app_settings WHERE id = 1`\n ).first<Row>();\n if (!row) {\n // 极端情况:迁移未执行\n return {\n site_title: \"vinext Blog\",\n google_enabled: 0,\n google_client_id: null,\n google_client_secret: null,\n google_updated_at: null,\n turnstile_enabled: 0,\n turnstile_site_key: null,\n turnstile_updated_at: null,\n admin_email: DEFAULT_ADMIN_EMAIL,\n updated_at: \"\",\n };\n }\n return rowToSettings(row);\n});\n\nexport async function getAppSettings(): Promise<AppSettings> {\n return getAppSettingsCached();\n}\n\n/** Turnstile 前端可见配置(site key 公开;secret 在 env)。 */\nexport async function getTurnstilePublicConfig(): Promise<{\n enabled: boolean;\n siteKey: string | null;\n secretConfigured: boolean;\n}> {\n try {\n const s = await getAppSettings();\n return buildTurnstilePublicConfig(s, workerEnv);\n } catch (error) {\n if (isSchemaDriftError(error)) {\n console.error(\n \"[settings] turnstile config unavailable due to schema drift; falling back to disabled state\",\n error\n );\n return { ...DEFAULT_TURNSTILE_PUBLIC_CONFIG };\n }\n throw error;\n }\n}\n\nexport async function updateTurnstileConfig(input: {\n enabled: boolean;\n siteKey: string;\n}): Promise<void> {\n const enabled = input.enabled ? 1 : 0;\n await getDatabase().prepare(\n `UPDATE app_settings\n SET turnstile_enabled = ?,\n turnstile_site_key = ?,\n turnstile_updated_at = datetime('now'),\n updated_at = datetime('now')\n WHERE id = 1`\n )\n .bind(enabled, input.siteKey || null)\n .run();\n}\n\nexport async function disableTurnstileConfig(): Promise<void> {\n await getDatabase().prepare(\n `UPDATE app_settings\n SET turnstile_enabled = 0,\n turnstile_updated_at = datetime('now'),\n updated_at = datetime('now')\n WHERE id = 1`\n ).run();\n}\n\n/** Google 登录实际配置:只有 enabled 且 id+secret 都存在才认为可用 */\nexport async function getGoogleOAuthConfig(): Promise<{\n enabled: boolean;\n clientId: string;\n clientSecret: string;\n} | null> {\n const s = await getAppSettings();\n if (!s.google_enabled) return null;\n if (!s.google_client_id || !s.google_client_secret) return null;\n return {\n enabled: true,\n clientId: s.google_client_id,\n clientSecret: s.google_client_secret,\n };\n}\n\nexport async function updateGoogleOAuthConfig(input: {\n enabled: boolean;\n clientId: string;\n clientSecret: string;\n}): Promise<void> {\n const enabled = input.enabled ? 1 : 0;\n await getDatabase().prepare(\n `UPDATE app_settings\n SET google_enabled = ?,\n google_client_id = ?,\n google_client_secret = ?,\n google_updated_at = datetime('now'),\n updated_at = datetime('now')\n WHERE id = 1`\n )\n .bind(enabled, input.clientId, input.clientSecret)\n .run();\n}\n\nexport async function clearGoogleOAuthConfig(): Promise<void> {\n await getDatabase().prepare(\n `UPDATE app_settings\n SET google_enabled = 0,\n google_client_id = NULL,\n google_client_secret = NULL,\n google_updated_at = datetime('now'),\n updated_at = datetime('now')\n WHERE id = 1`\n ).run();\n}\n\nexport async function updateSiteTitle(title: string): Promise<void> {\n await getDatabase().prepare(\n `UPDATE app_settings SET site_title = ?, updated_at = datetime('now') WHERE id = 1`\n )\n .bind(title)\n .run();\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 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","export const REQUIRED_SCHEMA_CHECKS = [\n {\n key: \"app_settings.turnstile_enabled\",\n sql: \"SELECT turnstile_enabled FROM app_settings LIMIT 1\",\n },\n {\n key: \"users.session_rev\",\n sql: \"SELECT session_rev FROM users LIMIT 1\",\n },\n {\n key: \"auth_rate_limits\",\n sql: \"SELECT 1 FROM auth_rate_limits LIMIT 1\",\n },\n];\n\nexport const DEFAULT_TURNSTILE_PUBLIC_CONFIG = {\n enabled: false,\n siteKey: null,\n secretConfigured: false,\n};\n\nexport function isSchemaDriftError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error ?? \"\");\n return (\n message.includes(\"no such column\") || message.includes(\"no such table\")\n );\n}\n\nexport function buildTurnstilePublicConfig(\n settings: { turnstile_enabled: number; turnstile_site_key: string | null },\n envLike: { TURNSTILE_SITE_KEY?: string; TURNSTILE_SECRET_KEY?: string }\n): { enabled: boolean; siteKey: string | null; secretConfigured: boolean } {\n const envSiteKey = envLike.TURNSTILE_SITE_KEY?.trim() || null;\n const siteKey = settings.turnstile_site_key?.trim() || envSiteKey || null;\n const secretConfigured = Boolean(envLike.TURNSTILE_SECRET_KEY?.trim());\n const enabled =\n (settings.turnstile_enabled === 1 || Boolean(envSiteKey)) &&\n Boolean(siteKey) &&\n secretConfigured;\n\n return {\n enabled,\n siteKey,\n secretConfigured,\n };\n}\n\nexport async function runSchemaHealthChecks(db: {\n prepare: (sql: string) => { first: () => Promise<unknown> };\n}): Promise<{ ok: boolean; missing: string[]; errors: string[] }> {\n const missing: string[] = [];\n const errors: string[] = [];\n\n for (const check of REQUIRED_SCHEMA_CHECKS) {\n try {\n await db.prepare(check.sql).first();\n } catch (error) {\n if (isSchemaDriftError(error)) {\n missing.push(check.key);\n } else {\n const message = error instanceof Error ? error.message : String(error);\n errors.push(`${check.key}: ${message}`);\n }\n }\n }\n\n return {\n ok: missing.length === 0 && errors.length === 0,\n missing,\n errors,\n };\n}\n","// lib/admin.ts - 单管理员身份识别\n// 设计:固定 admin_email = app_settings.admin_email(默认 zhaofilms@gmail.com)\n// 任何登录方式下,邮箱匹配即视为管理员。\n//\n// Internal to the package — not exposed via package.json exports.\n// The auth helpers (users.ts) call `isAdminEmail` to decide whether a\n// newly registered user gets the `admin` role. The starter still\n// re-exports this through `apps/moviebluebook/lib/admin.ts` for backward\n// compatibility with its own admin pages and tests.\n\nimport { getAppSettings } from \"./settings\";\nimport { getDatabase } from \"../../platform/current\";\n\nexport const DEFAULT_ADMIN_EMAIL = \"zhaofilms@gmail.com\";\n\nexport function normalizeEmail(email: string): string {\n return email.trim().toLowerCase();\n}\n\nexport async function isAdminEmail(email: string): Promise<boolean> {\n if (!email) return false;\n const settings = await getAppSettings();\n return normalizeEmail(email) === normalizeEmail(settings.admin_email);\n}\n\n/**\n * 提升某邮箱为管理员(仅在系统启动时、且匹配 admin_email 时调用)。\n * 实际上是把 users.role 置为 'admin',并清空其它冲突状态。\n */\nexport async function ensureAdminUser(email: string): Promise<void> {\n const normalized = normalizeEmail(email);\n if (!(await isAdminEmail(normalized))) return;\n await getDatabase().prepare(\n `UPDATE users SET role = 'admin' WHERE email = ?`\n ).bind(normalized).run();\n}\n"],"mappings":"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","names":["cache","env","options","getRuntimePlatform","DEFAULT_ADMIN_EMAIL"]}
1
+ {"version":3,"sources":["../../../src/internal/admin/settings.ts","../../../src/util/env.ts","../../../src/platform/runtime.ts","../../../src/platform/cloudflare-runtime.ts","../../../src/platform/current.ts","../../../src/internal/admin/schema-guard.ts","../../../src/internal/admin/admin.ts"],"sourcesContent":["// lib/settings.ts - 读取和更新后台系统设置(单管理员模型)\n// 数据保存在 SQL 表 app_settings,目前固定为 1 行。\n//\n// Internal to the package — not exposed via package.json exports. The\n// auth helpers (turnstile.ts, users.ts) call into the read functions;\n// consumer apps may re-export the update functions for local admin pages.\n\nimport { cache } from \"react\";\nimport { workerEnv } from \"../../util/env\";\nimport { getDatabase } from \"../../platform/current\";\nimport {\n buildTurnstilePublicConfig,\n DEFAULT_TURNSTILE_PUBLIC_CONFIG,\n isSchemaDriftError,\n} from \"./schema-guard\";\n\nexport type AppSettings = {\n site_title: string;\n google_enabled: 0 | 1;\n google_client_id: string | null;\n google_client_secret: string | null;\n google_updated_at: string | null;\n turnstile_enabled: 0 | 1;\n turnstile_site_key: string | null;\n turnstile_updated_at: string | null;\n admin_email: string;\n updated_at: string;\n};\n\ntype Row = {\n site_title: string;\n google_enabled: number;\n google_client_id: string | null;\n google_client_secret: string | null;\n google_updated_at: string | null;\n turnstile_enabled: number;\n turnstile_site_key: string | null;\n turnstile_updated_at: string | null;\n admin_email: string;\n updated_at: string;\n};\n\nconst DEFAULT_ADMIN_EMAIL = \"zhaofilms@gmail.com\";\n\nfunction rowToSettings(r: Row): AppSettings {\n return {\n site_title: r.site_title,\n google_enabled: r.google_enabled === 1 ? 1 : 0,\n google_client_id: r.google_client_id,\n google_client_secret: r.google_client_secret,\n google_updated_at: r.google_updated_at,\n turnstile_enabled: r.turnstile_enabled === 1 ? 1 : 0,\n turnstile_site_key: r.turnstile_site_key,\n turnstile_updated_at: r.turnstile_updated_at,\n admin_email: r.admin_email,\n updated_at: r.updated_at,\n };\n}\n\nconst getAppSettingsCached = cache(async (): Promise<AppSettings> => {\n const row = await getDatabase().prepare(\n `SELECT site_title, google_enabled, google_client_id, google_client_secret,\n google_updated_at, turnstile_enabled, turnstile_site_key,\n turnstile_updated_at, admin_email, updated_at\n FROM app_settings WHERE id = 1`\n ).first<Row>();\n if (!row) {\n // 极端情况:迁移未执行\n return {\n site_title: \"vinext Blog\",\n google_enabled: 0,\n google_client_id: null,\n google_client_secret: null,\n google_updated_at: null,\n turnstile_enabled: 0,\n turnstile_site_key: null,\n turnstile_updated_at: null,\n admin_email: DEFAULT_ADMIN_EMAIL,\n updated_at: \"\",\n };\n }\n return rowToSettings(row);\n});\n\nexport async function getAppSettings(): Promise<AppSettings> {\n return getAppSettingsCached();\n}\n\n/** Turnstile 前端可见配置(site key 公开;secret 在 env)。 */\nexport async function getTurnstilePublicConfig(): Promise<{\n enabled: boolean;\n siteKey: string | null;\n secretConfigured: boolean;\n}> {\n try {\n const s = await getAppSettings();\n return buildTurnstilePublicConfig(s, workerEnv);\n } catch (error) {\n if (isSchemaDriftError(error)) {\n console.error(\n \"[settings] turnstile config unavailable due to schema drift; falling back to disabled state\",\n error\n );\n return { ...DEFAULT_TURNSTILE_PUBLIC_CONFIG };\n }\n throw error;\n }\n}\n\nexport async function updateTurnstileConfig(input: {\n enabled: boolean;\n siteKey: string;\n}): Promise<void> {\n const enabled = input.enabled ? 1 : 0;\n await getDatabase().prepare(\n `UPDATE app_settings\n SET turnstile_enabled = ?,\n turnstile_site_key = ?,\n turnstile_updated_at = datetime('now'),\n updated_at = datetime('now')\n WHERE id = 1`\n )\n .bind(enabled, input.siteKey || null)\n .run();\n}\n\nexport async function disableTurnstileConfig(): Promise<void> {\n await getDatabase().prepare(\n `UPDATE app_settings\n SET turnstile_enabled = 0,\n turnstile_updated_at = datetime('now'),\n updated_at = datetime('now')\n WHERE id = 1`\n ).run();\n}\n\n/** Google 登录实际配置:只有 enabled 且 id+secret 都存在才认为可用 */\nexport async function getGoogleOAuthConfig(): Promise<{\n enabled: boolean;\n clientId: string;\n clientSecret: string;\n} | null> {\n const s = await getAppSettings();\n if (!s.google_enabled) return null;\n if (!s.google_client_id || !s.google_client_secret) return null;\n return {\n enabled: true,\n clientId: s.google_client_id,\n clientSecret: s.google_client_secret,\n };\n}\n\nexport async function updateGoogleOAuthConfig(input: {\n enabled: boolean;\n clientId: string;\n clientSecret: string;\n}): Promise<void> {\n const enabled = input.enabled ? 1 : 0;\n await getDatabase().prepare(\n `UPDATE app_settings\n SET google_enabled = ?,\n google_client_id = ?,\n google_client_secret = ?,\n google_updated_at = datetime('now'),\n updated_at = datetime('now')\n WHERE id = 1`\n )\n .bind(enabled, input.clientId, input.clientSecret)\n .run();\n}\n\nexport async function clearGoogleOAuthConfig(): Promise<void> {\n await getDatabase().prepare(\n `UPDATE app_settings\n SET google_enabled = 0,\n google_client_id = NULL,\n google_client_secret = NULL,\n google_updated_at = datetime('now'),\n updated_at = datetime('now')\n WHERE id = 1`\n ).run();\n}\n\nexport async function updateSiteTitle(title: string): Promise<void> {\n await getDatabase().prepare(\n `UPDATE app_settings SET site_title = ?, updated_at = datetime('now') WHERE id = 1`\n )\n .bind(title)\n .run();\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 /** 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","export const REQUIRED_SCHEMA_CHECKS = [\n {\n key: \"app_settings.turnstile_enabled\",\n sql: \"SELECT turnstile_enabled FROM app_settings LIMIT 1\",\n },\n {\n key: \"users.session_rev\",\n sql: \"SELECT session_rev FROM users LIMIT 1\",\n },\n {\n key: \"auth_rate_limits\",\n sql: \"SELECT 1 FROM auth_rate_limits LIMIT 1\",\n },\n];\n\nexport const DEFAULT_TURNSTILE_PUBLIC_CONFIG = {\n enabled: false,\n siteKey: null,\n secretConfigured: false,\n};\n\nexport function isSchemaDriftError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error ?? \"\");\n return (\n message.includes(\"no such column\") || message.includes(\"no such table\")\n );\n}\n\nexport function buildTurnstilePublicConfig(\n settings: { turnstile_enabled: number; turnstile_site_key: string | null },\n envLike: { TURNSTILE_SITE_KEY?: string; TURNSTILE_SECRET_KEY?: string }\n): { enabled: boolean; siteKey: string | null; secretConfigured: boolean } {\n const envSiteKey = envLike.TURNSTILE_SITE_KEY?.trim() || null;\n const siteKey = settings.turnstile_site_key?.trim() || envSiteKey || null;\n const secretConfigured = Boolean(envLike.TURNSTILE_SECRET_KEY?.trim());\n const enabled =\n (settings.turnstile_enabled === 1 || Boolean(envSiteKey)) &&\n Boolean(siteKey) &&\n secretConfigured;\n\n return {\n enabled,\n siteKey,\n secretConfigured,\n };\n}\n\nexport async function runSchemaHealthChecks(db: {\n prepare: (sql: string) => { first: () => Promise<unknown> };\n}): Promise<{ ok: boolean; missing: string[]; errors: string[] }> {\n const missing: string[] = [];\n const errors: string[] = [];\n\n for (const check of REQUIRED_SCHEMA_CHECKS) {\n try {\n await db.prepare(check.sql).first();\n } catch (error) {\n if (isSchemaDriftError(error)) {\n missing.push(check.key);\n } else {\n const message = error instanceof Error ? error.message : String(error);\n errors.push(`${check.key}: ${message}`);\n }\n }\n }\n\n return {\n ok: missing.length === 0 && errors.length === 0,\n missing,\n errors,\n };\n}\n","// lib/admin.ts - 单管理员身份识别\n// 设计:固定 admin_email = app_settings.admin_email(默认 zhaofilms@gmail.com)\n// 任何登录方式下,邮箱匹配即视为管理员。\n//\n// Internal to the package — not exposed via package.json exports.\n// The auth helpers (users.ts) call `isAdminEmail` to decide whether a\n// newly registered user gets the `admin` role. Consumer apps may\n// re-export this for backward compatibility with local imports.\n\nimport { getAppSettings } from \"./settings\";\nimport { getDatabase } from \"../../platform/current\";\n\nexport const DEFAULT_ADMIN_EMAIL = \"zhaofilms@gmail.com\";\n\nexport function normalizeEmail(email: string): string {\n return email.trim().toLowerCase();\n}\n\nexport async function isAdminEmail(email: string): Promise<boolean> {\n if (!email) return false;\n const settings = await getAppSettings();\n return normalizeEmail(email) === normalizeEmail(settings.admin_email);\n}\n\n/**\n * 提升某邮箱为管理员(仅在系统启动时、且匹配 admin_email 时调用)。\n * 实际上是把 users.role 置为 'admin',并清空其它冲突状态。\n */\nexport async function ensureAdminUser(email: string): Promise<void> {\n const normalized = normalizeEmail(email);\n if (!(await isAdminEmail(normalized))) return;\n await getDatabase().prepare(\n `UPDATE users SET role = 'admin' WHERE email = ?`\n ).bind(normalized).run();\n}\n"],"mappings":"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","names":["cache","env","options","getRuntimePlatform","DEFAULT_ADMIN_EMAIL"]}
@@ -4,6 +4,7 @@ var DEFAULT_IMAGE_QUALITY = 85;
4
4
  var MIN_IMAGE_QUALITY = 40;
5
5
  var LIST_IMAGE_WIDTHS = [320, 480, 640];
6
6
  var LIST_IMAGE_QUALITY = 70;
7
+ var RELATIVE_URL_BASE = "http://localhost";
7
8
  function clampQuality(quality) {
8
9
  if (!quality || Number.isNaN(quality)) {
9
10
  return DEFAULT_IMAGE_QUALITY;
@@ -12,7 +13,7 @@ function clampQuality(quality) {
12
13
  }
13
14
  function parseImageUrl(src) {
14
15
  try {
15
- return new URL(src, "https://moviebluebook.uk");
16
+ return new URL(src, RELATIVE_URL_BASE);
16
17
  } catch {
17
18
  return null;
18
19
  }
@@ -39,7 +40,7 @@ function buildSizedImageUrl(src, width, quality) {
39
40
  if (!url) return src;
40
41
  url.searchParams.set("w", String(width));
41
42
  url.searchParams.set("q", String(quality));
42
- return url.origin === "https://moviebluebook.uk" ? `${url.pathname}${url.search}${url.hash}` : url.toString();
43
+ return url.origin === RELATIVE_URL_BASE ? `${url.pathname}${url.search}${url.hash}` : url.toString();
43
44
  }
44
45
  function widthsForVariant(variant) {
45
46
  return variant === "list" ? LIST_IMAGE_WIDTHS : DEFAULT_IMAGE_WIDTHS;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/media/public-image.ts"],"sourcesContent":["const DEFAULT_IMAGE_WIDTHS = [320, 640, 960, 1200] as const;\nconst DEFAULT_IMAGE_QUALITY = 85;\nconst MIN_IMAGE_QUALITY = 40;\n\nconst LIST_IMAGE_WIDTHS = [320, 480, 640] as const;\nconst LIST_IMAGE_QUALITY = 70;\n\nexport type PublicImageVariant = \"list\" | \"detail\";\n\nfunction clampQuality(quality?: number) {\n if (!quality || Number.isNaN(quality)) {\n return DEFAULT_IMAGE_QUALITY;\n }\n\n return Math.max(MIN_IMAGE_QUALITY, Math.min(DEFAULT_IMAGE_QUALITY, quality));\n}\n\nfunction parseImageUrl(src: string) {\n try {\n return new URL(src, \"https://moviebluebook.uk\");\n } catch {\n return null;\n }\n}\n\nexport function isOptimizableCoverImage(src: string) {\n const url = parseImageUrl(src);\n if (!url) return false;\n\n return (\n url.pathname.startsWith(\"/api/cdn/\") ||\n url.pathname.startsWith(\"/api/notion/media/\")\n );\n}\n\nexport function isPublicImageUrlAllowed(src: string) {\n const url = parseImageUrl(src);\n if (!url) return false;\n\n if (url.pathname.startsWith(\"/api/cdn/\")) return true;\n if (url.pathname.startsWith(\"/api/notion/media/\")) return true;\n\n return [\n \"www.notion.so\",\n \"notion.so\",\n \"secure.notion-static.com\",\n \"prod-files-secure.s3.us-west-2.amazonaws.com\",\n ].includes(url.hostname);\n}\n\nfunction buildSizedImageUrl(src: string, width: number, quality: number) {\n const url = parseImageUrl(src);\n if (!url) return src;\n\n url.searchParams.set(\"w\", String(width));\n url.searchParams.set(\"q\", String(quality));\n\n return url.origin === \"https://moviebluebook.uk\"\n ? `${url.pathname}${url.search}${url.hash}`\n : url.toString();\n}\n\nfunction widthsForVariant(variant: PublicImageVariant) {\n return variant === \"list\" ? LIST_IMAGE_WIDTHS : DEFAULT_IMAGE_WIDTHS;\n}\n\nfunction qualityForVariant(variant: PublicImageVariant) {\n return variant === \"list\" ? LIST_IMAGE_QUALITY : DEFAULT_IMAGE_QUALITY;\n}\n\nexport function buildResponsiveImageAttrs(\n src: string,\n sizes: string,\n options?: { quality?: number; variant?: PublicImageVariant }\n) {\n if (!isOptimizableCoverImage(src)) {\n return { src, sizes };\n }\n\n const variant: PublicImageVariant = options?.variant ?? \"detail\";\n const explicitQuality = options?.quality;\n const quality = clampQuality(\n explicitQuality ?? qualityForVariant(variant)\n );\n const widths = widthsForVariant(variant);\n const max = widths[widths.length - 1] ?? DEFAULT_IMAGE_WIDTHS.at(-1) ?? 1200;\n\n return {\n src: buildSizedImageUrl(src, max, quality),\n srcSet: widths\n .map((width) => `${buildSizedImageUrl(src, width, quality)} ${width}w`)\n .join(\", \"),\n sizes,\n };\n}\n\nexport function getCoverImageLoading(\n index: number,\n variant: PublicImageVariant = \"detail\"\n) {\n // `variant` is reserved for future per-variant loading strategy\n // (e.g. eager above-the-fold for list thumbnails). Reference it\n // explicitly to satisfy `@typescript-eslint/no-unused-vars` while\n // keeping the public API stable.\n void variant;\n if (index === 0) {\n return {\n loading: \"eager\" as const,\n fetchPriority: \"high\" as const,\n };\n }\n\n return {\n loading: \"lazy\" as const,\n fetchPriority: \"auto\" as const,\n };\n}\n"],"mappings":";AAAA,IAAM,uBAAuB,CAAC,KAAK,KAAK,KAAK,IAAI;AACjD,IAAM,wBAAwB;AAC9B,IAAM,oBAAoB;AAE1B,IAAM,oBAAoB,CAAC,KAAK,KAAK,GAAG;AACxC,IAAM,qBAAqB;AAI3B,SAAS,aAAa,SAAkB;AACtC,MAAI,CAAC,WAAW,OAAO,MAAM,OAAO,GAAG;AACrC,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,IAAI,mBAAmB,KAAK,IAAI,uBAAuB,OAAO,CAAC;AAC7E;AAEA,SAAS,cAAc,KAAa;AAClC,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,0BAA0B;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,wBAAwB,KAAa;AACnD,QAAM,MAAM,cAAc,GAAG;AAC7B,MAAI,CAAC,IAAK,QAAO;AAEjB,SACE,IAAI,SAAS,WAAW,WAAW,KACnC,IAAI,SAAS,WAAW,oBAAoB;AAEhD;AAEO,SAAS,wBAAwB,KAAa;AACnD,QAAM,MAAM,cAAc,GAAG;AAC7B,MAAI,CAAC,IAAK,QAAO;AAEjB,MAAI,IAAI,SAAS,WAAW,WAAW,EAAG,QAAO;AACjD,MAAI,IAAI,SAAS,WAAW,oBAAoB,EAAG,QAAO;AAE1D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,SAAS,IAAI,QAAQ;AACzB;AAEA,SAAS,mBAAmB,KAAa,OAAe,SAAiB;AACvE,QAAM,MAAM,cAAc,GAAG;AAC7B,MAAI,CAAC,IAAK,QAAO;AAEjB,MAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AACvC,MAAI,aAAa,IAAI,KAAK,OAAO,OAAO,CAAC;AAEzC,SAAO,IAAI,WAAW,6BAClB,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM,GAAG,IAAI,IAAI,KACvC,IAAI,SAAS;AACnB;AAEA,SAAS,iBAAiB,SAA6B;AACrD,SAAO,YAAY,SAAS,oBAAoB;AAClD;AAEA,SAAS,kBAAkB,SAA6B;AACtD,SAAO,YAAY,SAAS,qBAAqB;AACnD;AAEO,SAAS,0BACd,KACA,OACA,SACA;AACA,MAAI,CAAC,wBAAwB,GAAG,GAAG;AACjC,WAAO,EAAE,KAAK,MAAM;AAAA,EACtB;AAEA,QAAM,UAA8B,SAAS,WAAW;AACxD,QAAM,kBAAkB,SAAS;AACjC,QAAM,UAAU;AAAA,IACd,mBAAmB,kBAAkB,OAAO;AAAA,EAC9C;AACA,QAAM,SAAS,iBAAiB,OAAO;AACvC,QAAM,MAAM,OAAO,OAAO,SAAS,CAAC,KAAK,qBAAqB,GAAG,EAAE,KAAK;AAExE,SAAO;AAAA,IACL,KAAK,mBAAmB,KAAK,KAAK,OAAO;AAAA,IACzC,QAAQ,OACL,IAAI,CAAC,UAAU,GAAG,mBAAmB,KAAK,OAAO,OAAO,CAAC,IAAI,KAAK,GAAG,EACrE,KAAK,IAAI;AAAA,IACZ;AAAA,EACF;AACF;AAEO,SAAS,qBACd,OACA,UAA8B,UAC9B;AAKA,OAAK;AACL,MAAI,UAAU,GAAG;AACf,WAAO;AAAA,MACL,SAAS;AAAA,MACT,eAAe;AAAA,IACjB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,eAAe;AAAA,EACjB;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/media/public-image.ts"],"sourcesContent":["const DEFAULT_IMAGE_WIDTHS = [320, 640, 960, 1200] as const;\nconst DEFAULT_IMAGE_QUALITY = 85;\nconst MIN_IMAGE_QUALITY = 40;\n\nconst LIST_IMAGE_WIDTHS = [320, 480, 640] as const;\nconst LIST_IMAGE_QUALITY = 70;\nconst RELATIVE_URL_BASE = \"http://localhost\";\n\nexport type PublicImageVariant = \"list\" | \"detail\";\n\nfunction clampQuality(quality?: number) {\n if (!quality || Number.isNaN(quality)) {\n return DEFAULT_IMAGE_QUALITY;\n }\n\n return Math.max(MIN_IMAGE_QUALITY, Math.min(DEFAULT_IMAGE_QUALITY, quality));\n}\n\nfunction parseImageUrl(src: string) {\n try {\n return new URL(src, RELATIVE_URL_BASE);\n } catch {\n return null;\n }\n}\n\nexport function isOptimizableCoverImage(src: string) {\n const url = parseImageUrl(src);\n if (!url) return false;\n\n return (\n url.pathname.startsWith(\"/api/cdn/\") ||\n url.pathname.startsWith(\"/api/notion/media/\")\n );\n}\n\nexport function isPublicImageUrlAllowed(src: string) {\n const url = parseImageUrl(src);\n if (!url) return false;\n\n if (url.pathname.startsWith(\"/api/cdn/\")) return true;\n if (url.pathname.startsWith(\"/api/notion/media/\")) return true;\n\n return [\n \"www.notion.so\",\n \"notion.so\",\n \"secure.notion-static.com\",\n \"prod-files-secure.s3.us-west-2.amazonaws.com\",\n ].includes(url.hostname);\n}\n\nfunction buildSizedImageUrl(src: string, width: number, quality: number) {\n const url = parseImageUrl(src);\n if (!url) return src;\n\n url.searchParams.set(\"w\", String(width));\n url.searchParams.set(\"q\", String(quality));\n\n return url.origin === RELATIVE_URL_BASE\n ? `${url.pathname}${url.search}${url.hash}`\n : url.toString();\n}\n\nfunction widthsForVariant(variant: PublicImageVariant) {\n return variant === \"list\" ? LIST_IMAGE_WIDTHS : DEFAULT_IMAGE_WIDTHS;\n}\n\nfunction qualityForVariant(variant: PublicImageVariant) {\n return variant === \"list\" ? LIST_IMAGE_QUALITY : DEFAULT_IMAGE_QUALITY;\n}\n\nexport function buildResponsiveImageAttrs(\n src: string,\n sizes: string,\n options?: { quality?: number; variant?: PublicImageVariant }\n) {\n if (!isOptimizableCoverImage(src)) {\n return { src, sizes };\n }\n\n const variant: PublicImageVariant = options?.variant ?? \"detail\";\n const explicitQuality = options?.quality;\n const quality = clampQuality(\n explicitQuality ?? qualityForVariant(variant)\n );\n const widths = widthsForVariant(variant);\n const max = widths[widths.length - 1] ?? DEFAULT_IMAGE_WIDTHS.at(-1) ?? 1200;\n\n return {\n src: buildSizedImageUrl(src, max, quality),\n srcSet: widths\n .map((width) => `${buildSizedImageUrl(src, width, quality)} ${width}w`)\n .join(\", \"),\n sizes,\n };\n}\n\nexport function getCoverImageLoading(\n index: number,\n variant: PublicImageVariant = \"detail\"\n) {\n // `variant` is reserved for future per-variant loading strategy\n // (e.g. eager above-the-fold for list thumbnails). Reference it\n // explicitly to satisfy `@typescript-eslint/no-unused-vars` while\n // keeping the public API stable.\n void variant;\n if (index === 0) {\n return {\n loading: \"eager\" as const,\n fetchPriority: \"high\" as const,\n };\n }\n\n return {\n loading: \"lazy\" as const,\n fetchPriority: \"auto\" as const,\n };\n}\n"],"mappings":";AAAA,IAAM,uBAAuB,CAAC,KAAK,KAAK,KAAK,IAAI;AACjD,IAAM,wBAAwB;AAC9B,IAAM,oBAAoB;AAE1B,IAAM,oBAAoB,CAAC,KAAK,KAAK,GAAG;AACxC,IAAM,qBAAqB;AAC3B,IAAM,oBAAoB;AAI1B,SAAS,aAAa,SAAkB;AACtC,MAAI,CAAC,WAAW,OAAO,MAAM,OAAO,GAAG;AACrC,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,IAAI,mBAAmB,KAAK,IAAI,uBAAuB,OAAO,CAAC;AAC7E;AAEA,SAAS,cAAc,KAAa;AAClC,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,iBAAiB;AAAA,EACvC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,wBAAwB,KAAa;AACnD,QAAM,MAAM,cAAc,GAAG;AAC7B,MAAI,CAAC,IAAK,QAAO;AAEjB,SACE,IAAI,SAAS,WAAW,WAAW,KACnC,IAAI,SAAS,WAAW,oBAAoB;AAEhD;AAEO,SAAS,wBAAwB,KAAa;AACnD,QAAM,MAAM,cAAc,GAAG;AAC7B,MAAI,CAAC,IAAK,QAAO;AAEjB,MAAI,IAAI,SAAS,WAAW,WAAW,EAAG,QAAO;AACjD,MAAI,IAAI,SAAS,WAAW,oBAAoB,EAAG,QAAO;AAE1D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,SAAS,IAAI,QAAQ;AACzB;AAEA,SAAS,mBAAmB,KAAa,OAAe,SAAiB;AACvE,QAAM,MAAM,cAAc,GAAG;AAC7B,MAAI,CAAC,IAAK,QAAO;AAEjB,MAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AACvC,MAAI,aAAa,IAAI,KAAK,OAAO,OAAO,CAAC;AAEzC,SAAO,IAAI,WAAW,oBAClB,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM,GAAG,IAAI,IAAI,KACvC,IAAI,SAAS;AACnB;AAEA,SAAS,iBAAiB,SAA6B;AACrD,SAAO,YAAY,SAAS,oBAAoB;AAClD;AAEA,SAAS,kBAAkB,SAA6B;AACtD,SAAO,YAAY,SAAS,qBAAqB;AACnD;AAEO,SAAS,0BACd,KACA,OACA,SACA;AACA,MAAI,CAAC,wBAAwB,GAAG,GAAG;AACjC,WAAO,EAAE,KAAK,MAAM;AAAA,EACtB;AAEA,QAAM,UAA8B,SAAS,WAAW;AACxD,QAAM,kBAAkB,SAAS;AACjC,QAAM,UAAU;AAAA,IACd,mBAAmB,kBAAkB,OAAO;AAAA,EAC9C;AACA,QAAM,SAAS,iBAAiB,OAAO;AACvC,QAAM,MAAM,OAAO,OAAO,SAAS,CAAC,KAAK,qBAAqB,GAAG,EAAE,KAAK;AAExE,SAAO;AAAA,IACL,KAAK,mBAAmB,KAAK,KAAK,OAAO;AAAA,IACzC,QAAQ,OACL,IAAI,CAAC,UAAU,GAAG,mBAAmB,KAAK,OAAO,OAAO,CAAC,IAAI,KAAK,GAAG,EACrE,KAAK,IAAI;AAAA,IACZ;AAAA,EACF;AACF;AAEO,SAAS,qBACd,OACA,UAA8B,UAC9B;AAKA,OAAK;AACL,MAAI,UAAU,GAAG;AACf,WAAO;AAAA,MACL,SAAS;AAAA,MACT,eAAe;AAAA,IACjB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,eAAe;AAAA,EACjB;AACF;","names":[]}
@@ -59,7 +59,6 @@ function readProcessEnv() {
59
59
  const env2 = {
60
60
  NOTION_TOKEN: process.env.NOTION_TOKEN,
61
61
  NOTION_DATA_SOURCE_ID: process.env.NOTION_DATA_SOURCE_ID,
62
- NOTION_MOVIES_DATA_SOURCE_ID: process.env.NOTION_MOVIES_DATA_SOURCE_ID,
63
62
  NOTION_API_BASE_URL: process.env.NOTION_API_BASE_URL,
64
63
  NOTION_EDIT_BASE_URL: process.env.NOTION_EDIT_BASE_URL,
65
64
  NOTION_WEBHOOK_VERIFICATION_TOKEN: process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/media/routes/notion-media.ts","../../../src/cache/cache-keys.ts","../../../src/notion/client.ts","../../../src/notion/config.ts","../../../src/notion/media.ts","../../../src/util/env.ts","../../../src/platform/runtime.ts","../../../src/platform/cloudflare-runtime.ts","../../../src/platform/current.ts"],"sourcesContent":["// media/routes/notion-media.ts\n//\n// GET /api/notion/media/[...ref] - Notion-hosted media proxy with\n// edge cache, R2 fanout, and image transformation.\n//\n// Originally lives at `apps/moviebluebook/app/api/notion/media/[...ref]/route.ts`.\n// The package exports a route object that exposes both a Next.js\n// handler (the `GET` field) and a worker-friendly `handle` function.\n//\n// The `vinext/shims/request-context` module is treated as a runtime\n// shim by the starter and the Cloudflare Workers runtime; it is\n// listed in the package's tsup `external` config so it is resolved by\n// the consumer at runtime.\n\nimport { NextResponse } from \"next/server\";\nimport { getRequestExecutionContext } from \"vinext/shims/request-context\";\nimport {\n notionMediaR2KeyForUrl,\n publicMediaCacheKeyForUrl,\n publicMediaVariantForAccept,\n type PublicMediaVariant,\n} from \"../../cache\";\nimport { createNotionClient } from \"../../notion/client\";\nimport { getNotionClientConfig } from \"../../notion/config\";\nimport {\n fileObjectForMediaBlock,\n normalizeNotionFileSource,\n pickFirstFilesPropertyValue,\n} from \"../../notion/media\";\nimport type { NotionBlock, NotionPageLike } from \"../../notion/types\";\nimport {\n getPublicCache,\n getRuntimePlatform,\n} 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;\nconst CACHEABLE_STATUS = new Set([200]);\n\ntype Props = {\n params: Promise<{ ref: string[] }>;\n};\n\nexport const notionMediaRoute = {\n async GET(_request: Request, props: Props) {\n const { ref } = await props.params;\n if (ref.some((part) => part === \"..\" || part.includes(\"/\"))) {\n return badRequest();\n }\n const url = new URL(_request.url);\n url.pathname = buildNotionMediaPath(ref);\n return notionMediaRoute.handle(new Request(url.toString(), _request));\n },\n async handle(request: Request): Promise<Response> {\n const variant = publicMediaVariantForAccept(\n request.headers.get(\"accept\") ?? \"\"\n );\n return withEdgeMediaCache(request, variant, () => loadMedia(request));\n },\n};\n\nfunction buildNotionMediaPath(ref: string[]) {\n return `/api/notion/media/${ref.map(encodeURIComponent).join(\"/\")}`;\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)) return fallback;\n return Math.max(min, Math.min(max, parsed));\n}\n\nfunction cacheControl(request: Request) {\n const url = new URL(request.url);\n if (url.searchParams.has(\"v\")) {\n return \"public, max-age=31536000, immutable\";\n }\n\n return \"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400\";\n}\n\nfunction canUseMediaCache(request: Request) {\n if (request.method !== \"GET\") return false;\n return !request.headers.has(\"range\");\n}\n\nfunction mediaCacheHeaders(\n response: Response,\n request: Request,\n state: \"HIT\" | \"MISS\" | \"BYPASS\",\n r2State?: \"HIT\" | \"MISS\" | \"BYPASS\"\n) {\n const headers = new Headers(response.headers);\n headers.set(\"Cache-Control\", cacheControl(request));\n headers.set(\"X-Notion-Media-Cache\", state);\n if (r2State) headers.set(\"X-Notion-Media-R2\", r2State);\n return headers;\n}\n\nasync function responseFromR2Cache(\n request: Request,\n variant: PublicMediaVariant\n) {\n const url = new URL(request.url);\n const r2Key = notionMediaR2KeyForUrl(url, variant);\n const storage = getRuntimePlatform().objectStorage;\n if (!r2Key || !storage) return null;\n\n const object = await storage.get(r2Key);\n if (!object?.body) return null;\n\n const contentType =\n object.contentType ?? (variant === \"avif\" ? \"image/avif\" : \"image/webp\");\n const headers = new Headers();\n headers.set(\"Content-Type\", contentType);\n headers.set(\"Cache-Control\", cacheControl(request));\n headers.set(\"Vary\", \"Accept\");\n headers.set(\"X-Notion-Media-Branch\", \"r2\");\n headers.set(\"X-Notion-Media-R2\", \"HIT\");\n if (object.etag) headers.set(\"ETag\", object.etag);\n\n return new Response(object.body, { headers });\n}\n\nasync function withEdgeMediaCache(\n request: Request,\n variant: PublicMediaVariant,\n load: () => Promise<Response>\n) {\n if (!canUseMediaCache(request)) {\n const response = await load();\n return new Response(response.body, {\n status: response.status,\n headers: mediaCacheHeaders(response, request, \"BYPASS\"),\n });\n }\n\n const url = new URL(request.url);\n const cache = getPublicCache();\n const cacheKey = publicMediaCacheKeyForUrl(url, variant);\n const cached = await cache.match(cacheKey);\n if (cached) {\n return new Response(cached.body, {\n status: cached.status,\n headers: mediaCacheHeaders(cached, request, \"HIT\"),\n });\n }\n\n const r2Response = await responseFromR2Cache(request, variant);\n const response = r2Response ?? (await load());\n const headers = mediaCacheHeaders(response, request, \"MISS\");\n const output = new Response(response.body, {\n status: response.status,\n headers,\n });\n\n if (CACHEABLE_STATUS.has(response.status)) {\n const toCache = output.clone();\n getRequestExecutionContext()?.waitUntil(cache.put(cacheKey, toCache));\n }\n\n return output;\n}\n\nfunction mediaRedirect(url: string) {\n const response = NextResponse.redirect(url, 302);\n response.headers.set(\n \"Cache-Control\",\n \"public, max-age=300, s-maxage=300, stale-while-revalidate=300\"\n );\n return response;\n}\n\nfunction notFound() {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n}\n\nfunction badRequest() {\n return NextResponse.json({ error: \"Invalid media ref\" }, { status: 400 });\n}\n\nfunction forbidden() {\n return NextResponse.json({ error: \"Forbidden\" }, { status: 403 });\n}\n\nasync function serveFileObject(\n input: unknown,\n request: Request,\n options?: { redirectNotionHosted?: boolean }\n) {\n const source = normalizeNotionFileSource(input);\n if (!source) return notFound();\n if (options?.redirectNotionHosted) return mediaRedirect(source.url);\n return proxyNotionHostedFile(source.url, request);\n}\n\nasync function proxyNotionHostedFile(url: string, request: Request) {\n const range = request.headers.get(\"range\");\n const ifRange = request.headers.get(\"if-range\");\n const upstreamHeaders = new Headers({\n Accept: request.headers.get(\"accept\") ?? \"*/*\",\n });\n if (range) upstreamHeaders.set(\"Range\", range);\n if (ifRange) upstreamHeaders.set(\"If-Range\", ifRange);\n\n const upstream = await fetch(url, {\n headers: upstreamHeaders,\n });\n if ((!upstream.ok && upstream.status !== 416) || !upstream.body) {\n return NextResponse.json(\n { error: \"Unable to fetch Notion media\" },\n { status: upstream.status || 502 }\n );\n }\n\n const contentType = upstream.headers.get(\"content-type\") ?? \"\";\n const isImage = contentType.startsWith(\"image/\");\n const accept = request.headers.get(\"accept\") ?? \"\";\n const variant = publicMediaVariantForAccept(accept);\n const urlObj = new URL(request.url);\n const width = clampInt(\n urlObj.searchParams.get(\"w\"),\n 64,\n MAX_WIDTH,\n DEFAULT_WIDTH\n );\n const quality = clampInt(\n urlObj.searchParams.get(\"q\"),\n MIN_QUALITY,\n MAX_QUALITY,\n DEFAULT_QUALITY\n );\n\n let outputFormat: \"image/avif\" | \"image/webp\" | null = null;\n if (variant === \"avif\") {\n outputFormat = \"image/avif\";\n } else if (variant === \"webp\") {\n outputFormat = \"image/webp\";\n }\n\n const platform = getRuntimePlatform();\n const imageTransformer = platform.imageTransformer;\n if (isImage && !range && outputFormat && imageTransformer) {\n const r2Key = notionMediaR2KeyForUrl(urlObj, variant);\n\n try {\n const result = await imageTransformer.transform(upstream.body, {\n width,\n format: outputFormat,\n quality,\n });\n const transformed = result.response();\n const headers = new Headers(transformed.headers);\n headers.set(\"Content-Type\", result.contentType);\n headers.set(\"Cache-Control\", cacheControl(request));\n headers.set(\"Vary\", \"Accept\");\n headers.set(\"X-Notion-Media-Branch\", \"transformed\");\n headers.set(\"X-Notion-Media-R2\", r2Key ? \"MISS\" : \"BYPASS\");\n headers.set(\"X-Optimized-Width\", String(width));\n headers.set(\"X-Optimized-Quality\", String(quality));\n\n if (transformed.body && r2Key && platform.objectStorage) {\n const [clientBody, r2Body] = transformed.body.tee();\n getRequestExecutionContext()?.waitUntil(\n platform.objectStorage.put(r2Key, r2Body, {\n contentType: result.contentType,\n cacheControl: \"public, max-age=31536000, immutable\",\n metadata: {\n source: \"notion\",\n cachedAt: new Date().toISOString(),\n width: String(width),\n quality: String(quality),\n },\n })\n );\n\n return new Response(clientBody, { headers });\n }\n\n return new Response(transformed.body, { headers });\n } catch {\n // Fall through to the original file when the image binding cannot transform.\n }\n }\n\n const headers = new Headers();\n for (const header of [\n \"accept-ranges\",\n \"content-disposition\",\n \"content-encoding\",\n \"content-length\",\n \"content-range\",\n \"content-type\",\n \"etag\",\n \"last-modified\",\n ]) {\n const value = upstream.headers.get(header);\n if (value) headers.set(header, value);\n }\n if (contentType && !headers.has(\"Content-Type\")) {\n headers.set(\"Content-Type\", contentType);\n }\n headers.set(\"Cache-Control\", cacheControl(request));\n headers.set(\"X-Notion-Media-Branch\", \"proxied\");\n return new Response(upstream.body, { status: upstream.status, headers });\n}\n\nasync function loadMedia(request: Request) {\n const url = new URL(request.url);\n const ref = readRefFromPathname(url.pathname);\n if (!ref) return badRequest();\n const client = createNotionClient(await getNotionClientConfig());\n\n if (ref[0] === \"page\" && ref[1] && ref[2] === \"cover\") {\n const page = (await client.pages.retrieve({\n page_id: ref[1],\n })) as NotionPageLike;\n return serveFileObject(page.cover, request);\n }\n\n if (ref[0] === \"page\" && ref[1] && ref[2] === \"property\" && ref[3]) {\n const page = (await client.pages.retrieve({\n page_id: ref[1],\n })) as NotionPageLike;\n const propertyName = decodeURIComponent(ref.slice(3).join(\"/\"));\n return serveFileObject(\n pickFirstFilesPropertyValue(page.properties?.[propertyName]),\n request\n );\n }\n\n if (ref[0] === \"block\" && ref[1]) {\n const block = (await client.blocks.retrieve({\n block_id: ref[1],\n })) as NotionBlock;\n if (block.type === \"video\") {\n return forbidden();\n }\n return serveFileObject(fileObjectForMediaBlock(block), request, {\n redirectNotionHosted:\n block.type === \"audio\" ||\n block.type === \"pdf\" ||\n block.type === \"file\",\n });\n }\n\n return badRequest();\n}\n\nfunction readRefFromPathname(pathname: string): string[] | null {\n const prefix = \"/api/notion/media/\";\n if (!pathname.startsWith(prefix)) return null;\n const encoded = pathname.slice(prefix.length);\n if (!encoded) return null;\n return encoded.split(\"/\").map((part) => decodeURIComponent(part));\n}\n\n// Top-level alias for callers that prefer a flat signature (e.g. the\n// Next.js `app/api/.../route.ts` delegates).\nexport const GET = notionMediaRoute.GET;\n\n/**\n * Worker-friendly single-arg handler. Used by the Cloudflare Workers\n * bootstrap in `@notionx/core/worker`. Equivalent to\n * `notionMediaRoute.handle`.\n */\nexport async function notionMediaRouteHandle(\n request: Request\n): Promise<Response> {\n return notionMediaRoute.handle(request);\n}\n","// Edge cache keys for transformed Notion media responses.\n//\n// Page and API route caching is handled by vinext's CDN cache adapter\n// (vite.config.ts). These helpers remain for the media proxy route.\n\nconst CACHE_ORIGIN = \"https://cache.local\";\nconst CACHE_NAMESPACE = \"/__public-cache/v20260609a\";\nconst NOTION_MEDIA_R2_PREFIX = \"notion-media/v1\";\n\nexport type PublicMediaVariant = \"avif\" | \"webp\" | \"source\";\n\nfunction normalizePath(pathname: string) {\n if (pathname === \"/\") return \"/\";\n return pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n}\n\nexport function publicMediaVariantForAccept(accept: string): PublicMediaVariant {\n if (accept.includes(\"image/avif\")) return \"avif\";\n if (accept.includes(\"image/webp\")) return \"webp\";\n return \"source\";\n}\n\nexport function publicMediaCacheKeyForUrl(\n input: URL,\n variant: PublicMediaVariant\n) {\n const url = new URL(\n `${CACHE_NAMESPACE}${normalizePath(input.pathname)}${input.search}`,\n CACHE_ORIGIN\n );\n url.searchParams.set(\"__variant\", variant);\n url.searchParams.sort();\n return url.toString();\n}\n\nfunction keySegment(value: string) {\n return encodeURIComponent(value || \"none\");\n}\n\nexport function notionMediaR2KeyForUrl(\n input: URL,\n variant: PublicMediaVariant\n) {\n if (variant === \"source\") return null;\n\n const version = input.searchParams.get(\"v\");\n if (!version) return null;\n\n const path = normalizePath(input.pathname)\n .split(\"/\")\n .filter(Boolean)\n .map(keySegment)\n .join(\"/\");\n const width = input.searchParams.get(\"w\") ?? \"source\";\n const quality = input.searchParams.get(\"q\") ?? \"source\";\n\n return [\n NOTION_MEDIA_R2_PREFIX,\n variant,\n path,\n `v-${keySegment(version)}`,\n `w-${keySegment(width)}`,\n `q-${keySegment(quality)}.${variant}`,\n ].join(\"/\");\n}\n","import { Client } from \"@notionhq/client\";\nimport type { NotionClientConfig } from \"./config\";\n\nexport function createNotionClient(config: NotionClientConfig) {\n return new Client({\n auth: config.token,\n baseUrl: config.apiBaseUrl,\n notionVersion: \"2026-03-11\",\n });\n}\n","import type { NotionContentModelLike } from \"./types\";\n\ntype NotionEnv = {\n NOTION_TOKEN?: string;\n NOTION_DATA_SOURCE_ID?: string;\n NOTION_MOVIES_DATA_SOURCE_ID?: string;\n NOTION_API_BASE_URL?: string;\n NOTION_EDIT_BASE_URL?: string;\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n [key: string]: string | undefined;\n};\n\nexport const DEFAULT_NOTION_MOVIES_DATA_SOURCE_ID =\n \"371dc62d-0738-8015-a601-000bc3944fcb\";\n\nexport type NotionClientConfig = {\n token: string;\n apiBaseUrl?: string;\n};\n\nexport type NotionConfig = {\n token: string;\n dataSourceId: string;\n apiBaseUrl?: string;\n editBaseUrl?: string;\n webhookVerificationToken?: string;\n};\n\nfunction readProcessEnv(): NotionEnv {\n const env: NotionEnv = {\n NOTION_TOKEN: process.env.NOTION_TOKEN,\n NOTION_DATA_SOURCE_ID: process.env.NOTION_DATA_SOURCE_ID,\n NOTION_MOVIES_DATA_SOURCE_ID: process.env.NOTION_MOVIES_DATA_SOURCE_ID,\n NOTION_API_BASE_URL: process.env.NOTION_API_BASE_URL,\n NOTION_EDIT_BASE_URL: process.env.NOTION_EDIT_BASE_URL,\n NOTION_WEBHOOK_VERIFICATION_TOKEN:\n process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN,\n };\n\n for (const [key, value] of Object.entries(process.env)) {\n if (key.startsWith(\"NOTION_\") && typeof value === \"string\") {\n env[key] = value;\n }\n }\n\n return env;\n}\n\nasync function readWorkerEnv(): Promise<NotionEnv> {\n try {\n const mod = (await import(\n /* webpackIgnore: true */ \"cloudflare:workers\"\n )) as unknown as { env?: Record<string, unknown> };\n const env: NotionEnv = {};\n for (const [key, value] of Object.entries(mod.env ?? {})) {\n if (key.startsWith(\"NOTION_\") && typeof value === \"string\") {\n env[key] = value;\n }\n }\n return env;\n } catch {\n return {};\n }\n}\n\nfunction readString(source: NotionEnv, name: string): string | undefined {\n const value = String(source[name] ?? \"\").trim();\n return value || undefined;\n}\n\nfunction mergeEnv(...sources: NotionEnv[]): NotionEnv {\n const merged: NotionEnv = {};\n\n for (const source of sources) {\n for (const name of Object.keys(source)) {\n if (!name.startsWith(\"NOTION_\")) continue;\n const value = readString(source, name);\n if (value) merged[name] = value;\n }\n }\n\n return merged;\n}\n\nasync function readEnv(): Promise<NotionEnv> {\n const processEnv = readProcessEnv();\n return mergeEnv(await readWorkerEnv(), processEnv);\n}\n\nfunction readRequired(\n source: NotionEnv,\n name: string\n): string {\n const value = readString(source, name);\n if (!value) {\n throw new Error(`Missing required Notion env: ${name}`);\n }\n return value;\n}\n\nexport function getNotionEditBaseUrl(): string {\n return readString(readProcessEnv(), \"NOTION_EDIT_BASE_URL\") ?? \"https://www.notion.so\";\n}\n\nexport async function hasNotionConfig(): Promise<boolean> {\n const env = await readEnv();\n return Boolean(\n readString(env, \"NOTION_TOKEN\") && readString(env, \"NOTION_DATA_SOURCE_ID\")\n );\n}\n\nexport async function hasNotionMovieConfig(): Promise<boolean> {\n const env = await readEnv();\n return Boolean(readString(env, \"NOTION_TOKEN\"));\n}\n\nexport async function hasNotionModelConfig(\n model: NotionContentModelLike\n): Promise<boolean> {\n const env = await readEnv();\n return Boolean(\n readString(env, \"NOTION_TOKEN\") &&\n (readString(env, model.source.dataSourceEnv) ||\n model.source.defaultDataSourceId)\n );\n}\n\nexport async function getNotionClientConfig(): Promise<NotionClientConfig> {\n const env = await readEnv();\n return {\n token: readRequired(env, \"NOTION_TOKEN\"),\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n };\n}\n\nexport async function getNotionConfig(): Promise<NotionConfig> {\n const env = await readEnv();\n return {\n token: readRequired(env, \"NOTION_TOKEN\"),\n dataSourceId: readRequired(env, \"NOTION_DATA_SOURCE_ID\"),\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n editBaseUrl: readString(env, \"NOTION_EDIT_BASE_URL\"),\n webhookVerificationToken: readString(\n env,\n \"NOTION_WEBHOOK_VERIFICATION_TOKEN\"\n ),\n };\n}\n\nexport async function getNotionWebhookVerificationToken(): Promise<\n string | undefined\n> {\n const env = await readEnv();\n return readString(env, \"NOTION_WEBHOOK_VERIFICATION_TOKEN\");\n}\n\nexport async function getNotionMovieConfig(): Promise<NotionConfig> {\n const env = await readEnv();\n return {\n token: readRequired(env, \"NOTION_TOKEN\"),\n dataSourceId:\n readString(env, \"NOTION_MOVIES_DATA_SOURCE_ID\") ??\n DEFAULT_NOTION_MOVIES_DATA_SOURCE_ID,\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n editBaseUrl: readString(env, \"NOTION_EDIT_BASE_URL\"),\n webhookVerificationToken: readString(\n env,\n \"NOTION_WEBHOOK_VERIFICATION_TOKEN\"\n ),\n };\n}\n\nexport async function getNotionConfigForModel(\n model: NotionContentModelLike\n): Promise<NotionConfig> {\n const env = await readEnv();\n const dataSourceId =\n readString(env, model.source.dataSourceEnv) ??\n model.source.defaultDataSourceId;\n if (!dataSourceId) {\n throw new Error(`Missing required Notion env: ${model.source.dataSourceEnv}`);\n }\n\n return {\n token: readRequired(env, model.source.tokenEnv),\n dataSourceId,\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n editBaseUrl: readString(env, \"NOTION_EDIT_BASE_URL\"),\n webhookVerificationToken: readString(\n env,\n \"NOTION_WEBHOOK_VERIFICATION_TOKEN\"\n ),\n };\n}\n","import type { NotionBlock, NotionFileSource, NotionPageLike } from \"./types\";\n\ntype FileLike = {\n type?: string;\n external?: { url?: string };\n file?: { url?: string; expiry_time?: string };\n name?: string;\n};\n\nfunction stripLeadingSlash(value: string) {\n return value.startsWith(\"/\") ? value.slice(1) : value;\n}\n\nfunction encodePathPart(value: string) {\n return encodeURIComponent(stripLeadingSlash(value));\n}\n\nfunction appendVersion(path: string, version?: string) {\n const value = String(version ?? \"\").trim();\n if (!value) return path;\n return `${path}?${new URLSearchParams({ v: value })}`;\n}\n\nfunction blockVersion(block: NotionBlock): string | undefined {\n return typeof block.last_edited_time === \"string\"\n ? block.last_edited_time\n : undefined;\n}\n\nexport function notionPageCoverMediaPath(pageId: string): string {\n return `/api/notion/media/page/${encodePathPart(pageId)}/cover`;\n}\n\nexport function notionPagePropertyMediaPath(\n pageId: string,\n propertyName: string\n): string {\n return `/api/notion/media/page/${encodePathPart(pageId)}/property/${encodePathPart(propertyName)}`;\n}\n\nexport function notionBlockMediaPath(blockId: string): string {\n return `/api/notion/media/block/${encodePathPart(blockId)}`;\n}\n\nexport function normalizeNotionFileSource(input: unknown): NotionFileSource | null {\n const file = input as FileLike | null | undefined;\n if (!file || typeof file !== \"object\") return null;\n\n if (file.type === \"external\") {\n const url = String(file.external?.url ?? \"\").trim();\n return url ? { type: \"external\", url } : null;\n }\n\n if (file.type === \"file\") {\n const url = String(file.file?.url ?? \"\").trim();\n if (!url) return null;\n return {\n type: \"file\",\n url,\n expiryTime: String(file.file?.expiry_time ?? \"\").trim() || null,\n };\n }\n\n return null;\n}\n\nexport function resolveNotionFileUrl(input: unknown): string | null {\n return normalizeNotionFileSource(input)?.url ?? null;\n}\n\nexport function isNotionHostedFile(input: unknown): boolean {\n return normalizeNotionFileSource(input)?.type === \"file\";\n}\n\nexport function pickFirstFilesPropertyValue(property: unknown): unknown | null {\n const value = property as { type?: string; files?: unknown[] } | null | undefined;\n if (!value || value.type !== \"files\" || !Array.isArray(value.files)) {\n return null;\n }\n return value.files[0] ?? null;\n}\n\nexport function pickPageCoverFile(page: NotionPageLike): unknown | null {\n return page.cover ?? null;\n}\n\nexport function coverImageUrlForPage(\n page: NotionPageLike,\n coverPropertyName = \"Cover\"\n): string | null {\n const propertyFile = pickFirstFilesPropertyValue(\n page.properties?.[coverPropertyName]\n );\n const propertySource = normalizeNotionFileSource(propertyFile);\n if (propertySource) {\n return appendVersion(\n notionPagePropertyMediaPath(page.id, coverPropertyName),\n page.last_edited_time\n );\n }\n\n const coverSource = normalizeNotionFileSource(pickPageCoverFile(page));\n if (coverSource) {\n return appendVersion(notionPageCoverMediaPath(page.id), page.last_edited_time);\n }\n\n return null;\n}\n\nexport function fileObjectForMediaBlock(block: NotionBlock): unknown | null {\n const typed = block[block.type] as Record<string, unknown> | undefined;\n if (!typed || typeof typed !== \"object\") return null;\n\n if (\n block.type === \"image\" ||\n block.type === \"video\" ||\n block.type === \"file\" ||\n block.type === \"pdf\" ||\n block.type === \"audio\"\n ) {\n return typed;\n }\n\n return null;\n}\n\nexport function mediaUrlForBlock(block: NotionBlock): string | null {\n const source = normalizeNotionFileSource(fileObjectForMediaBlock(block));\n if (!source) return null;\n if (source.type === \"external\" && block.type !== \"image\") {\n return source.url;\n }\n return appendVersion(notionBlockMediaPath(block.id), blockVersion(block));\n}\n\nexport function firstImageUrlFromBlocks(blocks: NotionBlock[]): string | null {\n for (const block of blocks) {\n if (block.type === \"image\") {\n const imageUrl = mediaUrlForBlock(block);\n if (imageUrl) return imageUrl;\n }\n\n const nested = block.children?.length\n ? firstImageUrlFromBlocks(block.children)\n : null;\n if (nested) return nested;\n }\n\n return null;\n}\n\nexport function publicMediaBlockForApi(block: NotionBlock): NotionBlock {\n const value = block[block.type];\n const source = normalizeNotionFileSource(value);\n const children = block.children?.map(publicMediaBlockForApi);\n\n if (!source) {\n return children ? { ...block, children } : { ...block };\n }\n\n const path = appendVersion(notionBlockMediaPath(block.id), blockVersion(block));\n const publicValue =\n source.type === \"external\"\n ? block.type === \"image\"\n ? {\n ...(value as Record<string, unknown>),\n external: { url: path },\n }\n : value\n : {\n ...(value as Record<string, unknown>),\n file: {\n url: path,\n expiry_time: null,\n },\n };\n\n return {\n ...block,\n [block.type]: publicValue,\n ...(children ? { children } : {}),\n };\n}\n\nexport function gatedMediaBlockForApi(\n block: NotionBlock,\n options?: { movieId?: string }\n): NotionBlock {\n const value = block[block.type];\n const source = normalizeNotionFileSource(value);\n const children = block.children?.map((child) =>\n gatedMediaBlockForApi(child, options)\n );\n\n if (block.type !== \"video\" || !source) {\n const publicBlock = publicMediaBlockForApi(block);\n return children ? { ...publicBlock, children } : publicBlock;\n }\n\n const gatedValue: Record<string, unknown> = {\n ...(value as Record<string, unknown>),\n gated: true,\n access_url: options?.movieId\n ? `/api/movies/${encodePathPart(options.movieId)}/video/${encodePathPart(block.id)}`\n : null,\n };\n\n if (source.type === \"external\") {\n gatedValue.external = { url: null };\n } else {\n gatedValue.file = {\n url: null,\n expiry_time: null,\n };\n }\n\n return {\n ...block,\n video: gatedValue,\n ...(children ? { children } : {}),\n };\n}\n\nexport function isDirectVideoUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return /\\.(mp4|webm|mov|m4v)(?:$|\\?)/i.test(parsed.pathname);\n } catch {\n return false;\n }\n}\n\nexport function videoEmbedUrl(url: string): string | null {\n try {\n const parsed = new URL(url);\n const host = parsed.hostname.replace(/^www\\./, \"\");\n\n if (host === \"youtube.com\" || host === \"m.youtube.com\") {\n const id = parsed.searchParams.get(\"v\");\n return id ? `https://www.youtube.com/embed/${encodeURIComponent(id)}` : null;\n }\n\n if (host === \"youtu.be\") {\n const id = parsed.pathname.split(\"/\").filter(Boolean)[0];\n return id ? `https://www.youtube.com/embed/${encodeURIComponent(id)}` : null;\n }\n\n if (host === \"youtube-nocookie.com\") {\n return parsed.toString();\n }\n\n if (host === \"vimeo.com\") {\n const id = parsed.pathname.split(\"/\").filter(Boolean)[0];\n return id ? `https://player.vimeo.com/video/${encodeURIComponent(id)}` : null;\n }\n\n if (host === \"player.vimeo.com\") {\n return parsed.toString();\n }\n } catch {\n return null;\n }\n\n return null;\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 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"],"mappings":"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","names":["env","env","env","options","getRuntimePlatform","getPublicCache","getRuntimePlatform","response","getPublicCache","headers"]}
1
+ {"version":3,"sources":["../../../src/media/routes/notion-media.ts","../../../src/cache/cache-keys.ts","../../../src/notion/client.ts","../../../src/notion/config.ts","../../../src/notion/media.ts","../../../src/util/env.ts","../../../src/platform/runtime.ts","../../../src/platform/cloudflare-runtime.ts","../../../src/platform/current.ts"],"sourcesContent":["// media/routes/notion-media.ts\n//\n// GET /api/notion/media/[...ref] - Notion-hosted media proxy with\n// edge cache, R2 fanout, and image transformation.\n//\n// The package exports a route object that exposes both a Next.js\n// handler (the `GET` field) and a worker-friendly `handle` function.\n//\n// The `vinext/shims/request-context` module is treated as a runtime\n// shim by the starter and the Cloudflare Workers runtime; it is\n// listed in the package's tsup `external` config so it is resolved by\n// the consumer at runtime.\n\nimport { NextResponse } from \"next/server\";\nimport { getRequestExecutionContext } from \"vinext/shims/request-context\";\nimport {\n notionMediaR2KeyForUrl,\n publicMediaCacheKeyForUrl,\n publicMediaVariantForAccept,\n type PublicMediaVariant,\n} from \"../../cache\";\nimport { createNotionClient } from \"../../notion/client\";\nimport { getNotionClientConfig } from \"../../notion/config\";\nimport {\n fileObjectForMediaBlock,\n normalizeNotionFileSource,\n pickFirstFilesPropertyValue,\n} from \"../../notion/media\";\nimport type { NotionBlock, NotionPageLike } from \"../../notion/types\";\nimport {\n getPublicCache,\n getRuntimePlatform,\n} 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;\nconst CACHEABLE_STATUS = new Set([200]);\n\ntype Props = {\n params: Promise<{ ref: string[] }>;\n};\n\nexport const notionMediaRoute = {\n async GET(_request: Request, props: Props) {\n const { ref } = await props.params;\n if (ref.some((part) => part === \"..\" || part.includes(\"/\"))) {\n return badRequest();\n }\n const url = new URL(_request.url);\n url.pathname = buildNotionMediaPath(ref);\n return notionMediaRoute.handle(new Request(url.toString(), _request));\n },\n async handle(request: Request): Promise<Response> {\n const variant = publicMediaVariantForAccept(\n request.headers.get(\"accept\") ?? \"\"\n );\n return withEdgeMediaCache(request, variant, () => loadMedia(request));\n },\n};\n\nfunction buildNotionMediaPath(ref: string[]) {\n return `/api/notion/media/${ref.map(encodeURIComponent).join(\"/\")}`;\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)) return fallback;\n return Math.max(min, Math.min(max, parsed));\n}\n\nfunction cacheControl(request: Request) {\n const url = new URL(request.url);\n if (url.searchParams.has(\"v\")) {\n return \"public, max-age=31536000, immutable\";\n }\n\n return \"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400\";\n}\n\nfunction canUseMediaCache(request: Request) {\n if (request.method !== \"GET\") return false;\n return !request.headers.has(\"range\");\n}\n\nfunction mediaCacheHeaders(\n response: Response,\n request: Request,\n state: \"HIT\" | \"MISS\" | \"BYPASS\",\n r2State?: \"HIT\" | \"MISS\" | \"BYPASS\"\n) {\n const headers = new Headers(response.headers);\n headers.set(\"Cache-Control\", cacheControl(request));\n headers.set(\"X-Notion-Media-Cache\", state);\n if (r2State) headers.set(\"X-Notion-Media-R2\", r2State);\n return headers;\n}\n\nasync function responseFromR2Cache(\n request: Request,\n variant: PublicMediaVariant\n) {\n const url = new URL(request.url);\n const r2Key = notionMediaR2KeyForUrl(url, variant);\n const storage = getRuntimePlatform().objectStorage;\n if (!r2Key || !storage) return null;\n\n const object = await storage.get(r2Key);\n if (!object?.body) return null;\n\n const contentType =\n object.contentType ?? (variant === \"avif\" ? \"image/avif\" : \"image/webp\");\n const headers = new Headers();\n headers.set(\"Content-Type\", contentType);\n headers.set(\"Cache-Control\", cacheControl(request));\n headers.set(\"Vary\", \"Accept\");\n headers.set(\"X-Notion-Media-Branch\", \"r2\");\n headers.set(\"X-Notion-Media-R2\", \"HIT\");\n if (object.etag) headers.set(\"ETag\", object.etag);\n\n return new Response(object.body, { headers });\n}\n\nasync function withEdgeMediaCache(\n request: Request,\n variant: PublicMediaVariant,\n load: () => Promise<Response>\n) {\n if (!canUseMediaCache(request)) {\n const response = await load();\n return new Response(response.body, {\n status: response.status,\n headers: mediaCacheHeaders(response, request, \"BYPASS\"),\n });\n }\n\n const url = new URL(request.url);\n const cache = getPublicCache();\n const cacheKey = publicMediaCacheKeyForUrl(url, variant);\n const cached = await cache.match(cacheKey);\n if (cached) {\n return new Response(cached.body, {\n status: cached.status,\n headers: mediaCacheHeaders(cached, request, \"HIT\"),\n });\n }\n\n const r2Response = await responseFromR2Cache(request, variant);\n const response = r2Response ?? (await load());\n const headers = mediaCacheHeaders(response, request, \"MISS\");\n const output = new Response(response.body, {\n status: response.status,\n headers,\n });\n\n if (CACHEABLE_STATUS.has(response.status)) {\n const toCache = output.clone();\n getRequestExecutionContext()?.waitUntil(cache.put(cacheKey, toCache));\n }\n\n return output;\n}\n\nfunction mediaRedirect(url: string) {\n const response = NextResponse.redirect(url, 302);\n response.headers.set(\n \"Cache-Control\",\n \"public, max-age=300, s-maxage=300, stale-while-revalidate=300\"\n );\n return response;\n}\n\nfunction notFound() {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n}\n\nfunction badRequest() {\n return NextResponse.json({ error: \"Invalid media ref\" }, { status: 400 });\n}\n\nfunction forbidden() {\n return NextResponse.json({ error: \"Forbidden\" }, { status: 403 });\n}\n\nasync function serveFileObject(\n input: unknown,\n request: Request,\n options?: { redirectNotionHosted?: boolean }\n) {\n const source = normalizeNotionFileSource(input);\n if (!source) return notFound();\n if (options?.redirectNotionHosted) return mediaRedirect(source.url);\n return proxyNotionHostedFile(source.url, request);\n}\n\nasync function proxyNotionHostedFile(url: string, request: Request) {\n const range = request.headers.get(\"range\");\n const ifRange = request.headers.get(\"if-range\");\n const upstreamHeaders = new Headers({\n Accept: request.headers.get(\"accept\") ?? \"*/*\",\n });\n if (range) upstreamHeaders.set(\"Range\", range);\n if (ifRange) upstreamHeaders.set(\"If-Range\", ifRange);\n\n const upstream = await fetch(url, {\n headers: upstreamHeaders,\n });\n if ((!upstream.ok && upstream.status !== 416) || !upstream.body) {\n return NextResponse.json(\n { error: \"Unable to fetch Notion media\" },\n { status: upstream.status || 502 }\n );\n }\n\n const contentType = upstream.headers.get(\"content-type\") ?? \"\";\n const isImage = contentType.startsWith(\"image/\");\n const accept = request.headers.get(\"accept\") ?? \"\";\n const variant = publicMediaVariantForAccept(accept);\n const urlObj = new URL(request.url);\n const width = clampInt(\n urlObj.searchParams.get(\"w\"),\n 64,\n MAX_WIDTH,\n DEFAULT_WIDTH\n );\n const quality = clampInt(\n urlObj.searchParams.get(\"q\"),\n MIN_QUALITY,\n MAX_QUALITY,\n DEFAULT_QUALITY\n );\n\n let outputFormat: \"image/avif\" | \"image/webp\" | null = null;\n if (variant === \"avif\") {\n outputFormat = \"image/avif\";\n } else if (variant === \"webp\") {\n outputFormat = \"image/webp\";\n }\n\n const platform = getRuntimePlatform();\n const imageTransformer = platform.imageTransformer;\n if (isImage && !range && outputFormat && imageTransformer) {\n const r2Key = notionMediaR2KeyForUrl(urlObj, variant);\n\n try {\n const result = await imageTransformer.transform(upstream.body, {\n width,\n format: outputFormat,\n quality,\n });\n const transformed = result.response();\n const headers = new Headers(transformed.headers);\n headers.set(\"Content-Type\", result.contentType);\n headers.set(\"Cache-Control\", cacheControl(request));\n headers.set(\"Vary\", \"Accept\");\n headers.set(\"X-Notion-Media-Branch\", \"transformed\");\n headers.set(\"X-Notion-Media-R2\", r2Key ? \"MISS\" : \"BYPASS\");\n headers.set(\"X-Optimized-Width\", String(width));\n headers.set(\"X-Optimized-Quality\", String(quality));\n\n if (transformed.body && r2Key && platform.objectStorage) {\n const [clientBody, r2Body] = transformed.body.tee();\n getRequestExecutionContext()?.waitUntil(\n platform.objectStorage.put(r2Key, r2Body, {\n contentType: result.contentType,\n cacheControl: \"public, max-age=31536000, immutable\",\n metadata: {\n source: \"notion\",\n cachedAt: new Date().toISOString(),\n width: String(width),\n quality: String(quality),\n },\n })\n );\n\n return new Response(clientBody, { headers });\n }\n\n return new Response(transformed.body, { headers });\n } catch {\n // Fall through to the original file when the image binding cannot transform.\n }\n }\n\n const headers = new Headers();\n for (const header of [\n \"accept-ranges\",\n \"content-disposition\",\n \"content-encoding\",\n \"content-length\",\n \"content-range\",\n \"content-type\",\n \"etag\",\n \"last-modified\",\n ]) {\n const value = upstream.headers.get(header);\n if (value) headers.set(header, value);\n }\n if (contentType && !headers.has(\"Content-Type\")) {\n headers.set(\"Content-Type\", contentType);\n }\n headers.set(\"Cache-Control\", cacheControl(request));\n headers.set(\"X-Notion-Media-Branch\", \"proxied\");\n return new Response(upstream.body, { status: upstream.status, headers });\n}\n\nasync function loadMedia(request: Request) {\n const url = new URL(request.url);\n const ref = readRefFromPathname(url.pathname);\n if (!ref) return badRequest();\n const client = createNotionClient(await getNotionClientConfig());\n\n if (ref[0] === \"page\" && ref[1] && ref[2] === \"cover\") {\n const page = (await client.pages.retrieve({\n page_id: ref[1],\n })) as NotionPageLike;\n return serveFileObject(page.cover, request);\n }\n\n if (ref[0] === \"page\" && ref[1] && ref[2] === \"property\" && ref[3]) {\n const page = (await client.pages.retrieve({\n page_id: ref[1],\n })) as NotionPageLike;\n const propertyName = decodeURIComponent(ref.slice(3).join(\"/\"));\n return serveFileObject(\n pickFirstFilesPropertyValue(page.properties?.[propertyName]),\n request\n );\n }\n\n if (ref[0] === \"block\" && ref[1]) {\n const block = (await client.blocks.retrieve({\n block_id: ref[1],\n })) as NotionBlock;\n if (block.type === \"video\") {\n return forbidden();\n }\n return serveFileObject(fileObjectForMediaBlock(block), request, {\n redirectNotionHosted:\n block.type === \"audio\" ||\n block.type === \"pdf\" ||\n block.type === \"file\",\n });\n }\n\n return badRequest();\n}\n\nfunction readRefFromPathname(pathname: string): string[] | null {\n const prefix = \"/api/notion/media/\";\n if (!pathname.startsWith(prefix)) return null;\n const encoded = pathname.slice(prefix.length);\n if (!encoded) return null;\n return encoded.split(\"/\").map((part) => decodeURIComponent(part));\n}\n\n// Top-level alias for callers that prefer a flat signature (e.g. the\n// Next.js `app/api/.../route.ts` delegates).\nexport const GET = notionMediaRoute.GET;\n\n/**\n * Worker-friendly single-arg handler. Used by the Cloudflare Workers\n * bootstrap in `@notionx/core/worker`. Equivalent to\n * `notionMediaRoute.handle`.\n */\nexport async function notionMediaRouteHandle(\n request: Request\n): Promise<Response> {\n return notionMediaRoute.handle(request);\n}\n","// Edge cache keys for transformed Notion media responses.\n//\n// Page and API route caching is handled by vinext's CDN cache adapter\n// (vite.config.ts). These helpers remain for the media proxy route.\n\nconst CACHE_ORIGIN = \"https://cache.local\";\nconst CACHE_NAMESPACE = \"/__public-cache/v20260609a\";\nconst NOTION_MEDIA_R2_PREFIX = \"notion-media/v1\";\n\nexport type PublicMediaVariant = \"avif\" | \"webp\" | \"source\";\n\nfunction normalizePath(pathname: string) {\n if (pathname === \"/\") return \"/\";\n return pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n}\n\nexport function publicMediaVariantForAccept(accept: string): PublicMediaVariant {\n if (accept.includes(\"image/avif\")) return \"avif\";\n if (accept.includes(\"image/webp\")) return \"webp\";\n return \"source\";\n}\n\nexport function publicMediaCacheKeyForUrl(\n input: URL,\n variant: PublicMediaVariant\n) {\n const url = new URL(\n `${CACHE_NAMESPACE}${normalizePath(input.pathname)}${input.search}`,\n CACHE_ORIGIN\n );\n url.searchParams.set(\"__variant\", variant);\n url.searchParams.sort();\n return url.toString();\n}\n\nfunction keySegment(value: string) {\n return encodeURIComponent(value || \"none\");\n}\n\nexport function notionMediaR2KeyForUrl(\n input: URL,\n variant: PublicMediaVariant\n) {\n if (variant === \"source\") return null;\n\n const version = input.searchParams.get(\"v\");\n if (!version) return null;\n\n const path = normalizePath(input.pathname)\n .split(\"/\")\n .filter(Boolean)\n .map(keySegment)\n .join(\"/\");\n const width = input.searchParams.get(\"w\") ?? \"source\";\n const quality = input.searchParams.get(\"q\") ?? \"source\";\n\n return [\n NOTION_MEDIA_R2_PREFIX,\n variant,\n path,\n `v-${keySegment(version)}`,\n `w-${keySegment(width)}`,\n `q-${keySegment(quality)}.${variant}`,\n ].join(\"/\");\n}\n","import { Client } from \"@notionhq/client\";\nimport type { NotionClientConfig } from \"./config\";\n\nexport function createNotionClient(config: NotionClientConfig) {\n return new Client({\n auth: config.token,\n baseUrl: config.apiBaseUrl,\n notionVersion: \"2026-03-11\",\n });\n}\n","import type { NotionContentModelLike } from \"./types\";\n\ntype NotionEnv = {\n NOTION_TOKEN?: string;\n NOTION_DATA_SOURCE_ID?: string;\n NOTION_API_BASE_URL?: string;\n NOTION_EDIT_BASE_URL?: string;\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n [key: string]: string | undefined;\n};\n\nexport type NotionClientConfig = {\n token: string;\n apiBaseUrl?: string;\n};\n\nexport type NotionConfig = {\n token: string;\n dataSourceId: string;\n apiBaseUrl?: string;\n editBaseUrl?: string;\n webhookVerificationToken?: string;\n};\n\nfunction readProcessEnv(): NotionEnv {\n const env: NotionEnv = {\n NOTION_TOKEN: process.env.NOTION_TOKEN,\n NOTION_DATA_SOURCE_ID: process.env.NOTION_DATA_SOURCE_ID,\n NOTION_API_BASE_URL: process.env.NOTION_API_BASE_URL,\n NOTION_EDIT_BASE_URL: process.env.NOTION_EDIT_BASE_URL,\n NOTION_WEBHOOK_VERIFICATION_TOKEN:\n process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN,\n };\n\n for (const [key, value] of Object.entries(process.env)) {\n if (key.startsWith(\"NOTION_\") && typeof value === \"string\") {\n env[key] = value;\n }\n }\n\n return env;\n}\n\nasync function readWorkerEnv(): Promise<NotionEnv> {\n try {\n const mod = (await import(\n /* webpackIgnore: true */ \"cloudflare:workers\"\n )) as unknown as { env?: Record<string, unknown> };\n const env: NotionEnv = {};\n for (const [key, value] of Object.entries(mod.env ?? {})) {\n if (key.startsWith(\"NOTION_\") && typeof value === \"string\") {\n env[key] = value;\n }\n }\n return env;\n } catch {\n return {};\n }\n}\n\nfunction readString(source: NotionEnv, name: string): string | undefined {\n const value = String(source[name] ?? \"\").trim();\n return value || undefined;\n}\n\nfunction mergeEnv(...sources: NotionEnv[]): NotionEnv {\n const merged: NotionEnv = {};\n\n for (const source of sources) {\n for (const name of Object.keys(source)) {\n if (!name.startsWith(\"NOTION_\")) continue;\n const value = readString(source, name);\n if (value) merged[name] = value;\n }\n }\n\n return merged;\n}\n\nasync function readEnv(): Promise<NotionEnv> {\n const processEnv = readProcessEnv();\n return mergeEnv(await readWorkerEnv(), processEnv);\n}\n\nfunction readRequired(\n source: NotionEnv,\n name: string\n): string {\n const value = readString(source, name);\n if (!value) {\n throw new Error(`Missing required Notion env: ${name}`);\n }\n return value;\n}\n\nexport function getNotionEditBaseUrl(): string {\n return readString(readProcessEnv(), \"NOTION_EDIT_BASE_URL\") ?? \"https://www.notion.so\";\n}\n\nexport async function hasNotionConfig(): Promise<boolean> {\n const env = await readEnv();\n return Boolean(\n readString(env, \"NOTION_TOKEN\") && readString(env, \"NOTION_DATA_SOURCE_ID\")\n );\n}\n\nexport async function hasNotionModelConfig(\n model: NotionContentModelLike\n): Promise<boolean> {\n const env = await readEnv();\n return Boolean(\n readString(env, \"NOTION_TOKEN\") &&\n (readString(env, model.source.dataSourceEnv) ||\n model.source.defaultDataSourceId)\n );\n}\n\nexport async function getNotionClientConfig(): Promise<NotionClientConfig> {\n const env = await readEnv();\n return {\n token: readRequired(env, \"NOTION_TOKEN\"),\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n };\n}\n\nexport async function getNotionConfig(): Promise<NotionConfig> {\n const env = await readEnv();\n return {\n token: readRequired(env, \"NOTION_TOKEN\"),\n dataSourceId: readRequired(env, \"NOTION_DATA_SOURCE_ID\"),\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n editBaseUrl: readString(env, \"NOTION_EDIT_BASE_URL\"),\n webhookVerificationToken: readString(\n env,\n \"NOTION_WEBHOOK_VERIFICATION_TOKEN\"\n ),\n };\n}\n\nexport async function getNotionWebhookVerificationToken(): Promise<\n string | undefined\n> {\n const env = await readEnv();\n return readString(env, \"NOTION_WEBHOOK_VERIFICATION_TOKEN\");\n}\n\nexport async function getNotionConfigForModel(\n model: NotionContentModelLike\n): Promise<NotionConfig> {\n const env = await readEnv();\n const dataSourceId =\n readString(env, model.source.dataSourceEnv) ??\n model.source.defaultDataSourceId;\n if (!dataSourceId) {\n throw new Error(`Missing required Notion env: ${model.source.dataSourceEnv}`);\n }\n\n return {\n token: readRequired(env, model.source.tokenEnv),\n dataSourceId,\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n editBaseUrl: readString(env, \"NOTION_EDIT_BASE_URL\"),\n webhookVerificationToken: readString(\n env,\n \"NOTION_WEBHOOK_VERIFICATION_TOKEN\"\n ),\n };\n}\n","import type { NotionBlock, NotionFileSource, NotionPageLike } from \"./types\";\n\ntype FileLike = {\n type?: string;\n external?: { url?: string };\n file?: { url?: string; expiry_time?: string };\n name?: string;\n};\n\nfunction stripLeadingSlash(value: string) {\n return value.startsWith(\"/\") ? value.slice(1) : value;\n}\n\nfunction encodePathPart(value: string) {\n return encodeURIComponent(stripLeadingSlash(value));\n}\n\nfunction appendVersion(path: string, version?: string) {\n const value = String(version ?? \"\").trim();\n if (!value) return path;\n return `${path}?${new URLSearchParams({ v: value })}`;\n}\n\nfunction blockVersion(block: NotionBlock): string | undefined {\n return typeof block.last_edited_time === \"string\"\n ? block.last_edited_time\n : undefined;\n}\n\nexport function notionPageCoverMediaPath(pageId: string): string {\n return `/api/notion/media/page/${encodePathPart(pageId)}/cover`;\n}\n\nexport function notionPagePropertyMediaPath(\n pageId: string,\n propertyName: string\n): string {\n return `/api/notion/media/page/${encodePathPart(pageId)}/property/${encodePathPart(propertyName)}`;\n}\n\nexport function notionBlockMediaPath(blockId: string): string {\n return `/api/notion/media/block/${encodePathPart(blockId)}`;\n}\n\nexport function normalizeNotionFileSource(input: unknown): NotionFileSource | null {\n const file = input as FileLike | null | undefined;\n if (!file || typeof file !== \"object\") return null;\n\n if (file.type === \"external\") {\n const url = String(file.external?.url ?? \"\").trim();\n return url ? { type: \"external\", url } : null;\n }\n\n if (file.type === \"file\") {\n const url = String(file.file?.url ?? \"\").trim();\n if (!url) return null;\n return {\n type: \"file\",\n url,\n expiryTime: String(file.file?.expiry_time ?? \"\").trim() || null,\n };\n }\n\n return null;\n}\n\nexport function resolveNotionFileUrl(input: unknown): string | null {\n return normalizeNotionFileSource(input)?.url ?? null;\n}\n\nexport function isNotionHostedFile(input: unknown): boolean {\n return normalizeNotionFileSource(input)?.type === \"file\";\n}\n\nexport function pickFirstFilesPropertyValue(property: unknown): unknown | null {\n const value = property as { type?: string; files?: unknown[] } | null | undefined;\n if (!value || value.type !== \"files\" || !Array.isArray(value.files)) {\n return null;\n }\n return value.files[0] ?? null;\n}\n\nexport function pickPageCoverFile(page: NotionPageLike): unknown | null {\n return page.cover ?? null;\n}\n\nexport function coverImageUrlForPage(\n page: NotionPageLike,\n coverPropertyName = \"Cover\"\n): string | null {\n const propertyFile = pickFirstFilesPropertyValue(\n page.properties?.[coverPropertyName]\n );\n const propertySource = normalizeNotionFileSource(propertyFile);\n if (propertySource) {\n return appendVersion(\n notionPagePropertyMediaPath(page.id, coverPropertyName),\n page.last_edited_time\n );\n }\n\n const coverSource = normalizeNotionFileSource(pickPageCoverFile(page));\n if (coverSource) {\n return appendVersion(notionPageCoverMediaPath(page.id), page.last_edited_time);\n }\n\n return null;\n}\n\nexport function fileObjectForMediaBlock(block: NotionBlock): unknown | null {\n const typed = block[block.type] as Record<string, unknown> | undefined;\n if (!typed || typeof typed !== \"object\") return null;\n\n if (\n block.type === \"image\" ||\n block.type === \"video\" ||\n block.type === \"file\" ||\n block.type === \"pdf\" ||\n block.type === \"audio\"\n ) {\n return typed;\n }\n\n return null;\n}\n\nexport function mediaUrlForBlock(block: NotionBlock): string | null {\n const source = normalizeNotionFileSource(fileObjectForMediaBlock(block));\n if (!source) return null;\n if (source.type === \"external\" && block.type !== \"image\") {\n return source.url;\n }\n return appendVersion(notionBlockMediaPath(block.id), blockVersion(block));\n}\n\nexport function firstImageUrlFromBlocks(blocks: NotionBlock[]): string | null {\n for (const block of blocks) {\n if (block.type === \"image\") {\n const imageUrl = mediaUrlForBlock(block);\n if (imageUrl) return imageUrl;\n }\n\n const nested = block.children?.length\n ? firstImageUrlFromBlocks(block.children)\n : null;\n if (nested) return nested;\n }\n\n return null;\n}\n\nexport function publicMediaBlockForApi(block: NotionBlock): NotionBlock {\n const value = block[block.type];\n const source = normalizeNotionFileSource(value);\n const children = block.children?.map(publicMediaBlockForApi);\n\n if (!source) {\n return children ? { ...block, children } : { ...block };\n }\n\n const path = appendVersion(notionBlockMediaPath(block.id), blockVersion(block));\n const publicValue =\n source.type === \"external\"\n ? block.type === \"image\"\n ? {\n ...(value as Record<string, unknown>),\n external: { url: path },\n }\n : value\n : {\n ...(value as Record<string, unknown>),\n file: {\n url: path,\n expiry_time: null,\n },\n };\n\n return {\n ...block,\n [block.type]: publicValue,\n ...(children ? { children } : {}),\n };\n}\n\nexport function gatedMediaBlockForApi(\n block: NotionBlock,\n options?: { accessUrlForBlock?: (block: NotionBlock) => string | null }\n): NotionBlock {\n const value = block[block.type];\n const source = normalizeNotionFileSource(value);\n const children = block.children?.map((child) =>\n gatedMediaBlockForApi(child, options)\n );\n\n if (block.type !== \"video\" || !source) {\n const publicBlock = publicMediaBlockForApi(block);\n return children ? { ...publicBlock, children } : publicBlock;\n }\n\n const gatedValue: Record<string, unknown> = {\n ...(value as Record<string, unknown>),\n gated: true,\n access_url: options?.accessUrlForBlock?.(block) ?? null,\n };\n\n if (source.type === \"external\") {\n gatedValue.external = { url: null };\n } else {\n gatedValue.file = {\n url: null,\n expiry_time: null,\n };\n }\n\n return {\n ...block,\n video: gatedValue,\n ...(children ? { children } : {}),\n };\n}\n\nexport function isDirectVideoUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return /\\.(mp4|webm|mov|m4v)(?:$|\\?)/i.test(parsed.pathname);\n } catch {\n return false;\n }\n}\n\nexport function videoEmbedUrl(url: string): string | null {\n try {\n const parsed = new URL(url);\n const host = parsed.hostname.replace(/^www\\./, \"\");\n\n if (host === \"youtube.com\" || host === \"m.youtube.com\") {\n const id = parsed.searchParams.get(\"v\");\n return id ? `https://www.youtube.com/embed/${encodeURIComponent(id)}` : null;\n }\n\n if (host === \"youtu.be\") {\n const id = parsed.pathname.split(\"/\").filter(Boolean)[0];\n return id ? `https://www.youtube.com/embed/${encodeURIComponent(id)}` : null;\n }\n\n if (host === \"youtube-nocookie.com\") {\n return parsed.toString();\n }\n\n if (host === \"vimeo.com\") {\n const id = parsed.pathname.split(\"/\").filter(Boolean)[0];\n return id ? `https://player.vimeo.com/video/${encodeURIComponent(id)}` : null;\n }\n\n if (host === \"player.vimeo.com\") {\n return parsed.toString();\n }\n } catch {\n return null;\n }\n\n return null;\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 /** 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"],"mappings":"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","names":["env","env","env","options","getRuntimePlatform","getPublicCache","getRuntimePlatform","response","getPublicCache","headers"]}
@@ -59,7 +59,6 @@ function readProcessEnv() {
59
59
  const env2 = {
60
60
  NOTION_TOKEN: process.env.NOTION_TOKEN,
61
61
  NOTION_DATA_SOURCE_ID: process.env.NOTION_DATA_SOURCE_ID,
62
- NOTION_MOVIES_DATA_SOURCE_ID: process.env.NOTION_MOVIES_DATA_SOURCE_ID,
63
62
  NOTION_API_BASE_URL: process.env.NOTION_API_BASE_URL,
64
63
  NOTION_EDIT_BASE_URL: process.env.NOTION_EDIT_BASE_URL,
65
64
  NOTION_WEBHOOK_VERIFICATION_TOKEN: process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN