@notionx/core 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/rate-limit.js.map +1 -1
- package/dist/auth/routes/google-callback.js.map +1 -1
- package/dist/auth/routes/google.js.map +1 -1
- package/dist/auth/routes/index.js.map +1 -1
- package/dist/auth/routes/verify-email.js.map +1 -1
- package/dist/auth/routes/viewer.js.map +1 -1
- package/dist/auth/turnstile.js.map +1 -1
- package/dist/auth/user-session.d.ts +1 -1
- package/dist/auth/user-session.js.map +1 -1
- package/dist/auth/users.js.map +1 -1
- package/dist/content/index.d.ts +3 -2
- package/dist/content/index.js +176 -60
- package/dist/content/index.js.map +1 -1
- package/dist/content/localized.d.ts +67 -0
- package/dist/content/localized.js +170 -0
- package/dist/content/localized.js.map +1 -0
- package/dist/content/revalidate.d.ts +2 -1
- package/dist/content/revalidate.js +5 -28
- package/dist/content/revalidate.js.map +1 -1
- package/dist/content/search-index.d.ts +1 -1
- package/dist/content/search-index.js.map +1 -1
- package/dist/content/search.d.ts +2 -5
- package/dist/content/search.js +3 -32
- package/dist/content/search.js.map +1 -1
- package/dist/email/index.js.map +1 -1
- package/dist/{env-C5qu-0R-.d.ts → env-hoez1e-n.d.ts} +0 -4
- package/dist/i18n/index.d.ts +18 -24
- package/dist/i18n/index.js +29 -54
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/admin/index.js.map +1 -1
- package/dist/media/index.js +3 -2
- package/dist/media/index.js.map +1 -1
- package/dist/media/routes/index.js +0 -1
- package/dist/media/routes/index.js.map +1 -1
- package/dist/media/routes/notion-media.js +0 -1
- package/dist/media/routes/notion-media.js.map +1 -1
- package/dist/notion/config.d.ts +1 -4
- package/dist/notion/config.js +1 -23
- package/dist/notion/config.js.map +1 -1
- package/dist/notion/content-cache.d.ts +1 -1
- package/dist/notion/generic-source.js +0 -1
- package/dist/notion/generic-source.js.map +1 -1
- package/dist/notion/index.d.ts +4 -4
- package/dist/notion/index.js +8 -23
- package/dist/notion/index.js.map +1 -1
- package/dist/notion/mappers.js.map +1 -1
- package/dist/notion/media.d.ts +1 -1
- package/dist/notion/media.js +1 -1
- package/dist/notion/media.js.map +1 -1
- package/dist/notion/property-mappers.d.ts +2 -1
- package/dist/notion/property-mappers.js +7 -0
- package/dist/notion/property-mappers.js.map +1 -1
- package/dist/notion/routes/index.d.ts +1 -1
- package/dist/notion/routes/index.js +0 -1
- package/dist/notion/routes/index.js.map +1 -1
- package/dist/notion/routes/webhook.d.ts +1 -1
- package/dist/notion/routes/webhook.js +0 -1
- package/dist/notion/routes/webhook.js.map +1 -1
- package/dist/notion/types.d.ts +1 -73
- package/dist/notion/webhook.d.ts +1 -1
- package/dist/notion/webhook.js +0 -1
- package/dist/notion/webhook.js.map +1 -1
- package/dist/pages/index.d.ts +117 -0
- package/dist/pages/index.js +487 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/platform/current.d.ts +1 -1
- package/dist/platform/current.js.map +1 -1
- package/dist/platform/index.d.ts +1 -1
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/runtime.d.ts +1 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/routes/cdn.js.map +1 -1
- package/dist/storage/routes/files.js.map +1 -1
- package/dist/storage/routes/index.js.map +1 -1
- package/dist/util/index.d.ts +1 -1
- package/dist/util/index.js +1 -2
- package/dist/util/index.js.map +1 -1
- package/dist/worker/index.js +0 -1
- package/dist/worker/index.js.map +1 -1
- package/dist/worker/routes/content-revalidate.d.ts +1 -1
- package/dist/worker/routes/health.js.map +1 -1
- package/dist/worker/routes/index.d.ts +1 -1
- package/dist/worker/routes/index.js.map +1 -1
- package/package.json +14 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pages/model.ts","../../src/pages/source.ts","../../src/notion/blocks.ts","../../src/notion/client.ts","../../src/notion/config.ts","../../src/notion/media.ts","../../src/notion/property-mappers.ts"],"sourcesContent":["import type {\n DefinedSitePageModel,\n SitePageFields,\n SitePageModel,\n} from \"./types\";\n\nexport const defaultSitePageFields: SitePageFields = {\n title: \"Name\",\n key: \"Key\",\n slug: \"Slug\",\n status: \"Status\",\n layout: \"Layout\",\n description: \"Description\",\n seoTitle: \"SEO Title\",\n seoDescription: \"SEO Description\",\n showHeader: \"Show Header\",\n showFooter: \"Show Footer\",\n showInNav: \"Show in Nav\",\n navLabel: \"Nav Label\",\n navOrder: \"Nav Order\",\n showInFooter: \"Show in Footer\",\n footerLabel: \"Footer Label\",\n footerGroup: \"Footer Group\",\n footerOrder: \"Footer Order\",\n contentSource: \"Content Source\",\n cover: \"Cover\",\n};\n\nexport const defaultPagesDataSourceEnv = \"NOTION_PAGES_DATA_SOURCE_ID\";\n\nexport function defineSitePageModel(model: SitePageModel): DefinedSitePageModel {\n return {\n ...model,\n source: {\n ...model.source,\n fields: {\n ...defaultSitePageFields,\n ...(model.source.fields ?? {}),\n },\n query: {\n pageSize: 100,\n ...(model.source.query ?? {}),\n },\n },\n };\n}\n","import { cache } from \"react\";\nimport { listBlockChildrenDeep, type NotionBlockClient } from \"../notion/blocks\";\nimport { createNotionClient } from \"../notion/client\";\nimport { getNotionConfigForModel, hasNotionModelConfig } from \"../notion/config\";\nimport { coverImageUrlForPage } from \"../notion/media\";\nimport {\n getCheckboxProperty,\n getNumberProperty,\n getRichTextProperty,\n getSelectProperty,\n isRecord,\n notionPageEditUrl,\n} from \"../notion/property-mappers\";\nimport type { NotionBlock, NotionPageLike } from \"../notion/types\";\nimport { defineSitePageModel } from \"./model\";\nimport type {\n SitePage,\n SitePageFields,\n SitePageFooterGroup,\n SitePageModel,\n SitePageNavItem,\n SitePageSourceDeps,\n SitePageLayout,\n} from \"./types\";\n\nfunction normalizePage(input: unknown): NotionPageLike | null {\n if (!input || typeof input !== \"object\") return null;\n const page = input as NotionPageLike;\n return page.id ? page : null;\n}\n\nexport function slugToHref(slug: string) {\n const normalized = slug.trim().replace(/^\\/+|\\/+$/g, \"\");\n return normalized ? `/${normalized}` : \"/\";\n}\n\nexport function normalizePageSlug(slug: string) {\n return slug.trim().replace(/^\\/+|\\/+$/g, \"\").toLowerCase();\n}\n\nfunction isPublishedStatus(value: string) {\n return value.trim().toLowerCase() === \"published\";\n}\n\nfunction getCheckboxPropertyWithFallback(\n properties: Record<string, unknown>,\n key: string,\n fallback: boolean\n) {\n const property = properties[key] as Record<string, unknown> | undefined;\n if (property?.type !== \"checkbox\") return fallback;\n return getCheckboxProperty(properties, key);\n}\n\nfunction normalizeLayout(value: string): SitePageLayout {\n const normalized = value.trim().toLowerCase();\n if (normalized === \"home\") return \"home\";\n if (normalized === \"legal\") return \"legal\";\n if (normalized === \"content-list\") return \"content-list\";\n return \"default\";\n}\n\nexport function mapNotionPageToSitePage(\n page: NotionPageLike,\n fields: SitePageFields,\n blocks: NotionBlock[] = [],\n options?: { editBaseUrl?: string }\n): SitePage | null {\n const properties = isRecord(page.properties) ? page.properties : {};\n const title = getRichTextProperty(properties, fields.title);\n const key = getRichTextProperty(properties, fields.key).toLowerCase();\n const slug = normalizePageSlug(getRichTextProperty(properties, fields.slug));\n const status = getSelectProperty(properties, fields.status);\n if (!title || !key || !isPublishedStatus(status)) return null;\n\n const description = getRichTextProperty(properties, fields.description);\n const seoTitle = getRichTextProperty(properties, fields.seoTitle) || title;\n const seoDescription =\n getRichTextProperty(properties, fields.seoDescription) || description;\n const navLabel = getRichTextProperty(properties, fields.navLabel) || title;\n const footerLabel =\n getRichTextProperty(properties, fields.footerLabel) || navLabel;\n\n return {\n pageId: page.id,\n key,\n slug,\n href: slugToHref(slug),\n title,\n description,\n seoTitle,\n seoDescription,\n layout: normalizeLayout(getSelectProperty(properties, fields.layout)),\n published: true,\n showHeader: getCheckboxPropertyWithFallback(\n properties,\n fields.showHeader,\n true\n ),\n showFooter: getCheckboxPropertyWithFallback(\n properties,\n fields.showFooter,\n true\n ),\n showInNav: getCheckboxProperty(properties, fields.showInNav),\n navLabel,\n navOrder: getNumberProperty(properties, fields.navOrder, 100),\n showInFooter: getCheckboxProperty(properties, fields.showInFooter),\n footerLabel,\n footerGroup: getSelectProperty(properties, fields.footerGroup) || \"Company\",\n footerOrder: getNumberProperty(properties, fields.footerOrder, 100),\n contentSource: getRichTextProperty(properties, fields.contentSource),\n coverImage: coverImageUrlForPage(page, fields.cover),\n editUrl: notionPageEditUrl(page.id, options?.editBaseUrl),\n blocks,\n };\n}\n\nexport function createSitePageSource(\n modelInput: SitePageModel,\n deps: SitePageSourceDeps\n) {\n const model = defineSitePageModel(modelInput);\n const fields = model.source.fields;\n\n return {\n async listPages(): Promise<SitePage[]> {\n const pages: SitePage[] = [];\n let cursor: string | undefined;\n\n do {\n const response = await deps.queryDataSource({ startCursor: cursor });\n for (const item of response.results ?? []) {\n const page = normalizePage(item);\n if (!page) continue;\n const mapped = mapNotionPageToSitePage(\n page,\n fields,\n await deps.getPageBlocks(page.id),\n { editBaseUrl: deps.editBaseUrl }\n );\n if (mapped) pages.push(mapped);\n }\n\n cursor = response.next_cursor ?? undefined;\n if (!response.has_more) break;\n } while (cursor);\n\n return pages.sort((a, b) => a.navOrder - b.navOrder || a.title.localeCompare(b.title));\n },\n };\n}\n\nasync function createDefaultSitePageSource(modelInput: SitePageModel) {\n const model = defineSitePageModel(modelInput);\n if (!(await hasNotionModelConfig(model))) return null;\n\n const config = await getNotionConfigForModel(model);\n const client = createNotionClient(config);\n return createSitePageSource(model, {\n editBaseUrl: config.editBaseUrl,\n queryDataSource: async ({ startCursor } = {}) =>\n client.dataSources.query({\n data_source_id: config.dataSourceId,\n page_size: model.source.query.pageSize,\n sorts: [{ property: model.source.fields.navOrder, direction: \"ascending\" }],\n ...(startCursor ? { start_cursor: startCursor } : {}),\n }),\n getPageBlocks: (pageId) =>\n listBlockChildrenDeep(client as unknown as NotionBlockClient, pageId),\n });\n}\n\nconst defaultSourceCache = cache(createDefaultSitePageSource);\n\nexport function createSitePagesApi(input: {\n model: SitePageModel;\n fallbackPages?: readonly SitePage[];\n}) {\n const fallbackPages = [...(input.fallbackPages ?? [])];\n\n const listSitePages = cache(async (): Promise<SitePage[]> => {\n try {\n const source = await defaultSourceCache(input.model);\n const pages = source ? await source.listPages() : [];\n return pages.length ? pages : fallbackPages;\n } catch {\n return fallbackPages;\n }\n });\n\n return {\n listSitePages,\n async getSitePageByKey(key: string) {\n const pages = await listSitePages();\n return pages.find((page) => page.key === key.toLowerCase()) ?? null;\n },\n async getSitePageBySlug(slug: string) {\n const normalized = normalizePageSlug(slug);\n const pages = await listSitePages();\n return pages.find((page) => page.slug === normalized) ?? null;\n },\n async getSitePageForContentSource(sourceId: string) {\n const pages = await listSitePages();\n return (\n pages.find(\n (page) =>\n page.layout === \"content-list\" && page.contentSource === sourceId\n ) ?? null\n );\n },\n async getSiteNavigation(): Promise<SitePageNavItem[]> {\n const pages = await listSitePages();\n return deriveSiteNavigation(pages);\n },\n async getSiteFooterGroups(): Promise<SitePageFooterGroup[]> {\n const pages = await listSitePages();\n return deriveSiteFooterGroups(pages);\n },\n };\n}\n\nexport function deriveSiteNavigation(\n pages: readonly SitePage[]\n): SitePageNavItem[] {\n return pages\n .filter((page) => page.showInNav)\n .map((page) => ({\n label: page.navLabel,\n href: page.href,\n order: page.navOrder,\n pageKey: page.key,\n }))\n .sort((a, b) => a.order - b.order || a.label.localeCompare(b.label));\n}\n\nexport function deriveSiteFooterGroups(\n pages: readonly SitePage[]\n): SitePageFooterGroup[] {\n const groups = new Map<string, SitePageNavItem[]>();\n\n for (const page of pages.filter((candidate) => candidate.showInFooter)) {\n const label = page.footerGroup || \"Company\";\n const items = groups.get(label) ?? [];\n items.push({\n label: page.footerLabel,\n href: page.href,\n order: page.footerOrder,\n pageKey: page.key,\n });\n groups.set(label, items);\n }\n\n return Array.from(groups.entries()).map(([label, items]) => ({\n label,\n items: items.sort(\n (a, b) => a.order - b.order || a.label.localeCompare(b.label)\n ),\n }));\n}\n","import type { NotionBlock } from \"./types\";\n\ntype BlockChildrenListResponse = {\n results?: unknown[];\n has_more?: boolean;\n next_cursor?: string | null;\n};\n\nexport type NotionBlockClient = {\n blocks: {\n children: {\n list: (args: {\n block_id: string;\n page_size?: number;\n start_cursor?: string;\n }) => Promise<BlockChildrenListResponse>;\n };\n };\n};\n\nfunction normalizeBlock(input: unknown): NotionBlock | null {\n if (!input || typeof input !== \"object\") return null;\n const block = input as NotionBlock;\n return block.id && block.type ? block : null;\n}\n\nexport async function listBlockChildren(\n client: NotionBlockClient,\n blockId: string\n): Promise<NotionBlock[]> {\n const results: NotionBlock[] = [];\n let cursor: string | undefined;\n\n do {\n const response = await client.blocks.children.list({\n block_id: blockId,\n page_size: 100,\n ...(cursor ? { start_cursor: cursor } : {}),\n });\n\n for (const item of response.results ?? []) {\n const block = normalizeBlock(item);\n if (block) results.push(block);\n }\n\n cursor = response.next_cursor ?? undefined;\n if (!response.has_more) break;\n } while (cursor);\n\n return results;\n}\n\nexport async function listBlockChildrenDeep(\n client: NotionBlockClient,\n blockId: string,\n options?: { maxDepth?: number }\n): Promise<NotionBlock[]> {\n const maxDepth = options?.maxDepth ?? 6;\n\n async function visit(id: string, depth: number): Promise<NotionBlock[]> {\n const children = await listBlockChildren(client, id);\n if (depth >= maxDepth) return children;\n\n return Promise.all(\n children.map(async (block) => {\n if (!block.has_children) return block;\n return {\n ...block,\n children: await visit(block.id, depth + 1),\n };\n })\n );\n }\n\n return visit(blockId, 0);\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","type PropertyMap = Record<string, unknown>;\n\ntype TextPart = {\n plain_text?: string;\n};\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value && typeof value === \"object\");\n}\n\nfunction getPlainText(parts: unknown): string {\n if (!Array.isArray(parts)) return \"\";\n return parts\n .map((part: TextPart) => part.plain_text ?? \"\")\n .join(\"\")\n .trim();\n}\n\nfunction getProperty(properties: PropertyMap, key: string) {\n return properties[key] as Record<string, unknown> | undefined;\n}\n\nfunction firstPropertyOfType(properties: PropertyMap, type: string) {\n return Object.values(properties).find(\n (property): property is Record<string, unknown> =>\n isRecord(property) && property.type === type\n );\n}\n\nexport function getFirstTitleProperty(properties: PropertyMap): string {\n const property = firstPropertyOfType(properties, \"title\");\n return property ? getPlainText(property.title) : \"\";\n}\n\nexport function getRichTextProperty(properties: PropertyMap, key: string): string {\n const property = getProperty(properties, key);\n if (!property) return \"\";\n\n if (property.type === \"title\") return getPlainText(property.title);\n if (property.type === \"rich_text\") return getPlainText(property.rich_text);\n if (property.type === \"url\") return String(property.url ?? \"\").trim();\n if (property.type === \"email\") return String(property.email ?? \"\").trim();\n if (property.type === \"phone_number\") {\n return String(property.phone_number ?? \"\").trim();\n }\n\n return \"\";\n}\n\nexport function getDateProperty(properties: PropertyMap, key: string): string {\n const property = getProperty(properties, key);\n if (property?.type !== \"date\") return \"\";\n const date = property.date as { start?: string } | null | undefined;\n return String(date?.start ?? \"\").trim();\n}\n\nexport function getFirstDateProperty(properties: PropertyMap): string {\n const property = firstPropertyOfType(properties, \"date\");\n if (!property) return \"\";\n const date = property.date as { start?: string } | null | undefined;\n return String(date?.start ?? \"\").trim();\n}\n\nexport function getSelectProperty(properties: PropertyMap, key: string): string {\n const property = getProperty(properties, key);\n if (property?.type !== \"select\") return \"\";\n const select = property.select as { name?: string } | null | undefined;\n return String(select?.name ?? \"\").trim();\n}\n\nexport function getCheckboxProperty(properties: PropertyMap, key: string): boolean {\n const property = getProperty(properties, key);\n if (property?.type !== \"checkbox\") return false;\n return Boolean(property.checkbox);\n}\n\nexport function getNumberProperty(\n properties: PropertyMap,\n key: string,\n fallback = 0\n): number {\n const property = getProperty(properties, key);\n if (property?.type !== \"number\") return fallback;\n const value = Number(property.number);\n return Number.isFinite(value) ? value : fallback;\n}\n\nexport function getRelationPageIds(properties: PropertyMap, key: string): string[] {\n const property = getProperty(properties, key);\n if (property?.type !== \"relation\" || !Array.isArray(property.relation)) {\n return [];\n }\n\n return property.relation\n .map((item: { id?: string }) => String(item.id ?? \"\").trim())\n .filter(Boolean);\n}\n\nexport function getTagsProperty(properties: PropertyMap, key: string): string[] {\n const property = getProperty(properties, key);\n if (property?.type === \"multi_select\" && Array.isArray(property.multi_select)) {\n return property.multi_select\n .map((item: { name?: string }) => String(item.name ?? \"\").trim())\n .filter(Boolean);\n }\n\n if (property?.type === \"select\") {\n const select = property.select as { name?: string } | null | undefined;\n const name = String(select?.name ?? \"\").trim();\n return name ? [name] : [];\n }\n\n return [];\n}\n\nexport function getFirstTagsProperty(properties: PropertyMap): string[] {\n const multiSelect = firstPropertyOfType(properties, \"multi_select\");\n if (multiSelect && Array.isArray(multiSelect.multi_select)) {\n return multiSelect.multi_select\n .map((item: { name?: string }) => String(item.name ?? \"\").trim())\n .filter(Boolean);\n }\n\n const select = firstPropertyOfType(properties, \"select\");\n const name = String((select?.select as { name?: string } | null)?.name ?? \"\").trim();\n return name ? [name] : [];\n}\n\nexport function getAuthorProperty(properties: PropertyMap, key: string): string {\n const property = getProperty(properties, key);\n if (!property) return \"\";\n\n if (property.type === \"people\" && Array.isArray(property.people)) {\n return property.people\n .map((person: { name?: string; person?: { email?: string } }) =>\n String(person.name ?? person.person?.email ?? \"\").trim()\n )\n .filter(Boolean)\n .join(\", \");\n }\n\n return getRichTextProperty(properties, key);\n}\n\nexport function getFirstPeopleProperty(properties: PropertyMap): string {\n const property = firstPropertyOfType(properties, \"people\");\n if (!property || !Array.isArray(property.people)) return \"\";\n\n return property.people\n .map((person: { name?: string; person?: { email?: string } }) =>\n String(person.name ?? person.person?.email ?? \"\").trim()\n )\n .filter(Boolean)\n .join(\", \");\n}\n\nexport function pickPublishedFlag(properties: PropertyMap): boolean {\n const published = getProperty(properties, \"Published\");\n if (published?.type === \"checkbox\") {\n return Boolean(published.checkbox);\n }\n\n const status = getProperty(properties, \"Status\");\n if (status?.type === \"status\") {\n const statusValue = status.status as { name?: string } | null | undefined;\n return String(statusValue?.name ?? \"\").trim().toLowerCase() === \"published\";\n }\n\n if (status?.type === \"select\") {\n const statusValue = status.select as { name?: string } | null | undefined;\n return String(statusValue?.name ?? \"\").trim().toLowerCase() === \"published\";\n }\n\n return false;\n}\n\nexport function pickDescriptionFallback(description: string, title: string): string {\n return description.trim() || title.trim();\n}\n\nexport function isValidPublicSlug(slug: string): boolean {\n return /^[a-z0-9][a-z0-9-]{0,79}$/.test(slug);\n}\n\nexport function notionPageEditUrl(pageId: string, editBaseUrl?: string): string {\n const compactPageId = pageId.replaceAll(\"-\", \"\");\n if (editBaseUrl?.includes(\"{pageId}\")) {\n return editBaseUrl.replaceAll(\"{pageId}\", compactPageId);\n }\n return `https://www.notion.so/${compactPageId}`;\n}\n\n/**\n * Normalize a Notion page id (with or without dashes) to a compact lowercase\n * string. Used as a stable identifier in URLs and cache keys.\n */\nexport function compactNotionId(id: string): string {\n return id.replaceAll(\"-\", \"\").toLowerCase();\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SqlDatabaseAdapter, KeyValueCacheAdapter, PublicCacheAdapter, RuntimePlatform } from './runtime.js';
|
|
2
2
|
import { currentRuntimeId } from './selection.js';
|
|
3
|
-
import '../env-
|
|
3
|
+
import '../env-hoez1e-n.js';
|
|
4
4
|
|
|
5
5
|
declare function getRuntimePlatform(): RuntimePlatform;
|
|
6
6
|
declare function getDatabase(): SqlDatabaseAdapter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/util/env.ts","../../src/platform/runtime.ts","../../src/platform/cloudflare-runtime.ts","../../src/platform/selection.ts","../../src/platform/current.ts"],"sourcesContent":["// 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","export type RuntimeId = \"cloudflare-workers\";\n\nexport type RuntimeKind = \"cloudflare\";\n\nexport interface RuntimeSelection {\n kind: RuntimeKind;\n runtimeId: RuntimeId;\n}\n\nfunction hasCloudflareBindings(env: unknown): boolean {\n if (!env || typeof env !== \"object\") return false;\n const record = env as Record<string, unknown>;\n return (\n \"DB\" in record ||\n \"ASSETS_BUCKET\" in record ||\n \"R2\" in record ||\n \"IMAGES\" in record ||\n \"CONTENT_CACHE\" in record\n );\n}\n\nexport function selectRuntime(env: unknown): RuntimeSelection {\n if (hasCloudflareBindings(env)) {\n return { kind: \"cloudflare\", runtimeId: \"cloudflare-workers\" };\n }\n throw new Error(\n \"No supported runtime detected. Expected Cloudflare Workers bindings \" +\n \"(DB, ASSETS_BUCKET, R2, IMAGES, or CONTENT_CACHE).\",\n );\n}\n\nexport function currentRuntimeId(): RuntimeId {\n return \"cloudflare-workers\";\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","options","getRuntimePlatform","getPublicCache"]}
|
|
1
|
+
{"version":3,"sources":["../../src/util/env.ts","../../src/platform/runtime.ts","../../src/platform/cloudflare-runtime.ts","../../src/platform/selection.ts","../../src/platform/current.ts"],"sourcesContent":["// 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","export type RuntimeId = \"cloudflare-workers\";\n\nexport type RuntimeKind = \"cloudflare\";\n\nexport interface RuntimeSelection {\n kind: RuntimeKind;\n runtimeId: RuntimeId;\n}\n\nfunction hasCloudflareBindings(env: unknown): boolean {\n if (!env || typeof env !== \"object\") return false;\n const record = env as Record<string, unknown>;\n return (\n \"DB\" in record ||\n \"ASSETS_BUCKET\" in record ||\n \"R2\" in record ||\n \"IMAGES\" in record ||\n \"CONTENT_CACHE\" in record\n );\n}\n\nexport function selectRuntime(env: unknown): RuntimeSelection {\n if (hasCloudflareBindings(env)) {\n return { kind: \"cloudflare\", runtimeId: \"cloudflare-workers\" };\n }\n throw new Error(\n \"No supported runtime detected. Expected Cloudflare Workers bindings \" +\n \"(DB, ASSETS_BUCKET, R2, IMAGES, or CONTENT_CACHE).\",\n );\n}\n\nexport function currentRuntimeId(): RuntimeId {\n return \"cloudflare-workers\";\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","options","getRuntimePlatform","getPublicCache"]}
|
package/dist/platform/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { ImageTransformOptions, ImageTransformResult, ImageTransformerAdapter, K
|
|
|
2
2
|
export { RuntimeAdapterDefinition, RuntimeCapability, RuntimeServiceStatus, cloudflareWorkersAdapter, getRuntimeAdapter, runtimeAdapters, runtimeServiceStatus } from './capabilities.js';
|
|
3
3
|
export { getDatabase, getKeyValueCache, getPublicCache, getRuntimePlatform, runtimeSelection } from './current.js';
|
|
4
4
|
export { RuntimeId, RuntimeKind, RuntimeSelection, currentRuntimeId, selectRuntime } from './selection.js';
|
|
5
|
-
import '../env-
|
|
5
|
+
import '../env-hoez1e-n.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/platform/runtime.ts","../../src/platform/capabilities.ts","../../src/util/env.ts","../../src/platform/cloudflare-runtime.ts","../../src/platform/selection.ts","../../src/platform/current.ts"],"sourcesContent":["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 type { RuntimeId } from \"./selection\";\n\nexport type RuntimeCapability =\n | \"server-rendering\"\n | \"edge-cache\"\n | \"relational-storage\"\n | \"object-storage\"\n | \"image-optimization\"\n | \"secrets\"\n | \"observability\";\n\nexport type RuntimeAdapterDefinition = {\n id: RuntimeId;\n label: string;\n status: \"active\" | \"partial\" | \"planned\";\n services: {\n compute: string;\n relationalStorage: string;\n objectStorage: string;\n imageOptimization: string;\n cache: string;\n authStorage: string;\n };\n capabilities: readonly RuntimeCapability[];\n};\n\nexport type RuntimeServiceStatus = {\n database: boolean;\n objectStorage: boolean;\n imageTransformer: boolean;\n publicCache: boolean;\n};\n\nexport const cloudflareWorkersAdapter: RuntimeAdapterDefinition = {\n id: \"cloudflare-workers\",\n label: \"Cloudflare Workers + D1\",\n status: \"active\",\n services: {\n compute: \"Cloudflare Workers via vinext\",\n relationalStorage: \"D1 through the runtime SQL adapter\",\n objectStorage: \"R2\",\n imageOptimization: \"Cloudflare Images\",\n cache: \"vinext CDN/data adapters and caches.default for media\",\n authStorage: \"D1 users and signed cookies\",\n },\n capabilities: [\n \"server-rendering\",\n \"edge-cache\",\n \"relational-storage\",\n \"object-storage\",\n \"image-optimization\",\n \"secrets\",\n \"observability\",\n ],\n};\n\nexport const runtimeAdapters = [cloudflareWorkersAdapter] as const;\n\nexport function getRuntimeAdapter(id: RuntimeAdapterDefinition[\"id\"]) {\n return runtimeAdapters.find((adapter) => adapter.id === id);\n}\n\nexport function runtimeServiceStatus(\n platform: {\n database: unknown;\n objectStorage: unknown;\n imageTransformer: unknown;\n publicCache: unknown;\n }\n): RuntimeServiceStatus {\n return {\n database: Boolean(platform.database),\n objectStorage: Boolean(platform.objectStorage),\n imageTransformer: Boolean(platform.imageTransformer),\n publicCache: Boolean(platform.publicCache),\n };\n}\n","// lib/env.ts - 集中获取 Cloudflare bindings\n// 用 cloudflare:workers 模块(workerd 内置),作为平台 adapter 的绑定入口\n\n/// <reference types=\"@cloudflare/workers-types\" />\nimport { env } from \"cloudflare:workers\";\n\nexport type AppEnv = {\n DB: D1Database;\n ASSETS: Fetcher;\n IMAGES: ImagesBinding;\n ASSETS_BUCKET?: R2Bucket;\n CONTENT_CACHE?: KVNamespace;\n ADMIN_PASSWORD: string;\n ADMIN_EMAIL?: string;\n SITE_URL?: string;\n RESEND_API_KEY?: string;\n RESEND_FROM?: string;\n // Google OAuth 仍然兼容 Cloudflare Secret 作为兜底。\n // 实际生效值以 app_settings.google_client_id / google_client_secret 为准。\n GOOGLE_CLIENT_ID?: string;\n GOOGLE_CLIENT_SECRET?: string;\n /** Turnstile site key fallback when not stored in app_settings */\n TURNSTILE_SITE_KEY?: string;\n /** Turnstile secret — set via `wrangler secret put TURNSTILE_SECRET_KEY` */\n TURNSTILE_SECRET_KEY?: string;\n /** Notion integration token for the blog data source */\n NOTION_TOKEN?: string;\n /** Notion data source ID used by dataSources.query */\n NOTION_DATA_SOURCE_ID?: string;\n /** Notion data source ID for the public movie catalog */\n NOTION_MOVIES_DATA_SOURCE_ID?: string;\n /** Notion data source ID for localized movie copy */\n NOTION_MOVIE_TRANSLATIONS_DATA_SOURCE_ID?: string;\n /** Optional Notion API base URL for tests or proxies */\n NOTION_API_BASE_URL?: string;\n /** Optional Notion edit URL for admin handoff screens */\n NOTION_EDIT_BASE_URL?: string;\n /** Optional webhook verification token for Notion invalidation */\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n};\n\n// 强制类型:vinext 把 env 类型放在 env.d.ts(interface VinextEnv extends Env),\n// 但 TS server 经常解析不到。运行时一定有 DB,类型断言保证编译通过。\nexport const workerEnv = env as unknown as AppEnv;\n","import { workerEnv } from \"../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","export type RuntimeId = \"cloudflare-workers\";\n\nexport type RuntimeKind = \"cloudflare\";\n\nexport interface RuntimeSelection {\n kind: RuntimeKind;\n runtimeId: RuntimeId;\n}\n\nfunction hasCloudflareBindings(env: unknown): boolean {\n if (!env || typeof env !== \"object\") return false;\n const record = env as Record<string, unknown>;\n return (\n \"DB\" in record ||\n \"ASSETS_BUCKET\" in record ||\n \"R2\" in record ||\n \"IMAGES\" in record ||\n \"CONTENT_CACHE\" in record\n );\n}\n\nexport function selectRuntime(env: unknown): RuntimeSelection {\n if (hasCloudflareBindings(env)) {\n return { kind: \"cloudflare\", runtimeId: \"cloudflare-workers\" };\n }\n throw new Error(\n \"No supported runtime detected. Expected Cloudflare Workers bindings \" +\n \"(DB, ASSETS_BUCKET, R2, IMAGES, or CONTENT_CACHE).\",\n );\n}\n\nexport function currentRuntimeId(): RuntimeId {\n return \"cloudflare-workers\";\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","options","env","getRuntimePlatform","getPublicCache"]}
|
|
1
|
+
{"version":3,"sources":["../../src/platform/runtime.ts","../../src/platform/capabilities.ts","../../src/util/env.ts","../../src/platform/cloudflare-runtime.ts","../../src/platform/selection.ts","../../src/platform/current.ts"],"sourcesContent":["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 type { RuntimeId } from \"./selection\";\n\nexport type RuntimeCapability =\n | \"server-rendering\"\n | \"edge-cache\"\n | \"relational-storage\"\n | \"object-storage\"\n | \"image-optimization\"\n | \"secrets\"\n | \"observability\";\n\nexport type RuntimeAdapterDefinition = {\n id: RuntimeId;\n label: string;\n status: \"active\" | \"partial\" | \"planned\";\n services: {\n compute: string;\n relationalStorage: string;\n objectStorage: string;\n imageOptimization: string;\n cache: string;\n authStorage: string;\n };\n capabilities: readonly RuntimeCapability[];\n};\n\nexport type RuntimeServiceStatus = {\n database: boolean;\n objectStorage: boolean;\n imageTransformer: boolean;\n publicCache: boolean;\n};\n\nexport const cloudflareWorkersAdapter: RuntimeAdapterDefinition = {\n id: \"cloudflare-workers\",\n label: \"Cloudflare Workers + D1\",\n status: \"active\",\n services: {\n compute: \"Cloudflare Workers via vinext\",\n relationalStorage: \"D1 through the runtime SQL adapter\",\n objectStorage: \"R2\",\n imageOptimization: \"Cloudflare Images\",\n cache: \"vinext CDN/data adapters and caches.default for media\",\n authStorage: \"D1 users and signed cookies\",\n },\n capabilities: [\n \"server-rendering\",\n \"edge-cache\",\n \"relational-storage\",\n \"object-storage\",\n \"image-optimization\",\n \"secrets\",\n \"observability\",\n ],\n};\n\nexport const runtimeAdapters = [cloudflareWorkersAdapter] as const;\n\nexport function getRuntimeAdapter(id: RuntimeAdapterDefinition[\"id\"]) {\n return runtimeAdapters.find((adapter) => adapter.id === id);\n}\n\nexport function runtimeServiceStatus(\n platform: {\n database: unknown;\n objectStorage: unknown;\n imageTransformer: unknown;\n publicCache: unknown;\n }\n): RuntimeServiceStatus {\n return {\n database: Boolean(platform.database),\n objectStorage: Boolean(platform.objectStorage),\n imageTransformer: Boolean(platform.imageTransformer),\n publicCache: Boolean(platform.publicCache),\n };\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 { 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","export type RuntimeId = \"cloudflare-workers\";\n\nexport type RuntimeKind = \"cloudflare\";\n\nexport interface RuntimeSelection {\n kind: RuntimeKind;\n runtimeId: RuntimeId;\n}\n\nfunction hasCloudflareBindings(env: unknown): boolean {\n if (!env || typeof env !== \"object\") return false;\n const record = env as Record<string, unknown>;\n return (\n \"DB\" in record ||\n \"ASSETS_BUCKET\" in record ||\n \"R2\" in record ||\n \"IMAGES\" in record ||\n \"CONTENT_CACHE\" in record\n );\n}\n\nexport function selectRuntime(env: unknown): RuntimeSelection {\n if (hasCloudflareBindings(env)) {\n return { kind: \"cloudflare\", runtimeId: \"cloudflare-workers\" };\n }\n throw new Error(\n \"No supported runtime detected. Expected Cloudflare Workers bindings \" +\n \"(DB, ASSETS_BUCKET, R2, IMAGES, or CONTENT_CACHE).\",\n );\n}\n\nexport function currentRuntimeId(): RuntimeId {\n return \"cloudflare-workers\";\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","options","env","getRuntimePlatform","getPublicCache"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/util/env.ts","../../src/platform/runtime.ts","../../src/platform/cloudflare-runtime.ts","../../src/platform/current.ts","../../src/storage/r2.ts"],"sourcesContent":["// 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","// Object storage helpers for file upload, list, and delete.\n//\n// Cloudflare Workers uses R2 as the backing store; the abstraction in\n// platform/runtime.ts hides the concrete binding.\n//\n// File naming: <randomUUID>.<ext> — prevents collisions and makes keys\n// non-guessable.\n// Public access: today every read goes through the worker's\n// /api/files/[key] proxy (avoids the complexity of a public bucket).\n// To serve directly from R2: make the bucket public and attach a\n// custom domain.\n\nimport { getRuntimePlatform } from \"../platform/current\";\n\nexport type UploadResult = {\n key: string;\n url: string;\n size: number;\n contentType: string;\n};\n\nexport function buildAssetUrl(kind: \"cdn\" | \"files\", key: string): string {\n // Encode each path segment separately so nested R2 keys remain routable.\n const safeKey = key\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => encodeURIComponent(segment))\n .join(\"/\");\n return `/api/${kind}/${safeKey}`;\n}\n\n// Whitelisted MIME types — keeps the bucket from becoming a dumping\n// ground. The browser can occasionally send an empty type, so the\n// extension is also checked.\nconst ALLOWED = new Set([\n \"image/jpeg\",\n \"image/jpg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/avif\",\n \"application/pdf\",\n \"text/plain\",\n]);\n\nconst ALLOWED_IMAGE_EXT = /\\.(jpe?g|png|gif|webp|avif|svg)$/i;\n\n// 100 MB cap (worker request body limit).\nconst MAX_SIZE = 100 * 1024 * 1024;\n\nexport async function uploadFile(\n file: File,\n prefix = \"uploads\"\n): Promise<UploadResult> {\n const storage = getRuntimePlatform().objectStorage;\n if (!storage) {\n throw new Error(\"Object storage binding not configured\");\n }\n if (file.size > MAX_SIZE) {\n throw new Error(`File too large: ${file.size} bytes (max ${MAX_SIZE})`);\n }\n // Belt and suspenders: pass the MIME whitelist OR a matching extension.\n if (!ALLOWED.has(file.type) && !ALLOWED_IMAGE_EXT.test(file.name)) {\n throw new Error(`Unsupported file type: ${file.type || \"(empty)\"}`);\n }\n\n // Random key (non-guessable).\n const ext = file.name.split(\".\").pop() || \"bin\";\n const rand = crypto.randomUUID().replace(/-/g, \"\").slice(0, 16);\n const date = new Date().toISOString().slice(0, 10);\n const key = `${prefix}/${date}/${rand}.${ext}`;\n\n // Write to object storage with content-type and immutable cache headers.\n await storage.put(key, file, {\n contentType: file.type,\n cacheControl: \"public, max-age=31536000, immutable\",\n metadata: {\n originalName: file.name.slice(0, 100),\n uploadedAt: new Date().toISOString(),\n },\n });\n\n return {\n key,\n // Images go through the CDN (auto WebP/AVIF optimization); other\n // files go through the raw /api/files proxy.\n url: file.type.startsWith(\"image/\")\n ? buildAssetUrl(\"cdn\", key)\n : buildAssetUrl(\"files\", key),\n size: file.size,\n contentType: file.type,\n };\n}\n\nexport async function deleteFile(key: string): Promise<void> {\n const storage = getRuntimePlatform().objectStorage;\n if (!storage) return;\n await storage.delete(key);\n}\n\nexport async function listFiles(prefix = \"uploads\", limit = 50) {\n const storage = getRuntimePlatform().objectStorage;\n if (!storage) return [];\n const listed = await storage.list({ prefix, limit });\n return listed.map((object) => ({\n key: object.key,\n size: object.size,\n uploaded: object.uploaded.toISOString(),\n url: buildAssetUrl(\"files\", object.key),\n }));\n}\n"],"mappings":"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","names":["env","options","getRuntimePlatform","getRuntimePlatform"]}
|
|
1
|
+
{"version":3,"sources":["../../src/util/env.ts","../../src/platform/runtime.ts","../../src/platform/cloudflare-runtime.ts","../../src/platform/current.ts","../../src/storage/r2.ts"],"sourcesContent":["// 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","// Object storage helpers for file upload, list, and delete.\n//\n// Cloudflare Workers uses R2 as the backing store; the abstraction in\n// platform/runtime.ts hides the concrete binding.\n//\n// File naming: <randomUUID>.<ext> — prevents collisions and makes keys\n// non-guessable.\n// Public access: today every read goes through the worker's\n// /api/files/[key] proxy (avoids the complexity of a public bucket).\n// To serve directly from R2: make the bucket public and attach a\n// custom domain.\n\nimport { getRuntimePlatform } from \"../platform/current\";\n\nexport type UploadResult = {\n key: string;\n url: string;\n size: number;\n contentType: string;\n};\n\nexport function buildAssetUrl(kind: \"cdn\" | \"files\", key: string): string {\n // Encode each path segment separately so nested R2 keys remain routable.\n const safeKey = key\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => encodeURIComponent(segment))\n .join(\"/\");\n return `/api/${kind}/${safeKey}`;\n}\n\n// Whitelisted MIME types — keeps the bucket from becoming a dumping\n// ground. The browser can occasionally send an empty type, so the\n// extension is also checked.\nconst ALLOWED = new Set([\n \"image/jpeg\",\n \"image/jpg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/avif\",\n \"application/pdf\",\n \"text/plain\",\n]);\n\nconst ALLOWED_IMAGE_EXT = /\\.(jpe?g|png|gif|webp|avif|svg)$/i;\n\n// 100 MB cap (worker request body limit).\nconst MAX_SIZE = 100 * 1024 * 1024;\n\nexport async function uploadFile(\n file: File,\n prefix = \"uploads\"\n): Promise<UploadResult> {\n const storage = getRuntimePlatform().objectStorage;\n if (!storage) {\n throw new Error(\"Object storage binding not configured\");\n }\n if (file.size > MAX_SIZE) {\n throw new Error(`File too large: ${file.size} bytes (max ${MAX_SIZE})`);\n }\n // Belt and suspenders: pass the MIME whitelist OR a matching extension.\n if (!ALLOWED.has(file.type) && !ALLOWED_IMAGE_EXT.test(file.name)) {\n throw new Error(`Unsupported file type: ${file.type || \"(empty)\"}`);\n }\n\n // Random key (non-guessable).\n const ext = file.name.split(\".\").pop() || \"bin\";\n const rand = crypto.randomUUID().replace(/-/g, \"\").slice(0, 16);\n const date = new Date().toISOString().slice(0, 10);\n const key = `${prefix}/${date}/${rand}.${ext}`;\n\n // Write to object storage with content-type and immutable cache headers.\n await storage.put(key, file, {\n contentType: file.type,\n cacheControl: \"public, max-age=31536000, immutable\",\n metadata: {\n originalName: file.name.slice(0, 100),\n uploadedAt: new Date().toISOString(),\n },\n });\n\n return {\n key,\n // Images go through the CDN (auto WebP/AVIF optimization); other\n // files go through the raw /api/files proxy.\n url: file.type.startsWith(\"image/\")\n ? buildAssetUrl(\"cdn\", key)\n : buildAssetUrl(\"files\", key),\n size: file.size,\n contentType: file.type,\n };\n}\n\nexport async function deleteFile(key: string): Promise<void> {\n const storage = getRuntimePlatform().objectStorage;\n if (!storage) return;\n await storage.delete(key);\n}\n\nexport async function listFiles(prefix = \"uploads\", limit = 50) {\n const storage = getRuntimePlatform().objectStorage;\n if (!storage) return [];\n const listed = await storage.list({ prefix, limit });\n return listed.map((object) => ({\n key: object.key,\n size: object.size,\n uploaded: object.uploaded.toISOString(),\n url: buildAssetUrl(\"files\", object.key),\n }));\n}\n"],"mappings":"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","names":["env","options","getRuntimePlatform","getRuntimePlatform"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/storage/routes/cdn.ts","../../../src/util/env.ts","../../../src/platform/runtime.ts","../../../src/platform/cloudflare-runtime.ts","../../../src/platform/current.ts"],"sourcesContent":["// storage/routes/cdn.ts\n//\n// GET /api/cdn/[...key] - 从对象存储取原图,调用运行时图片转换转 WebP/AVIF 后返回\n//\n// 用 catch-all 参数兼容两种 URL:\n// 1. /api/cdn/uploads/2026-06-06/file.jpg\n// 2. /api/cdn/uploads%2F2026-06-06%2Ffile.jpg\n//\n// Like the other storage route, the package exports a route object that\n// exposes both a Next.js handler and a worker-friendly single-arg\n// handler. The body is otherwise identical to the original starter\n// implementation.\n\nimport { NextResponse } from \"next/server\";\nimport {\n type StoredObject,\n} from \"../../platform/runtime\";\nimport { getRuntimePlatform } from \"../../platform/current\";\n\nexport const dynamic = \"force-dynamic\";\n\nconst DEFAULT_WIDTH = 1200;\nconst MAX_WIDTH = 2400;\nconst DEFAULT_QUALITY = 75;\nconst MIN_QUALITY = 40;\nconst MAX_QUALITY = 85;\n\ntype Props = {\n params: Promise<{ key: string[] }>;\n};\n\nexport const cdnRoute = {\n async GET(request: Request, props: Props) {\n const { key } = await props.params;\n return cdnRoute.handle(new Request(buildInternalUrl(request, key)));\n },\n async handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const key = readKeyFromPathname(url.pathname);\n if (!key) {\n return NextResponse.json({ error: \"Invalid key\" }, { status: 400 });\n }\n\n if (key.includes(\"..\") || key.startsWith(\"/\")) {\n return NextResponse.json({ error: \"Invalid key\" }, { status: 400 });\n }\n\n const platform = getRuntimePlatform();\n const storage = platform.objectStorage;\n if (!storage) {\n return NextResponse.json(\n { error: \"Object storage not configured\" },\n { status: 503 }\n );\n }\n\n const object = await storage.get(key);\n if (!object) {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n }\n\n const accept = request.headers.get(\"accept\") ?? \"\";\n const isImage = object.contentType?.startsWith(\"image/\") ?? false;\n\n if (!isImage) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": \"non-image\",\n \"X-Debug-Cdn-Key\": key,\n });\n }\n\n let outputFormat: \"image/avif\" | \"image/webp\" | null = null;\n let outputQuality: number | undefined = undefined;\n\n if (accept.includes(\"image/avif\")) {\n outputFormat = \"image/avif\";\n outputQuality = 60;\n } else if (accept.includes(\"image/webp\")) {\n outputFormat = \"image/webp\";\n outputQuality = 75;\n }\n\n const isSvg = object.contentType === \"image/svg+xml\";\n if (!outputFormat || isSvg || !platform.imageTransformer) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": isSvg\n ? \"svg-bypass\"\n : !platform.imageTransformer\n ? \"transformer-bypass\"\n : \"format-bypass\",\n \"X-Debug-Cdn-Accept\": accept.includes(\"image/avif\")\n ? \"avif\"\n : accept.includes(\"image/webp\")\n ? \"webp\"\n : \"other\",\n \"X-Debug-Cdn-Key\": key,\n });\n }\n\n try {\n const width = clampInt(\n url.searchParams.get(\"w\"),\n 64,\n MAX_WIDTH,\n DEFAULT_WIDTH\n );\n const quality = clampInt(\n url.searchParams.get(\"q\"),\n MIN_QUALITY,\n MAX_QUALITY,\n outputQuality ?? DEFAULT_QUALITY\n );\n\n const result = await platform.imageTransformer.transform(object.body, {\n width,\n format: outputFormat,\n quality,\n });\n\n return new Response(result.body, {\n headers: {\n \"Content-Type\": result.contentType,\n \"Cache-Control\": \"public, max-age=31536000, immutable\",\n Vary: \"Accept\",\n \"X-Debug-Cdn-Branch\": \"transformed\",\n \"X-Debug-Cdn-Key\": key,\n \"X-Optimized-Width\": String(width),\n \"X-Optimized-Quality\": String(quality),\n \"X-Original-Format\": object.contentType ?? \"unknown\",\n \"X-Optimized-Format\": outputFormat,\n },\n });\n } catch (e) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": \"transform-error-fallback\",\n \"X-Debug-Cdn-Key\": key,\n \"X-Debug-Cdn-Error\": e instanceof Error ? e.name : \"unknown\",\n });\n }\n },\n};\n\nfunction buildInternalUrl(request: Request, keyParts: string[]) {\n const url = new URL(request.url);\n url.pathname = `/api/cdn/${keyParts.map(encodeURIComponent).join(\"/\")}`;\n return url.toString();\n}\n\nfunction readKeyFromPathname(pathname: string): string | null {\n const prefix = \"/api/cdn/\";\n if (!pathname.startsWith(prefix)) return null;\n const encoded = pathname.slice(prefix.length);\n if (!encoded) return null;\n return decodeURIComponent(encoded);\n}\n\nfunction clampInt(\n value: string | null,\n min: number,\n max: number,\n fallback: number\n) {\n const parsed = Number.parseInt(value ?? \"\", 10);\n if (!Number.isFinite(parsed)) {\n return fallback;\n }\n\n return Math.max(min, Math.min(max, parsed));\n}\n\nfunction streamObject(\n object: StoredObject,\n extraHeaders?: Record<string, string>\n): Response {\n const headers = new Headers();\n if (object.contentType) {\n headers.set(\"Content-Type\", object.contentType);\n }\n headers.set(\"Cache-Control\", \"public, max-age=31536000, immutable\");\n headers.set(\"Content-Length\", String(object.size));\n if (object.etag) headers.set(\"ETag\", object.etag);\n for (const [key, value] of Object.entries(extraHeaders ?? {})) {\n headers.set(key, value);\n }\n return new Response(object.body, { headers });\n}\n\n// Top-level aliases for callers that prefer a flat signature (e.g. the\n// Next.js `app/api/.../route.ts` delegates).\nexport const GET = cdnRoute.GET;\n\n/**\n * Worker-friendly single-arg handler. Used by the Cloudflare Workers\n * bootstrap in `@notionx/core/worker`. Equivalent to\n * `cdnRoute.handle`.\n */\nexport async function cdnRouteHandle(request: Request): Promise<Response> {\n return cdnRoute.handle(request);\n}\n","// 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","options","getRuntimePlatform","getRuntimePlatform"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/storage/routes/cdn.ts","../../../src/util/env.ts","../../../src/platform/runtime.ts","../../../src/platform/cloudflare-runtime.ts","../../../src/platform/current.ts"],"sourcesContent":["// storage/routes/cdn.ts\n//\n// GET /api/cdn/[...key] - 从对象存储取原图,调用运行时图片转换转 WebP/AVIF 后返回\n//\n// 用 catch-all 参数兼容两种 URL:\n// 1. /api/cdn/uploads/2026-06-06/file.jpg\n// 2. /api/cdn/uploads%2F2026-06-06%2Ffile.jpg\n//\n// Like the other storage route, the package exports a route object that\n// exposes both a Next.js handler and a worker-friendly single-arg\n// handler. The body is otherwise identical to the original starter\n// implementation.\n\nimport { NextResponse } from \"next/server\";\nimport {\n type StoredObject,\n} from \"../../platform/runtime\";\nimport { getRuntimePlatform } from \"../../platform/current\";\n\nexport const dynamic = \"force-dynamic\";\n\nconst DEFAULT_WIDTH = 1200;\nconst MAX_WIDTH = 2400;\nconst DEFAULT_QUALITY = 75;\nconst MIN_QUALITY = 40;\nconst MAX_QUALITY = 85;\n\ntype Props = {\n params: Promise<{ key: string[] }>;\n};\n\nexport const cdnRoute = {\n async GET(request: Request, props: Props) {\n const { key } = await props.params;\n return cdnRoute.handle(new Request(buildInternalUrl(request, key)));\n },\n async handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const key = readKeyFromPathname(url.pathname);\n if (!key) {\n return NextResponse.json({ error: \"Invalid key\" }, { status: 400 });\n }\n\n if (key.includes(\"..\") || key.startsWith(\"/\")) {\n return NextResponse.json({ error: \"Invalid key\" }, { status: 400 });\n }\n\n const platform = getRuntimePlatform();\n const storage = platform.objectStorage;\n if (!storage) {\n return NextResponse.json(\n { error: \"Object storage not configured\" },\n { status: 503 }\n );\n }\n\n const object = await storage.get(key);\n if (!object) {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n }\n\n const accept = request.headers.get(\"accept\") ?? \"\";\n const isImage = object.contentType?.startsWith(\"image/\") ?? false;\n\n if (!isImage) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": \"non-image\",\n \"X-Debug-Cdn-Key\": key,\n });\n }\n\n let outputFormat: \"image/avif\" | \"image/webp\" | null = null;\n let outputQuality: number | undefined = undefined;\n\n if (accept.includes(\"image/avif\")) {\n outputFormat = \"image/avif\";\n outputQuality = 60;\n } else if (accept.includes(\"image/webp\")) {\n outputFormat = \"image/webp\";\n outputQuality = 75;\n }\n\n const isSvg = object.contentType === \"image/svg+xml\";\n if (!outputFormat || isSvg || !platform.imageTransformer) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": isSvg\n ? \"svg-bypass\"\n : !platform.imageTransformer\n ? \"transformer-bypass\"\n : \"format-bypass\",\n \"X-Debug-Cdn-Accept\": accept.includes(\"image/avif\")\n ? \"avif\"\n : accept.includes(\"image/webp\")\n ? \"webp\"\n : \"other\",\n \"X-Debug-Cdn-Key\": key,\n });\n }\n\n try {\n const width = clampInt(\n url.searchParams.get(\"w\"),\n 64,\n MAX_WIDTH,\n DEFAULT_WIDTH\n );\n const quality = clampInt(\n url.searchParams.get(\"q\"),\n MIN_QUALITY,\n MAX_QUALITY,\n outputQuality ?? DEFAULT_QUALITY\n );\n\n const result = await platform.imageTransformer.transform(object.body, {\n width,\n format: outputFormat,\n quality,\n });\n\n return new Response(result.body, {\n headers: {\n \"Content-Type\": result.contentType,\n \"Cache-Control\": \"public, max-age=31536000, immutable\",\n Vary: \"Accept\",\n \"X-Debug-Cdn-Branch\": \"transformed\",\n \"X-Debug-Cdn-Key\": key,\n \"X-Optimized-Width\": String(width),\n \"X-Optimized-Quality\": String(quality),\n \"X-Original-Format\": object.contentType ?? \"unknown\",\n \"X-Optimized-Format\": outputFormat,\n },\n });\n } catch (e) {\n return streamObject(object, {\n \"X-Debug-Cdn-Branch\": \"transform-error-fallback\",\n \"X-Debug-Cdn-Key\": key,\n \"X-Debug-Cdn-Error\": e instanceof Error ? e.name : \"unknown\",\n });\n }\n },\n};\n\nfunction buildInternalUrl(request: Request, keyParts: string[]) {\n const url = new URL(request.url);\n url.pathname = `/api/cdn/${keyParts.map(encodeURIComponent).join(\"/\")}`;\n return url.toString();\n}\n\nfunction readKeyFromPathname(pathname: string): string | null {\n const prefix = \"/api/cdn/\";\n if (!pathname.startsWith(prefix)) return null;\n const encoded = pathname.slice(prefix.length);\n if (!encoded) return null;\n return decodeURIComponent(encoded);\n}\n\nfunction clampInt(\n value: string | null,\n min: number,\n max: number,\n fallback: number\n) {\n const parsed = Number.parseInt(value ?? \"\", 10);\n if (!Number.isFinite(parsed)) {\n return fallback;\n }\n\n return Math.max(min, Math.min(max, parsed));\n}\n\nfunction streamObject(\n object: StoredObject,\n extraHeaders?: Record<string, string>\n): Response {\n const headers = new Headers();\n if (object.contentType) {\n headers.set(\"Content-Type\", object.contentType);\n }\n headers.set(\"Cache-Control\", \"public, max-age=31536000, immutable\");\n headers.set(\"Content-Length\", String(object.size));\n if (object.etag) headers.set(\"ETag\", object.etag);\n for (const [key, value] of Object.entries(extraHeaders ?? {})) {\n headers.set(key, value);\n }\n return new Response(object.body, { headers });\n}\n\n// Top-level aliases for callers that prefer a flat signature (e.g. the\n// Next.js `app/api/.../route.ts` delegates).\nexport const GET = cdnRoute.GET;\n\n/**\n * Worker-friendly single-arg handler. Used by the Cloudflare Workers\n * bootstrap in `@notionx/core/worker`. Equivalent to\n * `cdnRoute.handle`.\n */\nexport async function cdnRouteHandle(request: Request): Promise<Response> {\n return cdnRoute.handle(request);\n}\n","// 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","options","getRuntimePlatform","getRuntimePlatform"]}
|