@notionx/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/dist/admin/index.d.ts +137 -0
  2. package/dist/admin/index.js +206 -0
  3. package/dist/admin/index.js.map +1 -0
  4. package/dist/admin/pages/index.d.ts +324 -0
  5. package/dist/admin/pages/index.js +827 -0
  6. package/dist/admin/pages/index.js.map +1 -0
  7. package/dist/auth/auth-pages/forgot-password.d.ts +20 -0
  8. package/dist/auth/auth-pages/forgot-password.js +70 -0
  9. package/dist/auth/auth-pages/forgot-password.js.map +1 -0
  10. package/dist/auth/auth-pages/index.d.ts +6 -0
  11. package/dist/auth/auth-pages/index.js +342 -0
  12. package/dist/auth/auth-pages/index.js.map +1 -0
  13. package/dist/auth/auth-pages/login.d.ts +30 -0
  14. package/dist/auth/auth-pages/login.js +125 -0
  15. package/dist/auth/auth-pages/login.js.map +1 -0
  16. package/dist/auth/auth-pages/register.d.ts +17 -0
  17. package/dist/auth/auth-pages/register.js +81 -0
  18. package/dist/auth/auth-pages/register.js.map +1 -0
  19. package/dist/auth/auth-pages/reset-password.d.ts +18 -0
  20. package/dist/auth/auth-pages/reset-password.js +72 -0
  21. package/dist/auth/auth-pages/reset-password.js.map +1 -0
  22. package/dist/auth/index.d.ts +72 -0
  23. package/dist/auth/index.js +1011 -0
  24. package/dist/auth/index.js.map +1 -0
  25. package/dist/auth/passwords.d.ts +6 -0
  26. package/dist/auth/passwords.js +79 -0
  27. package/dist/auth/passwords.js.map +1 -0
  28. package/dist/auth/rate-limit.d.ts +28 -0
  29. package/dist/auth/rate-limit.js +245 -0
  30. package/dist/auth/rate-limit.js.map +1 -0
  31. package/dist/auth/routes/google-callback.d.ts +6 -0
  32. package/dist/auth/routes/google-callback.js +404 -0
  33. package/dist/auth/routes/google-callback.js.map +1 -0
  34. package/dist/auth/routes/google.d.ts +6 -0
  35. package/dist/auth/routes/google.js +250 -0
  36. package/dist/auth/routes/google.js.map +1 -0
  37. package/dist/auth/routes/index.d.ts +22 -0
  38. package/dist/auth/routes/index.js +619 -0
  39. package/dist/auth/routes/index.js.map +1 -0
  40. package/dist/auth/routes/verify-email.d.ts +6 -0
  41. package/dist/auth/routes/verify-email.js +317 -0
  42. package/dist/auth/routes/verify-email.js.map +1 -0
  43. package/dist/auth/routes/viewer.d.ts +6 -0
  44. package/dist/auth/routes/viewer.js +372 -0
  45. package/dist/auth/routes/viewer.js.map +1 -0
  46. package/dist/auth/session.d.ts +9 -0
  47. package/dist/auth/session.js +1 -0
  48. package/dist/auth/session.js.map +1 -0
  49. package/dist/auth/turnstile.d.ts +20 -0
  50. package/dist/auth/turnstile.js +301 -0
  51. package/dist/auth/turnstile.js.map +1 -0
  52. package/dist/auth/user-session.d.ts +42 -0
  53. package/dist/auth/user-session.js +419 -0
  54. package/dist/auth/user-session.js.map +1 -0
  55. package/dist/auth/users.d.ts +112 -0
  56. package/dist/auth/users.js +558 -0
  57. package/dist/auth/users.js.map +1 -0
  58. package/dist/bootstrap-CN2g76M6.d.ts +67 -0
  59. package/dist/cache/index.d.ts +6 -0
  60. package/dist/cache/index.js +47 -0
  61. package/dist/cache/index.js.map +1 -0
  62. package/dist/content/admin-summary.d.ts +24 -0
  63. package/dist/content/admin-summary.js +36 -0
  64. package/dist/content/admin-summary.js.map +1 -0
  65. package/dist/content/index.d.ts +9 -0
  66. package/dist/content/index.js +473 -0
  67. package/dist/content/index.js.map +1 -0
  68. package/dist/content/models.d.ts +69 -0
  69. package/dist/content/models.js +24 -0
  70. package/dist/content/models.js.map +1 -0
  71. package/dist/content/prewarm.d.ts +28 -0
  72. package/dist/content/prewarm.js +56 -0
  73. package/dist/content/prewarm.js.map +1 -0
  74. package/dist/content/revalidate.d.ts +37 -0
  75. package/dist/content/revalidate.js +170 -0
  76. package/dist/content/revalidate.js.map +1 -0
  77. package/dist/content/search-index.d.ts +54 -0
  78. package/dist/content/search-index.js +172 -0
  79. package/dist/content/search-index.js.map +1 -0
  80. package/dist/content/search.d.ts +8 -0
  81. package/dist/content/search.js +57 -0
  82. package/dist/content/search.js.map +1 -0
  83. package/dist/doctor/cli.d.ts +1 -0
  84. package/dist/doctor/cli.js +360 -0
  85. package/dist/doctor/cli.js.map +1 -0
  86. package/dist/doctor/index.d.ts +139 -0
  87. package/dist/doctor/index.js +289 -0
  88. package/dist/doctor/index.js.map +1 -0
  89. package/dist/email/index.d.ts +38 -0
  90. package/dist/email/index.js +126 -0
  91. package/dist/email/index.js.map +1 -0
  92. package/dist/env-C5qu-0R-.d.ts +35 -0
  93. package/dist/hooks/index.d.ts +2 -0
  94. package/dist/hooks/index.js +1 -0
  95. package/dist/hooks/index.js.map +1 -0
  96. package/dist/i18n/index.d.ts +26 -0
  97. package/dist/i18n/index.js +73 -0
  98. package/dist/i18n/index.js.map +1 -0
  99. package/dist/index.d.ts +8 -0
  100. package/dist/index.js +1281 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/internal/admin/index.d.ts +75 -0
  103. package/dist/internal/admin/index.js +365 -0
  104. package/dist/internal/admin/index.js.map +1 -0
  105. package/dist/media/index.d.ts +24 -0
  106. package/dist/media/index.js +86 -0
  107. package/dist/media/index.js.map +1 -0
  108. package/dist/media/routes/index.d.ts +1 -0
  109. package/dist/media/routes/index.js +585 -0
  110. package/dist/media/routes/index.js.map +1 -0
  111. package/dist/media/routes/notion-media.d.ts +19 -0
  112. package/dist/media/routes/notion-media.js +588 -0
  113. package/dist/media/routes/notion-media.js.map +1 -0
  114. package/dist/middleware.d.ts +95 -0
  115. package/dist/middleware.js +79 -0
  116. package/dist/middleware.js.map +1 -0
  117. package/dist/notion/block-text.d.ts +5 -0
  118. package/dist/notion/block-text.js +37 -0
  119. package/dist/notion/block-text.js.map +1 -0
  120. package/dist/notion/blocks.d.ts +24 -0
  121. package/dist/notion/blocks.js +46 -0
  122. package/dist/notion/blocks.js.map +1 -0
  123. package/dist/notion/client.d.ts +7 -0
  124. package/dist/notion/client.js +13 -0
  125. package/dist/notion/client.js.map +1 -0
  126. package/dist/notion/config.d.ts +25 -0
  127. package/dist/notion/config.js +147 -0
  128. package/dist/notion/config.js.map +1 -0
  129. package/dist/notion/content-cache.d.ts +45 -0
  130. package/dist/notion/content-cache.js +166 -0
  131. package/dist/notion/content-cache.js.map +1 -0
  132. package/dist/notion/generic-source.d.ts +61 -0
  133. package/dist/notion/generic-source.js +408 -0
  134. package/dist/notion/generic-source.js.map +1 -0
  135. package/dist/notion/index.d.ts +13 -0
  136. package/dist/notion/index.js +1278 -0
  137. package/dist/notion/index.js.map +1 -0
  138. package/dist/notion/mappers.d.ts +1 -0
  139. package/dist/notion/mappers.js +152 -0
  140. package/dist/notion/mappers.js.map +1 -0
  141. package/dist/notion/media.d.ts +22 -0
  142. package/dist/notion/media.js +209 -0
  143. package/dist/notion/media.js.map +1 -0
  144. package/dist/notion/property-mappers.d.ts +24 -0
  145. package/dist/notion/property-mappers.js +152 -0
  146. package/dist/notion/property-mappers.js.map +1 -0
  147. package/dist/notion/routes/index.d.ts +8 -0
  148. package/dist/notion/routes/index.js +428 -0
  149. package/dist/notion/routes/index.js.map +1 -0
  150. package/dist/notion/routes/webhook.d.ts +98 -0
  151. package/dist/notion/routes/webhook.js +428 -0
  152. package/dist/notion/routes/webhook.js.map +1 -0
  153. package/dist/notion/types.d.ts +152 -0
  154. package/dist/notion/types.js +1 -0
  155. package/dist/notion/types.js.map +1 -0
  156. package/dist/notion/webhook.d.ts +83 -0
  157. package/dist/notion/webhook.js +490 -0
  158. package/dist/notion/webhook.js.map +1 -0
  159. package/dist/platform/capabilities.d.ts +34 -0
  160. package/dist/platform/capabilities.js +42 -0
  161. package/dist/platform/capabilities.js.map +1 -0
  162. package/dist/platform/current.d.ts +13 -0
  163. package/dist/platform/current.js +181 -0
  164. package/dist/platform/current.js.map +1 -0
  165. package/dist/platform/index.d.ts +5 -0
  166. package/dist/platform/index.js +269 -0
  167. package/dist/platform/index.js.map +1 -0
  168. package/dist/platform/runtime.d.ts +118 -0
  169. package/dist/platform/runtime.js +160 -0
  170. package/dist/platform/runtime.js.map +1 -0
  171. package/dist/platform/selection.d.ts +10 -0
  172. package/dist/platform/selection.js +22 -0
  173. package/dist/platform/selection.js.map +1 -0
  174. package/dist/storage/index.d.ts +17 -0
  175. package/dist/storage/index.js +218 -0
  176. package/dist/storage/index.js.map +1 -0
  177. package/dist/storage/routes/cdn.d.ts +19 -0
  178. package/dist/storage/routes/cdn.js +289 -0
  179. package/dist/storage/routes/cdn.js.map +1 -0
  180. package/dist/storage/routes/files.d.ts +27 -0
  181. package/dist/storage/routes/files.js +216 -0
  182. package/dist/storage/routes/files.js.map +1 -0
  183. package/dist/storage/routes/index.d.ts +2 -0
  184. package/dist/storage/routes/index.js +352 -0
  185. package/dist/storage/routes/index.js.map +1 -0
  186. package/dist/types-BsAcZSNX.d.ts +94 -0
  187. package/dist/types.d.ts +78 -0
  188. package/dist/types.js +1 -0
  189. package/dist/types.js.map +1 -0
  190. package/dist/util/index.d.ts +18 -0
  191. package/dist/util/index.js +48 -0
  192. package/dist/util/index.js.map +1 -0
  193. package/dist/worker/index.d.ts +6 -0
  194. package/dist/worker/index.js +1026 -0
  195. package/dist/worker/index.js.map +1 -0
  196. package/dist/worker/routes/content-prewarm.d.ts +34 -0
  197. package/dist/worker/routes/content-prewarm.js +38 -0
  198. package/dist/worker/routes/content-prewarm.js.map +1 -0
  199. package/dist/worker/routes/content-revalidate.d.ts +81 -0
  200. package/dist/worker/routes/content-revalidate.js +64 -0
  201. package/dist/worker/routes/content-revalidate.js.map +1 -0
  202. package/dist/worker/routes/health.d.ts +14 -0
  203. package/dist/worker/routes/health.js +278 -0
  204. package/dist/worker/routes/health.js.map +1 -0
  205. package/dist/worker/routes/index.d.ts +6 -0
  206. package/dist/worker/routes/index.js +373 -0
  207. package/dist/worker/routes/index.js.map +1 -0
  208. package/package.json +124 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/notion/routes/webhook.ts","../../../src/notion/client.ts","../../../src/notion/config.ts","../../../src/notion/webhook.ts","../../../src/util/env.ts","../../../src/platform/runtime.ts","../../../src/platform/cloudflare-runtime.ts","../../../src/platform/current.ts"],"sourcesContent":["// notion/routes/webhook.ts\n//\n// POST /api/notion/webhook - Notion webhook receiver\n//\n// Verifies the Notion signature, parses the event payload, and triggers\n// content revalidation for each event. The revalidation itself is\n// caller-supplied (`revalidateContentModel`) so the package does not\n// reach into the starter's content model registry. The parser is also\n// caller-supplied so the starter can inject its content models.\n//\n// The `RevalidateContentModelFromWebhookFn` type intentionally matches\n// the shape consumed by the package's content revalidate route\n// factory. Tier restrictions prevent us from importing the worker\n// route type directly, so the shape is duplicated here; both the\n// `createNotionWebhookRoute` factory and the\n// `createContentRevalidateRoute` factory agree on this contract\n// because callers wire them together.\n\nimport { NextResponse } from \"next/server\";\n\nimport {\n getStoredNotionWebhookVerificationToken,\n notionWebhookEventToRevalidateRequest,\n putStoredNotionWebhookVerificationToken,\n verifyNotionWebhookSignatureWithTokens,\n type NotionWebhookParseResult,\n type NotionPageRetriever,\n} from \"../webhook\";\nimport { getNotionWebhookVerificationToken } from \"../config\";\nimport { getRuntimePlatform } from \"../../platform/current\";\nimport type {\n KeyValueCacheAdapter,\n SqlDatabaseAdapter,\n} from \"../../platform/runtime\";\n\nexport type NotionWebhookParserFn = (\n payload: unknown,\n options?: { retrievePage?: NotionPageRetriever; lookupPages?: boolean }\n) => Promise<NotionWebhookParseResult>;\n\nexport type RevalidateContentModelFromWebhookFn = (input: {\n request: {\n modelId: string;\n pageId?: string;\n routeId?: string;\n previousRouteId?: string;\n locale?: string;\n kind?: \"publish\" | \"update\" | \"delete\";\n includeApi?: boolean;\n };\n tokenAuthorized: boolean;\n revalidatePath: (\n path: string,\n type?: \"page\" | \"layout\"\n ) => void | Promise<void>;\n contentCache?: KeyValueCacheAdapter;\n getContentCache?: () => KeyValueCacheAdapter | null;\n database?: SqlDatabaseAdapter;\n getDatabase?: () => SqlDatabaseAdapter | null;\n}) => Promise<\n | {\n ok: true;\n model: {\n id: string;\n routes: { listPath: string; detailPath: string; publicApiPath?: string };\n };\n routeId?: string;\n revalidatedPaths: string[];\n contentCache: unknown;\n searchIndex: unknown;\n }\n | {\n ok: false;\n status: 400 | 401 | 404;\n error: string;\n }\n>;\n\nexport type CreateNotionWebhookRouteOptions = {\n revalidatePath: (\n path: string,\n type?: \"page\" | \"layout\"\n ) => void | Promise<void>;\n revalidateContentModel: RevalidateContentModelFromWebhookFn;\n /**\n * The starter's parser wrapper that injects its content models.\n * Required; there is no sensible package default because the parser\n * depends on the consumer's content model registrations.\n */\n parseNotionWebhookPayload: NotionWebhookParserFn;\n getVerificationToken?: typeof getNotionWebhookVerificationToken;\n};\n\nexport function createNotionWebhookRoute(options: CreateNotionWebhookRouteOptions) {\n const parseFn = options.parseNotionWebhookPayload;\n const getToken =\n options.getVerificationToken ?? getNotionWebhookVerificationToken;\n\n return {\n async POST(request: Request) {\n return handlePost(request, options, parseFn, getToken);\n },\n async handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\") {\n return NextResponse.json(\n { ok: false, error: \"Method not allowed\" },\n { status: 405, headers: { \"Cache-Control\": \"no-store\" } }\n );\n }\n return handlePost(request, options, parseFn, getToken);\n },\n };\n}\n\nasync function handlePost(\n request: Request,\n options: CreateNotionWebhookRouteOptions,\n parseFn: NotionWebhookParserFn,\n getToken: typeof getNotionWebhookVerificationToken\n) {\n const bodyText = await request.text();\n let payload: unknown;\n try {\n payload = JSON.parse(bodyText || \"{}\");\n } catch {\n return NextResponse.json(\n { ok: false, error: \"Invalid JSON\" },\n { status: 400, headers: { \"Cache-Control\": \"no-store\" } }\n );\n }\n\n const platform = getRuntimePlatform();\n const parsed = await parseFn(payload, { lookupPages: false });\n\n if (parsed.type === \"verification\") {\n const verified = await verifyNotionWebhookSignatureWithTokens({\n body: bodyText,\n signature: request.headers.get(\"x-notion-signature\"),\n verificationTokens: [parsed.verificationToken],\n });\n if (!verified) {\n return NextResponse.json(\n { ok: false, error: \"Invalid Notion webhook verification signature\" },\n { status: 401, headers: { \"Cache-Control\": \"no-store\" } }\n );\n }\n\n const stored = await putStoredNotionWebhookVerificationToken(\n platform.keyValueCache,\n parsed.verificationToken\n );\n return NextResponse.json(\n {\n verification_token: parsed.verificationToken,\n stored,\n },\n { headers: { \"Cache-Control\": \"no-store\" } }\n );\n }\n\n const token = await getToken();\n const storedToken = await getStoredNotionWebhookVerificationToken(\n platform.keyValueCache\n );\n const verified = await verifyNotionWebhookSignatureWithTokens({\n body: bodyText,\n signature: request.headers.get(\"x-notion-signature\"),\n verificationTokens: [token, storedToken],\n });\n if (!verified) {\n return NextResponse.json(\n { ok: false, error: \"Invalid Notion webhook signature\" },\n { status: 401, headers: { \"Cache-Control\": \"no-store\" } }\n );\n }\n\n const resolved = await parseFn(payload);\n if (resolved.type === \"verification\") {\n return NextResponse.json(\n { ok: false, error: \"Unexpected verification payload\" },\n { status: 400, headers: { \"Cache-Control\": \"no-store\" } }\n );\n }\n\n const results = [];\n for (const event of resolved.events) {\n const result = await options.revalidateContentModel({\n request: notionWebhookEventToRevalidateRequest(event),\n tokenAuthorized: true,\n revalidatePath: options.revalidatePath,\n contentCache: platform.keyValueCache ?? undefined,\n getContentCache: () => platform.keyValueCache,\n database: platform.database ?? undefined,\n getDatabase: () => platform.database,\n });\n results.push({\n eventId: event.id,\n eventType: event.eventType,\n modelId: event.modelId,\n routeId: event.routeId ?? null,\n result,\n });\n }\n\n return NextResponse.json(\n {\n ok: true,\n received: parsed.events.length,\n results,\n },\n { headers: { \"Cache-Control\": \"no-store\" } }\n );\n}\n","import { Client } from \"@notionhq/client\";\nimport type { NotionClientConfig } from \"./config\";\n\nexport function createNotionClient(config: NotionClientConfig) {\n return new Client({\n auth: config.token,\n baseUrl: config.apiBaseUrl,\n notionVersion: \"2026-03-11\",\n });\n}\n","import type { NotionContentModelLike } from \"./types\";\n\ntype NotionEnv = {\n NOTION_TOKEN?: string;\n NOTION_DATA_SOURCE_ID?: string;\n NOTION_MOVIES_DATA_SOURCE_ID?: string;\n NOTION_API_BASE_URL?: string;\n NOTION_EDIT_BASE_URL?: string;\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n [key: string]: string | undefined;\n};\n\nexport const DEFAULT_NOTION_MOVIES_DATA_SOURCE_ID =\n \"371dc62d-0738-8015-a601-000bc3944fcb\";\n\nexport type NotionClientConfig = {\n token: string;\n apiBaseUrl?: string;\n};\n\nexport type NotionConfig = {\n token: string;\n dataSourceId: string;\n apiBaseUrl?: string;\n editBaseUrl?: string;\n webhookVerificationToken?: string;\n};\n\nfunction readProcessEnv(): NotionEnv {\n const env: NotionEnv = {\n NOTION_TOKEN: process.env.NOTION_TOKEN,\n NOTION_DATA_SOURCE_ID: process.env.NOTION_DATA_SOURCE_ID,\n NOTION_MOVIES_DATA_SOURCE_ID: process.env.NOTION_MOVIES_DATA_SOURCE_ID,\n NOTION_API_BASE_URL: process.env.NOTION_API_BASE_URL,\n NOTION_EDIT_BASE_URL: process.env.NOTION_EDIT_BASE_URL,\n NOTION_WEBHOOK_VERIFICATION_TOKEN:\n process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN,\n };\n\n for (const [key, value] of Object.entries(process.env)) {\n if (key.startsWith(\"NOTION_\") && typeof value === \"string\") {\n env[key] = value;\n }\n }\n\n return env;\n}\n\nasync function readWorkerEnv(): Promise<NotionEnv> {\n try {\n const mod = (await import(\n /* webpackIgnore: true */ \"cloudflare:workers\"\n )) as unknown as { env?: Record<string, unknown> };\n const env: NotionEnv = {};\n for (const [key, value] of Object.entries(mod.env ?? {})) {\n if (key.startsWith(\"NOTION_\") && typeof value === \"string\") {\n env[key] = value;\n }\n }\n return env;\n } catch {\n return {};\n }\n}\n\nfunction readString(source: NotionEnv, name: string): string | undefined {\n const value = String(source[name] ?? \"\").trim();\n return value || undefined;\n}\n\nfunction mergeEnv(...sources: NotionEnv[]): NotionEnv {\n const merged: NotionEnv = {};\n\n for (const source of sources) {\n for (const name of Object.keys(source)) {\n if (!name.startsWith(\"NOTION_\")) continue;\n const value = readString(source, name);\n if (value) merged[name] = value;\n }\n }\n\n return merged;\n}\n\nasync function readEnv(): Promise<NotionEnv> {\n const processEnv = readProcessEnv();\n return mergeEnv(await readWorkerEnv(), processEnv);\n}\n\nfunction readRequired(\n source: NotionEnv,\n name: string\n): string {\n const value = readString(source, name);\n if (!value) {\n throw new Error(`Missing required Notion env: ${name}`);\n }\n return value;\n}\n\nexport function getNotionEditBaseUrl(): string {\n return readString(readProcessEnv(), \"NOTION_EDIT_BASE_URL\") ?? \"https://www.notion.so\";\n}\n\nexport async function hasNotionConfig(): Promise<boolean> {\n const env = await readEnv();\n return Boolean(\n readString(env, \"NOTION_TOKEN\") && readString(env, \"NOTION_DATA_SOURCE_ID\")\n );\n}\n\nexport async function hasNotionMovieConfig(): Promise<boolean> {\n const env = await readEnv();\n return Boolean(readString(env, \"NOTION_TOKEN\"));\n}\n\nexport async function hasNotionModelConfig(\n model: NotionContentModelLike\n): Promise<boolean> {\n const env = await readEnv();\n return Boolean(\n readString(env, \"NOTION_TOKEN\") &&\n (readString(env, model.source.dataSourceEnv) ||\n model.source.defaultDataSourceId)\n );\n}\n\nexport async function getNotionClientConfig(): Promise<NotionClientConfig> {\n const env = await readEnv();\n return {\n token: readRequired(env, \"NOTION_TOKEN\"),\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n };\n}\n\nexport async function getNotionConfig(): Promise<NotionConfig> {\n const env = await readEnv();\n return {\n token: readRequired(env, \"NOTION_TOKEN\"),\n dataSourceId: readRequired(env, \"NOTION_DATA_SOURCE_ID\"),\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n editBaseUrl: readString(env, \"NOTION_EDIT_BASE_URL\"),\n webhookVerificationToken: readString(\n env,\n \"NOTION_WEBHOOK_VERIFICATION_TOKEN\"\n ),\n };\n}\n\nexport async function getNotionWebhookVerificationToken(): Promise<\n string | undefined\n> {\n const env = await readEnv();\n return readString(env, \"NOTION_WEBHOOK_VERIFICATION_TOKEN\");\n}\n\nexport async function getNotionMovieConfig(): Promise<NotionConfig> {\n const env = await readEnv();\n return {\n token: readRequired(env, \"NOTION_TOKEN\"),\n dataSourceId:\n readString(env, \"NOTION_MOVIES_DATA_SOURCE_ID\") ??\n DEFAULT_NOTION_MOVIES_DATA_SOURCE_ID,\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n editBaseUrl: readString(env, \"NOTION_EDIT_BASE_URL\"),\n webhookVerificationToken: readString(\n env,\n \"NOTION_WEBHOOK_VERIFICATION_TOKEN\"\n ),\n };\n}\n\nexport async function getNotionConfigForModel(\n model: NotionContentModelLike\n): Promise<NotionConfig> {\n const env = await readEnv();\n const dataSourceId =\n readString(env, model.source.dataSourceEnv) ??\n model.source.defaultDataSourceId;\n if (!dataSourceId) {\n throw new Error(`Missing required Notion env: ${model.source.dataSourceEnv}`);\n }\n\n return {\n token: readRequired(env, model.source.tokenEnv),\n dataSourceId,\n apiBaseUrl: readString(env, \"NOTION_API_BASE_URL\"),\n editBaseUrl: readString(env, \"NOTION_EDIT_BASE_URL\"),\n webhookVerificationToken: readString(\n env,\n \"NOTION_WEBHOOK_VERIFICATION_TOKEN\"\n ),\n };\n}\n","import type { KeyValueCacheAdapter } from \"../platform/runtime\";\nimport { createNotionClient } from \"./client\";\nimport { getNotionConfigForModel } from \"./config\";\nimport {\n compactNotionId,\n getRichTextProperty,\n getSelectProperty,\n isValidPublicSlug,\n} from \"./property-mappers\";\nimport type {\n NotionFieldMap,\n NotionGenericContentModel,\n NotionPageLike,\n} from \"./types\";\n\ntype JsonRecord = Record<string, unknown>;\n\ntype StoredWebhookVerificationToken = {\n token: string;\n updatedAt: string;\n};\n\nconst WEBHOOK_VERIFICATION_TOKEN_CACHE_KEY =\n \"notion:webhook:verification-token:v1\";\n\nexport type NotionWebhookParseResult =\n | { type: \"verification\"; verificationToken: string }\n | { type: \"events\"; events: NotionWebhookEvent[] };\n\nexport type NotionWebhookEvent = {\n id?: string;\n eventType: string;\n modelId: string;\n pageId?: string;\n dataSourceId?: string;\n routeId?: string;\n locale?: string;\n kind: \"publish\" | \"update\" | \"delete\";\n includeApi: boolean;\n reason: \"page\" | \"data_source\";\n};\n\n/**\n * Generic shape that callers (i.e. the starter) supply so the webhook can\n * route events to the correct content model. Each entry must expose the\n * Notion data source id that the model is bound to, and a hook to derive a\n * route id and locale from the Notion page payload.\n */\nexport type NotionWebhookModelRegistration<\n TFields extends NotionFieldMap = NotionFieldMap,\n> = NotionGenericContentModel & {\n source: { fields: TFields };\n resolveRouteId?: (page: JsonRecord) => string;\n resolveLocale?: (page: JsonRecord) => string;\n};\n\nexport type NotionPageRetriever = (\n pageId: string,\n model: NotionGenericContentModel\n) => Promise<NotionPageLike | null>;\n\nexport type NotionWebhookParseOptions<\n TFields extends NotionFieldMap = NotionFieldMap,\n> = {\n models: ReadonlyArray<NotionWebhookModelRegistration<TFields>>;\n /**\n * Optional override for resolving the data source id of a model. Defaults\n * to `process.env[model.source.dataSourceEnv] ?? model.source.defaultDataSourceId`.\n */\n getModelDataSourceId?: (model: NotionWebhookModelRegistration<TFields>) => string | null;\n};\n\nexport type InvalidationKind = \"publish\" | \"update\" | \"delete\";\n\n/**\n * Request shape consumed by the revalidation pipeline. Mirrors the starter's\n * `ContentRevalidateRequest`; duplicated here so the package does not need\n * a runtime dependency on the starter.\n */\nexport type NotionWebhookRevalidateRequest = {\n modelId: string;\n pageId?: string;\n routeId?: string;\n previousRouteId?: string;\n locale?: string;\n kind?: InvalidationKind;\n includeApi?: boolean;\n};\n\nfunction isRecord(value: unknown): value is JsonRecord {\n return Boolean(value && typeof value === \"object\" && !Array.isArray(value));\n}\n\nfunction readString(source: unknown, key: string) {\n if (!isRecord(source)) return \"\";\n const value = source[key];\n return typeof value === \"string\" ? value.trim() : \"\";\n}\n\nfunction asRecords(value: unknown) {\n if (Array.isArray(value)) return value.filter(isRecord);\n if (isRecord(value)) return [value];\n return [];\n}\n\nfunction nestedRecords(source: JsonRecord, ...keys: string[]) {\n const records: JsonRecord[] = [];\n for (const key of keys) {\n const value = source[key];\n if (isRecord(value)) records.push(value);\n }\n return records;\n}\n\nfunction firstString(...values: string[]) {\n return values.find(Boolean) ?? \"\";\n}\n\nfunction normalizeId(value: string) {\n return value.replaceAll(\"-\", \"\").toLowerCase();\n}\n\nfunction findIdByType(input: JsonRecord, type: string): string {\n const stack = [input];\n const seen = new Set<JsonRecord>();\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current || seen.has(current)) continue;\n seen.add(current);\n\n if (readString(current, \"type\") === type) {\n const id = readString(current, \"id\");\n if (id) return id;\n }\n\n for (const value of Object.values(current)) {\n if (isRecord(value)) stack.push(value);\n if (Array.isArray(value)) {\n for (const item of value) {\n if (isRecord(item)) stack.push(item);\n }\n }\n }\n }\n\n return \"\";\n}\n\nfunction findDataSourceId(input: JsonRecord): string {\n const stack = [input];\n const seen = new Set<JsonRecord>();\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (!current || seen.has(current)) continue;\n seen.add(current);\n\n const direct = firstString(\n readString(current, \"data_source_id\"),\n readString(current, \"source_id\")\n );\n if (direct) return direct;\n\n const type = readString(current, \"type\");\n if (type === \"data_source\") {\n const id = readString(current, \"id\");\n if (id) return id;\n }\n\n for (const value of Object.values(current)) {\n if (isRecord(value)) stack.push(value);\n if (Array.isArray(value)) {\n for (const item of value) {\n if (isRecord(item)) stack.push(item);\n }\n }\n }\n }\n\n return \"\";\n}\n\nfunction firstFieldName(value: string | readonly string[] | undefined) {\n if (Array.isArray(value)) return value[0];\n return value;\n}\n\nfunction defaultRouteIdFromProperties(\n model: NotionGenericContentModel,\n page: JsonRecord\n) {\n const properties = isRecord(page.properties) ? page.properties : {};\n const slugField = firstFieldName(model.source.fields.slug);\n const slug = slugField\n ? getRichTextProperty(properties, slugField).toLowerCase()\n : \"\";\n return isValidPublicSlug(slug) ? slug : \"\";\n}\n\nfunction defaultLocaleFromProperties(model: NotionGenericContentModel, page: JsonRecord) {\n const properties = isRecord(page.properties) ? page.properties : {};\n const localeField = firstFieldName(model.source.fields.locale);\n return localeField ? getSelectProperty(properties, localeField) : \"\";\n}\n\nfunction eventKind(eventType: string): \"publish\" | \"update\" | \"delete\" {\n if (eventType.includes(\".deleted\")) return \"delete\";\n if (eventType.includes(\".created\") || eventType.includes(\".undeleted\")) {\n return \"publish\";\n }\n return \"update\";\n}\n\nfunction defaultGetModelDataSourceId(\n model: NotionGenericContentModel\n): string | null {\n const fromEnv = process.env[model.source.dataSourceEnv];\n if (fromEnv) return fromEnv;\n return model.source.defaultDataSourceId ?? null;\n}\n\nfunction matchModelByDataSourceId<\n TFields extends NotionFieldMap,\n>(\n models: ReadonlyArray<NotionWebhookModelRegistration<TFields>>,\n dataSourceId: string,\n getDataSourceId: (model: NotionWebhookModelRegistration<TFields>) => string | null\n) {\n if (!dataSourceId) return null;\n const normalized = normalizeId(dataSourceId);\n return (\n models.find((model) => {\n const configured = getDataSourceId(model);\n return configured ? normalizeId(configured) === normalized : false;\n }) ?? null\n );\n}\n\nfunction findModelForEvent<\n TFields extends NotionFieldMap,\n>(\n models: ReadonlyArray<NotionWebhookModelRegistration<TFields>>,\n event: JsonRecord,\n page: JsonRecord | undefined,\n getDataSourceId: (model: NotionWebhookModelRegistration<TFields>) => string | null\n) {\n const modelId = firstString(\n readString(event, \"modelId\"),\n readString(event, \"model_id\"),\n readString(event.data, \"modelId\"),\n readString(event.data, \"model_id\")\n );\n if (modelId) {\n return models.find((model) => model.id === modelId) ?? null;\n }\n\n const dataSourceId = firstString(\n findDataSourceId(event),\n findIdByType(event, \"data_source\"),\n readString(event, \"data_source_id\"),\n readString(event.data, \"data_source_id\"),\n readString(page?.parent, \"data_source_id\"),\n readString(page?.parent, \"database_id\")\n );\n return matchModelByDataSourceId(models, dataSourceId, getDataSourceId);\n}\n\nfunction pageIdForEvent(event: JsonRecord, page?: JsonRecord) {\n return firstString(\n readString(page, \"id\"),\n findIdByType(event, \"page\"),\n readString(event, \"page_id\"),\n readString(event.data, \"page_id\")\n );\n}\n\nfunction pageForEvent(event: JsonRecord) {\n let fallback: JsonRecord | null = null;\n for (const record of [\n ...nestedRecords(isRecord(event.data) ? event.data : {}, \"page\", \"entity\"),\n ...nestedRecords(event, \"page\", \"entity\"),\n event,\n ]) {\n const type = readString(record, \"type\");\n if (type === \"page\" || readString(record, \"object\") === \"page\") {\n if (!readString(record, \"id\")) continue;\n if (isRecord(record.properties) || isRecord(record.parent)) return record;\n fallback ??= record;\n }\n }\n return fallback;\n}\n\nexport function parseNotionWebhookPayload<\n TFields extends NotionFieldMap = NotionFieldMap,\n>(\n payload: unknown,\n options: NotionWebhookParseOptions<TFields>\n): NotionWebhookParseResult {\n if (!isRecord(payload)) return { type: \"events\", events: [] };\n\n const verificationToken = readString(payload, \"verification_token\");\n if (verificationToken) {\n return { type: \"verification\", verificationToken };\n }\n\n const getDataSourceId = options.getModelDataSourceId ?? defaultGetModelDataSourceId;\n\n const events = asRecords(payload.events ?? payload.event ?? payload)\n .map((event) =>\n parseEvent(event, options.models, getDataSourceId)\n )\n .filter((event): event is NotionWebhookEvent => Boolean(event));\n\n return { type: \"events\", events };\n}\n\nfunction parseEvent<\n TFields extends NotionFieldMap,\n>(\n event: JsonRecord,\n models: ReadonlyArray<NotionWebhookModelRegistration<TFields>>,\n getDataSourceId: (model: NotionWebhookModelRegistration<TFields>) => string | null\n): NotionWebhookEvent | null {\n const eventType = firstString(readString(event, \"type\"), readString(event, \"event\"));\n if (!eventType) return null;\n const page = pageForEvent(event);\n const model = findModelForEvent(\n models,\n event,\n page ?? undefined,\n getDataSourceId\n );\n if (!model) return null;\n\n const routeId = page\n ? (model.resolveRouteId?.(page) ??\n defaultRouteIdFromProperties(model, page))\n : \"\";\n const locale = page\n ? (model.resolveLocale?.(page) ?? defaultLocaleFromProperties(model, page))\n : \"\";\n const dataSourceId = firstString(\n findDataSourceId(event),\n findIdByType(event, \"data_source\"),\n readString(event, \"data_source_id\"),\n readString(event.data, \"data_source_id\"),\n readString(page?.parent, \"data_source_id\"),\n readString(page?.parent, \"database_id\")\n );\n const dataSourceEvent =\n eventType.startsWith(\"data_source.\") || eventType.startsWith(\"database.\");\n return {\n id: readString(event, \"id\") || undefined,\n eventType,\n modelId: model.id,\n pageId: pageIdForEvent(event, page ?? undefined) || undefined,\n dataSourceId: dataSourceId || undefined,\n routeId: routeId || undefined,\n locale: locale || undefined,\n kind: eventKind(eventType),\n includeApi: true,\n reason: page && routeId ? \"page\" : dataSourceEvent ? \"data_source\" : \"page\",\n };\n}\n\nasync function defaultRetrieveNotionPage(\n pageId: string,\n model: NotionGenericContentModel\n) {\n const config = await getNotionConfigForModel(model);\n const client = createNotionClient(config);\n const page = await client.pages.retrieve({ page_id: pageId });\n return page as NotionPageLike;\n}\n\nfunction resolveWebhookEventRoute<\n TFields extends NotionFieldMap,\n>(\n event: NotionWebhookEvent,\n models: ReadonlyArray<NotionWebhookModelRegistration<TFields>>,\n retrievePage: NotionPageRetriever\n): Promise<NotionWebhookEvent> {\n if (event.routeId || !event.pageId) return Promise.resolve(event);\n if (event.kind === \"delete\") return Promise.resolve(event);\n\n const model = models.find((m) => m.id === event.modelId);\n if (!model) return Promise.resolve(event);\n\n return retrievePage(event.pageId, model)\n .then((page) => {\n if (!page) return event;\n const pageRecord = page as unknown as JsonRecord;\n const routeId =\n model.resolveRouteId?.(pageRecord) ??\n defaultRouteIdFromProperties(model, pageRecord);\n const locale =\n model.resolveLocale?.(pageRecord) ??\n defaultLocaleFromProperties(model, pageRecord);\n if (!routeId) return event;\n return {\n ...event,\n routeId,\n locale: locale || event.locale,\n reason: \"page\" as const,\n };\n })\n .catch((error) => {\n const err = error as { code?: string; status?: number; message?: string };\n console.warn(\n JSON.stringify({\n tag: \"notion_webhook_page_lookup_failed\",\n eventId: event.id,\n eventType: event.eventType,\n modelId: event.modelId,\n pageId: event.pageId,\n code: err?.code,\n status: err?.status,\n message: err?.message ?? String(error),\n })\n );\n return event;\n });\n}\n\nexport async function parseNotionWebhookPayloadWithPageLookup<\n TFields extends NotionFieldMap = NotionFieldMap,\n>(\n payload: unknown,\n options: NotionWebhookParseOptions<TFields> & {\n retrievePage?: NotionPageRetriever;\n lookupPages?: boolean;\n }\n): Promise<NotionWebhookParseResult> {\n const parsed = parseNotionWebhookPayload(payload, options);\n if (parsed.type === \"verification\") return parsed;\n if (options?.lookupPages === false) return parsed;\n\n const retrievePage = options?.retrievePage ?? defaultRetrieveNotionPage;\n return {\n type: \"events\",\n events: await Promise.all(\n parsed.events.map((event) =>\n resolveWebhookEventRoute(event, options.models, retrievePage)\n )\n ),\n };\n}\n\nexport function notionWebhookEventToRevalidateRequest(\n event: NotionWebhookEvent\n): NotionWebhookRevalidateRequest {\n return {\n modelId: event.modelId,\n pageId: event.pageId,\n routeId: event.routeId,\n locale: event.locale,\n kind: event.kind,\n includeApi: event.includeApi,\n };\n}\n\nexport async function signNotionWebhookBody(body: string, verificationToken: string) {\n const key = await crypto.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(verificationToken),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n const signature = await crypto.subtle.sign(\n \"HMAC\",\n key,\n new TextEncoder().encode(body)\n );\n return Array.from(new Uint8Array(signature))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\nfunction normalizeNotionSignature(signature: string | null) {\n const value = String(signature ?? \"\").trim();\n const prefixed = value.match(/^sha256=(.+)$/i);\n return prefixed && prefixed[1] ? prefixed[1].trim() : value;\n}\n\nexport async function verifyNotionWebhookSignature(input: {\n body: string;\n signature: string | null;\n verificationToken?: string | null;\n}) {\n const token = String(input.verificationToken ?? \"\").trim();\n const signature = normalizeNotionSignature(input.signature);\n if (!token || !signature) return false;\n const expected = await signNotionWebhookBody(input.body, token);\n if (expected.length !== signature.length) return false;\n\n let diff = 0;\n for (let index = 0; index < expected.length; index += 1) {\n diff |= expected.charCodeAt(index) ^ signature.charCodeAt(index);\n }\n return diff === 0;\n}\n\nexport async function verifyNotionWebhookSignatureWithTokens(input: {\n body: string;\n signature: string | null;\n verificationTokens: Array<string | null | undefined>;\n}) {\n const tokens = Array.from(\n new Set(input.verificationTokens.map((token) => String(token ?? \"\").trim()))\n ).filter(Boolean);\n\n for (const token of tokens) {\n if (\n await verifyNotionWebhookSignature({\n body: input.body,\n signature: input.signature,\n verificationToken: token,\n })\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction readStoredWebhookVerificationToken(value: unknown) {\n if (typeof value === \"string\") return value.trim() || null;\n if (!isRecord(value)) return null;\n\n const token = readString(value, \"token\");\n return token || null;\n}\n\nexport async function getStoredNotionWebhookVerificationToken(\n cache: KeyValueCacheAdapter | null | undefined\n) {\n if (!cache) return null;\n\n try {\n const value = await cache.get<StoredWebhookVerificationToken | string>(\n WEBHOOK_VERIFICATION_TOKEN_CACHE_KEY,\n { cacheTtl: 60 }\n );\n return readStoredWebhookVerificationToken(value);\n } catch (error) {\n console.warn(\n JSON.stringify({\n tag: \"notion_webhook_token_lookup_failed\",\n message: error instanceof Error ? error.message : String(error),\n })\n );\n return null;\n }\n}\n\nexport async function putStoredNotionWebhookVerificationToken(\n cache: KeyValueCacheAdapter | null | undefined,\n token: string\n) {\n const normalized = token.trim();\n if (!cache || !normalized) return false;\n\n await cache.put<StoredWebhookVerificationToken>(\n WEBHOOK_VERIFICATION_TOKEN_CACHE_KEY,\n {\n token: normalized,\n updatedAt: new Date().toISOString(),\n },\n {\n metadata: {\n source: \"notion-webhook\",\n },\n }\n );\n return true;\n}\n\n// Re-exported for backwards compatibility with the starter.\nexport { compactNotionId };\n","// lib/env.ts - 集中获取 Cloudflare bindings\n// 用 cloudflare:workers 模块(workerd 内置),作为平台 adapter 的绑定入口\n\n/// <reference types=\"@cloudflare/workers-types\" />\nimport { env } from \"cloudflare:workers\";\n\nexport type AppEnv = {\n DB: D1Database;\n ASSETS: Fetcher;\n IMAGES: ImagesBinding;\n ASSETS_BUCKET?: R2Bucket;\n CONTENT_CACHE?: KVNamespace;\n ADMIN_PASSWORD: string;\n ADMIN_EMAIL?: string;\n SITE_URL?: string;\n RESEND_API_KEY?: string;\n RESEND_FROM?: string;\n // Google OAuth 仍然兼容 Cloudflare Secret 作为兜底。\n // 实际生效值以 app_settings.google_client_id / google_client_secret 为准。\n GOOGLE_CLIENT_ID?: string;\n GOOGLE_CLIENT_SECRET?: string;\n /** Turnstile site key fallback when not stored in app_settings */\n TURNSTILE_SITE_KEY?: string;\n /** Turnstile secret — set via `wrangler secret put TURNSTILE_SECRET_KEY` */\n TURNSTILE_SECRET_KEY?: string;\n /** Notion integration token for the blog data source */\n NOTION_TOKEN?: string;\n /** Notion data source ID used by dataSources.query */\n NOTION_DATA_SOURCE_ID?: string;\n /** Notion data source ID for the public movie catalog */\n NOTION_MOVIES_DATA_SOURCE_ID?: string;\n /** Notion data source ID for localized movie copy */\n NOTION_MOVIE_TRANSLATIONS_DATA_SOURCE_ID?: string;\n /** Optional Notion API base URL for tests or proxies */\n NOTION_API_BASE_URL?: string;\n /** Optional Notion edit URL for admin handoff screens */\n NOTION_EDIT_BASE_URL?: string;\n /** Optional webhook verification token for Notion invalidation */\n NOTION_WEBHOOK_VERIFICATION_TOKEN?: string;\n};\n\n// 强制类型:vinext 把 env 类型放在 env.d.ts(interface VinextEnv extends Env),\n// 但 TS server 经常解析不到。运行时一定有 DB,类型断言保证编译通过。\nexport const workerEnv = env as unknown as AppEnv;\n","import type { AppEnv } from \"../util/env\";\n\nexport type PlatformBindingEnv = Pick<\n AppEnv,\n \"ASSETS_BUCKET\" | \"CONTENT_CACHE\" | \"DB\" | \"IMAGES\"\n>;\n\nexport type StoredObject = {\n body: ReadableStream;\n size: number;\n etag?: string;\n contentType?: string;\n};\n\nexport type ObjectStoragePutOptions = {\n contentType?: string;\n cacheControl?: string;\n metadata?: Record<string, string>;\n};\n\nexport type ObjectStorageListItem = {\n key: string;\n size: number;\n uploaded: Date;\n};\n\nexport type ObjectStorageAdapter = {\n kind: \"r2\";\n get(key: string): Promise<StoredObject | null>;\n put(\n key: string,\n value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob,\n options?: ObjectStoragePutOptions\n ): Promise<void>;\n delete(key: string): Promise<void>;\n list(\n options?: { prefix?: string; limit?: number }\n ): Promise<ObjectStorageListItem[]>;\n};\n\nexport type ImageTransformOptions = {\n width?: number;\n format: \"image/avif\" | \"image/webp\";\n quality: number;\n};\n\nexport type ImageTransformResult = {\n body: ReadableStream;\n contentType: string;\n response(): Response;\n};\n\nexport type ImageTransformerAdapter = {\n kind: \"cloudflare-images\" | \"external\";\n transform(\n body: ReadableStream,\n options: ImageTransformOptions\n ): Promise<ImageTransformResult>;\n};\n\nexport type PublicCacheAdapter = {\n kind: \"cloudflare-cache\" | \"noop\" | \"external\";\n match(key: string): Promise<Response | null>;\n put(key: string, response: Response): Promise<void>;\n delete(key: string): Promise<boolean>;\n};\n\nexport type KeyValueCacheGetOptions = {\n cacheTtl?: number;\n};\n\nexport type KeyValueCachePutOptions = {\n expirationTtl?: number;\n metadata?: Record<string, string | number | boolean | null>;\n};\n\nexport type KeyValueCacheListOptions = {\n prefix?: string;\n limit?: number;\n cursor?: string;\n};\n\nexport type KeyValueCacheListResult = {\n keys: Array<{ name: string }>;\n cursor?: string;\n listComplete: boolean;\n};\n\nexport type KeyValueCacheAdapter = {\n kind: \"workers-kv\" | \"noop\" | \"external\";\n get<T = unknown>(\n key: string,\n options?: KeyValueCacheGetOptions\n ): Promise<T | null>;\n put<T = unknown>(\n key: string,\n value: T,\n options?: KeyValueCachePutOptions\n ): Promise<void>;\n delete(key: string): Promise<void>;\n list(options?: KeyValueCacheListOptions): Promise<KeyValueCacheListResult>;\n};\n\nexport type SqlValue = string | number | boolean | null;\n\nexport type SqlResult<T = Record<string, unknown>> = {\n results?: T[];\n success?: boolean;\n meta?: {\n changes?: number;\n duration?: number;\n last_row_id?: number;\n rows_read?: number;\n rows_written?: number;\n [key: string]: unknown;\n };\n};\n\nexport type SqlPreparedStatement = {\n bind(...values: SqlValue[]): SqlPreparedStatement;\n all<T = Record<string, unknown>>(): Promise<SqlResult<T>>;\n first<T = Record<string, unknown>>(columnName?: string): Promise<T | null>;\n run<T = Record<string, unknown>>(): Promise<SqlResult<T>>;\n};\n\nexport type SqlDatabaseAdapter = {\n kind: \"d1\";\n prepare(query: string): SqlPreparedStatement;\n batch<T = Record<string, unknown>>(\n statements: SqlPreparedStatement[]\n ): Promise<SqlResult<T>[]>;\n};\n\nexport type RuntimePlatform = {\n id: \"cloudflare-workers\";\n database: SqlDatabaseAdapter | null;\n objectStorage: ObjectStorageAdapter | null;\n imageTransformer: ImageTransformerAdapter | null;\n publicCache: PublicCacheAdapter | null;\n keyValueCache: KeyValueCacheAdapter | null;\n};\n\ntype CloudflareCacheLike = Pick<Cache, \"match\" | \"put\" | \"delete\">;\ntype CloudflareKvLike = Pick<KVNamespace, \"get\" | \"put\" | \"delete\" | \"list\">;\n\nfunction cacheRequestForKey(key: string) {\n return new Request(key, { method: \"GET\" });\n}\n\nexport function createCloudflarePublicCacheAdapter(\n cache: CloudflareCacheLike\n): PublicCacheAdapter {\n return {\n kind: \"cloudflare-cache\",\n async match(key) {\n return (await cache.match(cacheRequestForKey(key))) ?? null;\n },\n put(key, response) {\n return cache.put(cacheRequestForKey(key), response);\n },\n delete(key) {\n return cache.delete(cacheRequestForKey(key));\n },\n };\n}\n\nexport function createNoopPublicCacheAdapter(kind: \"noop\" = \"noop\"): PublicCacheAdapter {\n return {\n kind,\n async match() {\n return null;\n },\n async put() {},\n async delete() {\n return false;\n },\n };\n}\n\nexport function createCloudflareKeyValueCacheAdapter(\n namespace: CloudflareKvLike\n): KeyValueCacheAdapter {\n return {\n kind: \"workers-kv\",\n async get<T = unknown>(\n key: string,\n options?: KeyValueCacheGetOptions\n ): Promise<T | null> {\n return (await namespace.get(key, {\n type: \"json\",\n cacheTtl: options?.cacheTtl,\n })) as T | null;\n },\n async put(key, value, options) {\n await namespace.put(key, JSON.stringify(value), {\n expirationTtl: options?.expirationTtl,\n metadata: options?.metadata,\n });\n },\n delete(key) {\n return namespace.delete(key);\n },\n async list(options) {\n const result = await namespace.list({\n prefix: options?.prefix,\n limit: options?.limit,\n cursor: options?.cursor,\n });\n return {\n keys: result.keys.map((key) => ({ name: key.name })),\n cursor: result.list_complete ? undefined : result.cursor,\n listComplete: result.list_complete,\n };\n },\n };\n}\n\nexport function createNoopKeyValueCacheAdapter(\n kind: \"noop\" = \"noop\"\n): KeyValueCacheAdapter {\n return {\n kind,\n async get() {\n return null;\n },\n async put() {},\n async delete() {},\n async list() {\n return { keys: [], listComplete: true };\n },\n };\n}\n\nfunction r2ObjectToStoredObject(object: R2ObjectBody): StoredObject {\n return {\n body: object.body,\n size: object.size,\n etag: object.etag,\n contentType: object.httpMetadata?.contentType,\n };\n}\n\nexport function createCloudflareRuntimePlatform(\n env: PlatformBindingEnv,\n options?: { publicCache?: CloudflareCacheLike | null }\n): RuntimePlatform {\n const database: SqlDatabaseAdapter | null = env.DB\n ? ({\n kind: \"d1\",\n prepare(query: string) {\n return env.DB.prepare(query) as unknown as SqlPreparedStatement;\n },\n async batch(statements: SqlPreparedStatement[]) {\n return (await env.DB.batch(\n statements as unknown as D1PreparedStatement[]\n )) as unknown as SqlResult<Record<string, unknown>>[];\n },\n } as unknown as SqlDatabaseAdapter)\n : null;\n\n const objectStorage: ObjectStorageAdapter | null = env.ASSETS_BUCKET\n ? {\n kind: \"r2\",\n async get(key) {\n const object = await env.ASSETS_BUCKET?.get(key);\n return object ? r2ObjectToStoredObject(object) : null;\n },\n async put(key, value, options) {\n await env.ASSETS_BUCKET?.put(key, value, {\n httpMetadata: {\n contentType: options?.contentType,\n cacheControl: options?.cacheControl,\n },\n customMetadata: options?.metadata,\n });\n },\n async delete(key) {\n await env.ASSETS_BUCKET?.delete(key);\n },\n async list(options) {\n const listed = await env.ASSETS_BUCKET?.list({\n prefix: options?.prefix,\n limit: options?.limit,\n });\n return (\n listed?.objects.map((object) => ({\n key: object.key,\n size: object.size,\n uploaded: object.uploaded,\n })) ?? []\n );\n },\n }\n : null;\n\n const imageTransformer: ImageTransformerAdapter | null = env.IMAGES\n ? {\n kind: \"cloudflare-images\",\n async transform(body, options) {\n const result = await env.IMAGES.input(body)\n .transform(options.width ? { width: options.width } : {})\n .output({\n format: options.format,\n quality: options.quality,\n });\n return {\n body: result.image(),\n contentType: result.contentType(),\n response: () => result.response(),\n };\n },\n }\n : null;\n\n const keyValueCache: KeyValueCacheAdapter | null = env.CONTENT_CACHE\n ? createCloudflareKeyValueCacheAdapter(env.CONTENT_CACHE)\n : null;\n\n return {\n id: \"cloudflare-workers\",\n database,\n objectStorage,\n imageTransformer,\n keyValueCache,\n publicCache: options?.publicCache\n ? createCloudflarePublicCacheAdapter(options.publicCache)\n : null,\n };\n}\n","import { workerEnv } from \"../util/env\";\nimport {\n createCloudflarePublicCacheAdapter,\n createCloudflareRuntimePlatform,\n} from \"./runtime\";\n\nfunction getDefaultCloudflareCache() {\n const globalWithCaches = globalThis as typeof globalThis & {\n caches?: CacheStorage & { default?: Cache };\n };\n return globalWithCaches.caches?.default ?? null;\n}\n\nexport function getRuntimePlatform() {\n return createCloudflareRuntimePlatform(workerEnv, {\n publicCache: getDefaultCloudflareCache(),\n });\n}\n\nexport function getDatabase() {\n const database = getRuntimePlatform().database;\n if (!database) {\n throw new Error(\"SQL database binding not configured\");\n }\n return database;\n}\n\nexport function getPublicCache() {\n const cache = getDefaultCloudflareCache();\n if (!cache) {\n throw new Error(\"Cloudflare cache binding not configured\");\n }\n return createCloudflarePublicCacheAdapter(cache);\n}\n","import {\n getPublicCache as getCloudflarePublicCache,\n getRuntimePlatform as getCloudflareRuntimePlatform,\n} from \"./cloudflare-runtime\";\nimport { currentRuntimeId } from \"./selection\";\n\nexport function getRuntimePlatform() {\n return getCloudflareRuntimePlatform();\n}\n\nexport function getDatabase() {\n const platform = getRuntimePlatform();\n const database = platform.database;\n if (!database) {\n throw new Error(`SQL database adapter not configured for ${platform.id}`);\n }\n return database;\n}\n\nexport function getPublicCache() {\n return getCloudflarePublicCache();\n}\n\nexport function getKeyValueCache() {\n return getRuntimePlatform().keyValueCache;\n}\n\nexport const runtimeSelection = {\n currentRuntimeId,\n};\n"],"mappings":"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","names":["env","env","readString","readString","env","options","getRuntimePlatform","getRuntimePlatform","verified"]}
@@ -0,0 +1,98 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { NotionPageRetriever, NotionWebhookParseResult } from '../webhook.js';
3
+ import { getNotionWebhookVerificationToken } from '../config.js';
4
+ import { KeyValueCacheAdapter, SqlDatabaseAdapter } from '../../platform/runtime.js';
5
+ import '../property-mappers.js';
6
+ import '../types.js';
7
+ import '../../env-C5qu-0R-.js';
8
+
9
+ type NotionWebhookParserFn = (payload: unknown, options?: {
10
+ retrievePage?: NotionPageRetriever;
11
+ lookupPages?: boolean;
12
+ }) => Promise<NotionWebhookParseResult>;
13
+ type RevalidateContentModelFromWebhookFn = (input: {
14
+ request: {
15
+ modelId: string;
16
+ pageId?: string;
17
+ routeId?: string;
18
+ previousRouteId?: string;
19
+ locale?: string;
20
+ kind?: "publish" | "update" | "delete";
21
+ includeApi?: boolean;
22
+ };
23
+ tokenAuthorized: boolean;
24
+ revalidatePath: (path: string, type?: "page" | "layout") => void | Promise<void>;
25
+ contentCache?: KeyValueCacheAdapter;
26
+ getContentCache?: () => KeyValueCacheAdapter | null;
27
+ database?: SqlDatabaseAdapter;
28
+ getDatabase?: () => SqlDatabaseAdapter | null;
29
+ }) => Promise<{
30
+ ok: true;
31
+ model: {
32
+ id: string;
33
+ routes: {
34
+ listPath: string;
35
+ detailPath: string;
36
+ publicApiPath?: string;
37
+ };
38
+ };
39
+ routeId?: string;
40
+ revalidatedPaths: string[];
41
+ contentCache: unknown;
42
+ searchIndex: unknown;
43
+ } | {
44
+ ok: false;
45
+ status: 400 | 401 | 404;
46
+ error: string;
47
+ }>;
48
+ type CreateNotionWebhookRouteOptions = {
49
+ revalidatePath: (path: string, type?: "page" | "layout") => void | Promise<void>;
50
+ revalidateContentModel: RevalidateContentModelFromWebhookFn;
51
+ /**
52
+ * The starter's parser wrapper that injects its content models.
53
+ * Required; there is no sensible package default because the parser
54
+ * depends on the consumer's content model registrations.
55
+ */
56
+ parseNotionWebhookPayload: NotionWebhookParserFn;
57
+ getVerificationToken?: typeof getNotionWebhookVerificationToken;
58
+ };
59
+ declare function createNotionWebhookRoute(options: CreateNotionWebhookRouteOptions): {
60
+ POST(request: Request): Promise<NextResponse<{
61
+ ok: boolean;
62
+ error: string;
63
+ }> | NextResponse<{
64
+ verification_token: string;
65
+ stored: boolean;
66
+ }> | NextResponse<{
67
+ ok: boolean;
68
+ received: number;
69
+ results: {
70
+ eventId: string | undefined;
71
+ eventType: string;
72
+ modelId: string;
73
+ routeId: string | null;
74
+ result: {
75
+ ok: true;
76
+ model: {
77
+ id: string;
78
+ routes: {
79
+ listPath: string;
80
+ detailPath: string;
81
+ publicApiPath?: string;
82
+ };
83
+ };
84
+ routeId?: string;
85
+ revalidatedPaths: string[];
86
+ contentCache: unknown;
87
+ searchIndex: unknown;
88
+ } | {
89
+ ok: false;
90
+ status: 400 | 401 | 404;
91
+ error: string;
92
+ };
93
+ }[];
94
+ }>>;
95
+ handle(request: Request): Promise<Response>;
96
+ };
97
+
98
+ export { type CreateNotionWebhookRouteOptions, type NotionWebhookParserFn, type RevalidateContentModelFromWebhookFn, createNotionWebhookRoute };
@@ -0,0 +1,428 @@
1
+ // src/notion/routes/webhook.ts
2
+ import { NextResponse } from "next/server";
3
+
4
+ // src/notion/client.ts
5
+ import { Client } from "@notionhq/client";
6
+
7
+ // src/notion/config.ts
8
+ function readProcessEnv() {
9
+ const env2 = {
10
+ NOTION_TOKEN: process.env.NOTION_TOKEN,
11
+ NOTION_DATA_SOURCE_ID: process.env.NOTION_DATA_SOURCE_ID,
12
+ NOTION_MOVIES_DATA_SOURCE_ID: process.env.NOTION_MOVIES_DATA_SOURCE_ID,
13
+ NOTION_API_BASE_URL: process.env.NOTION_API_BASE_URL,
14
+ NOTION_EDIT_BASE_URL: process.env.NOTION_EDIT_BASE_URL,
15
+ NOTION_WEBHOOK_VERIFICATION_TOKEN: process.env.NOTION_WEBHOOK_VERIFICATION_TOKEN
16
+ };
17
+ for (const [key, value] of Object.entries(process.env)) {
18
+ if (key.startsWith("NOTION_") && typeof value === "string") {
19
+ env2[key] = value;
20
+ }
21
+ }
22
+ return env2;
23
+ }
24
+ async function readWorkerEnv() {
25
+ try {
26
+ const mod = await import(
27
+ /* webpackIgnore: true */
28
+ "cloudflare:workers"
29
+ );
30
+ const env2 = {};
31
+ for (const [key, value] of Object.entries(mod.env ?? {})) {
32
+ if (key.startsWith("NOTION_") && typeof value === "string") {
33
+ env2[key] = value;
34
+ }
35
+ }
36
+ return env2;
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+ function readString(source, name) {
42
+ const value = String(source[name] ?? "").trim();
43
+ return value || void 0;
44
+ }
45
+ function mergeEnv(...sources) {
46
+ const merged = {};
47
+ for (const source of sources) {
48
+ for (const name of Object.keys(source)) {
49
+ if (!name.startsWith("NOTION_")) continue;
50
+ const value = readString(source, name);
51
+ if (value) merged[name] = value;
52
+ }
53
+ }
54
+ return merged;
55
+ }
56
+ async function readEnv() {
57
+ const processEnv = readProcessEnv();
58
+ return mergeEnv(await readWorkerEnv(), processEnv);
59
+ }
60
+ async function getNotionWebhookVerificationToken() {
61
+ const env2 = await readEnv();
62
+ return readString(env2, "NOTION_WEBHOOK_VERIFICATION_TOKEN");
63
+ }
64
+
65
+ // src/notion/webhook.ts
66
+ var WEBHOOK_VERIFICATION_TOKEN_CACHE_KEY = "notion:webhook:verification-token:v1";
67
+ function isRecord(value) {
68
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
69
+ }
70
+ function readString2(source, key) {
71
+ if (!isRecord(source)) return "";
72
+ const value = source[key];
73
+ return typeof value === "string" ? value.trim() : "";
74
+ }
75
+ function notionWebhookEventToRevalidateRequest(event) {
76
+ return {
77
+ modelId: event.modelId,
78
+ pageId: event.pageId,
79
+ routeId: event.routeId,
80
+ locale: event.locale,
81
+ kind: event.kind,
82
+ includeApi: event.includeApi
83
+ };
84
+ }
85
+ async function signNotionWebhookBody(body, verificationToken) {
86
+ const key = await crypto.subtle.importKey(
87
+ "raw",
88
+ new TextEncoder().encode(verificationToken),
89
+ { name: "HMAC", hash: "SHA-256" },
90
+ false,
91
+ ["sign"]
92
+ );
93
+ const signature = await crypto.subtle.sign(
94
+ "HMAC",
95
+ key,
96
+ new TextEncoder().encode(body)
97
+ );
98
+ return Array.from(new Uint8Array(signature)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
99
+ }
100
+ function normalizeNotionSignature(signature) {
101
+ const value = String(signature ?? "").trim();
102
+ const prefixed = value.match(/^sha256=(.+)$/i);
103
+ return prefixed && prefixed[1] ? prefixed[1].trim() : value;
104
+ }
105
+ async function verifyNotionWebhookSignature(input) {
106
+ const token = String(input.verificationToken ?? "").trim();
107
+ const signature = normalizeNotionSignature(input.signature);
108
+ if (!token || !signature) return false;
109
+ const expected = await signNotionWebhookBody(input.body, token);
110
+ if (expected.length !== signature.length) return false;
111
+ let diff = 0;
112
+ for (let index = 0; index < expected.length; index += 1) {
113
+ diff |= expected.charCodeAt(index) ^ signature.charCodeAt(index);
114
+ }
115
+ return diff === 0;
116
+ }
117
+ async function verifyNotionWebhookSignatureWithTokens(input) {
118
+ const tokens = Array.from(
119
+ new Set(input.verificationTokens.map((token) => String(token ?? "").trim()))
120
+ ).filter(Boolean);
121
+ for (const token of tokens) {
122
+ if (await verifyNotionWebhookSignature({
123
+ body: input.body,
124
+ signature: input.signature,
125
+ verificationToken: token
126
+ })) {
127
+ return true;
128
+ }
129
+ }
130
+ return false;
131
+ }
132
+ function readStoredWebhookVerificationToken(value) {
133
+ if (typeof value === "string") return value.trim() || null;
134
+ if (!isRecord(value)) return null;
135
+ const token = readString2(value, "token");
136
+ return token || null;
137
+ }
138
+ async function getStoredNotionWebhookVerificationToken(cache) {
139
+ if (!cache) return null;
140
+ try {
141
+ const value = await cache.get(
142
+ WEBHOOK_VERIFICATION_TOKEN_CACHE_KEY,
143
+ { cacheTtl: 60 }
144
+ );
145
+ return readStoredWebhookVerificationToken(value);
146
+ } catch (error) {
147
+ console.warn(
148
+ JSON.stringify({
149
+ tag: "notion_webhook_token_lookup_failed",
150
+ message: error instanceof Error ? error.message : String(error)
151
+ })
152
+ );
153
+ return null;
154
+ }
155
+ }
156
+ async function putStoredNotionWebhookVerificationToken(cache, token) {
157
+ const normalized = token.trim();
158
+ if (!cache || !normalized) return false;
159
+ await cache.put(
160
+ WEBHOOK_VERIFICATION_TOKEN_CACHE_KEY,
161
+ {
162
+ token: normalized,
163
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
164
+ },
165
+ {
166
+ metadata: {
167
+ source: "notion-webhook"
168
+ }
169
+ }
170
+ );
171
+ return true;
172
+ }
173
+
174
+ // src/util/env.ts
175
+ import { env } from "cloudflare:workers";
176
+ var workerEnv = env;
177
+
178
+ // src/platform/runtime.ts
179
+ function cacheRequestForKey(key) {
180
+ return new Request(key, { method: "GET" });
181
+ }
182
+ function createCloudflarePublicCacheAdapter(cache) {
183
+ return {
184
+ kind: "cloudflare-cache",
185
+ async match(key) {
186
+ return await cache.match(cacheRequestForKey(key)) ?? null;
187
+ },
188
+ put(key, response) {
189
+ return cache.put(cacheRequestForKey(key), response);
190
+ },
191
+ delete(key) {
192
+ return cache.delete(cacheRequestForKey(key));
193
+ }
194
+ };
195
+ }
196
+ function createCloudflareKeyValueCacheAdapter(namespace) {
197
+ return {
198
+ kind: "workers-kv",
199
+ async get(key, options) {
200
+ return await namespace.get(key, {
201
+ type: "json",
202
+ cacheTtl: options?.cacheTtl
203
+ });
204
+ },
205
+ async put(key, value, options) {
206
+ await namespace.put(key, JSON.stringify(value), {
207
+ expirationTtl: options?.expirationTtl,
208
+ metadata: options?.metadata
209
+ });
210
+ },
211
+ delete(key) {
212
+ return namespace.delete(key);
213
+ },
214
+ async list(options) {
215
+ const result = await namespace.list({
216
+ prefix: options?.prefix,
217
+ limit: options?.limit,
218
+ cursor: options?.cursor
219
+ });
220
+ return {
221
+ keys: result.keys.map((key) => ({ name: key.name })),
222
+ cursor: result.list_complete ? void 0 : result.cursor,
223
+ listComplete: result.list_complete
224
+ };
225
+ }
226
+ };
227
+ }
228
+ function r2ObjectToStoredObject(object) {
229
+ return {
230
+ body: object.body,
231
+ size: object.size,
232
+ etag: object.etag,
233
+ contentType: object.httpMetadata?.contentType
234
+ };
235
+ }
236
+ function createCloudflareRuntimePlatform(env2, options) {
237
+ const database = env2.DB ? {
238
+ kind: "d1",
239
+ prepare(query) {
240
+ return env2.DB.prepare(query);
241
+ },
242
+ async batch(statements) {
243
+ return await env2.DB.batch(
244
+ statements
245
+ );
246
+ }
247
+ } : null;
248
+ const objectStorage = env2.ASSETS_BUCKET ? {
249
+ kind: "r2",
250
+ async get(key) {
251
+ const object = await env2.ASSETS_BUCKET?.get(key);
252
+ return object ? r2ObjectToStoredObject(object) : null;
253
+ },
254
+ async put(key, value, options2) {
255
+ await env2.ASSETS_BUCKET?.put(key, value, {
256
+ httpMetadata: {
257
+ contentType: options2?.contentType,
258
+ cacheControl: options2?.cacheControl
259
+ },
260
+ customMetadata: options2?.metadata
261
+ });
262
+ },
263
+ async delete(key) {
264
+ await env2.ASSETS_BUCKET?.delete(key);
265
+ },
266
+ async list(options2) {
267
+ const listed = await env2.ASSETS_BUCKET?.list({
268
+ prefix: options2?.prefix,
269
+ limit: options2?.limit
270
+ });
271
+ return listed?.objects.map((object) => ({
272
+ key: object.key,
273
+ size: object.size,
274
+ uploaded: object.uploaded
275
+ })) ?? [];
276
+ }
277
+ } : null;
278
+ const imageTransformer = env2.IMAGES ? {
279
+ kind: "cloudflare-images",
280
+ async transform(body, options2) {
281
+ const result = await env2.IMAGES.input(body).transform(options2.width ? { width: options2.width } : {}).output({
282
+ format: options2.format,
283
+ quality: options2.quality
284
+ });
285
+ return {
286
+ body: result.image(),
287
+ contentType: result.contentType(),
288
+ response: () => result.response()
289
+ };
290
+ }
291
+ } : null;
292
+ const keyValueCache = env2.CONTENT_CACHE ? createCloudflareKeyValueCacheAdapter(env2.CONTENT_CACHE) : null;
293
+ return {
294
+ id: "cloudflare-workers",
295
+ database,
296
+ objectStorage,
297
+ imageTransformer,
298
+ keyValueCache,
299
+ publicCache: options?.publicCache ? createCloudflarePublicCacheAdapter(options.publicCache) : null
300
+ };
301
+ }
302
+
303
+ // src/platform/cloudflare-runtime.ts
304
+ function getDefaultCloudflareCache() {
305
+ const globalWithCaches = globalThis;
306
+ return globalWithCaches.caches?.default ?? null;
307
+ }
308
+ function getRuntimePlatform() {
309
+ return createCloudflareRuntimePlatform(workerEnv, {
310
+ publicCache: getDefaultCloudflareCache()
311
+ });
312
+ }
313
+
314
+ // src/platform/current.ts
315
+ function getRuntimePlatform2() {
316
+ return getRuntimePlatform();
317
+ }
318
+
319
+ // src/notion/routes/webhook.ts
320
+ function createNotionWebhookRoute(options) {
321
+ const parseFn = options.parseNotionWebhookPayload;
322
+ const getToken = options.getVerificationToken ?? getNotionWebhookVerificationToken;
323
+ return {
324
+ async POST(request) {
325
+ return handlePost(request, options, parseFn, getToken);
326
+ },
327
+ async handle(request) {
328
+ if (request.method !== "POST") {
329
+ return NextResponse.json(
330
+ { ok: false, error: "Method not allowed" },
331
+ { status: 405, headers: { "Cache-Control": "no-store" } }
332
+ );
333
+ }
334
+ return handlePost(request, options, parseFn, getToken);
335
+ }
336
+ };
337
+ }
338
+ async function handlePost(request, options, parseFn, getToken) {
339
+ const bodyText = await request.text();
340
+ let payload;
341
+ try {
342
+ payload = JSON.parse(bodyText || "{}");
343
+ } catch {
344
+ return NextResponse.json(
345
+ { ok: false, error: "Invalid JSON" },
346
+ { status: 400, headers: { "Cache-Control": "no-store" } }
347
+ );
348
+ }
349
+ const platform = getRuntimePlatform2();
350
+ const parsed = await parseFn(payload, { lookupPages: false });
351
+ if (parsed.type === "verification") {
352
+ const verified2 = await verifyNotionWebhookSignatureWithTokens({
353
+ body: bodyText,
354
+ signature: request.headers.get("x-notion-signature"),
355
+ verificationTokens: [parsed.verificationToken]
356
+ });
357
+ if (!verified2) {
358
+ return NextResponse.json(
359
+ { ok: false, error: "Invalid Notion webhook verification signature" },
360
+ { status: 401, headers: { "Cache-Control": "no-store" } }
361
+ );
362
+ }
363
+ const stored = await putStoredNotionWebhookVerificationToken(
364
+ platform.keyValueCache,
365
+ parsed.verificationToken
366
+ );
367
+ return NextResponse.json(
368
+ {
369
+ verification_token: parsed.verificationToken,
370
+ stored
371
+ },
372
+ { headers: { "Cache-Control": "no-store" } }
373
+ );
374
+ }
375
+ const token = await getToken();
376
+ const storedToken = await getStoredNotionWebhookVerificationToken(
377
+ platform.keyValueCache
378
+ );
379
+ const verified = await verifyNotionWebhookSignatureWithTokens({
380
+ body: bodyText,
381
+ signature: request.headers.get("x-notion-signature"),
382
+ verificationTokens: [token, storedToken]
383
+ });
384
+ if (!verified) {
385
+ return NextResponse.json(
386
+ { ok: false, error: "Invalid Notion webhook signature" },
387
+ { status: 401, headers: { "Cache-Control": "no-store" } }
388
+ );
389
+ }
390
+ const resolved = await parseFn(payload);
391
+ if (resolved.type === "verification") {
392
+ return NextResponse.json(
393
+ { ok: false, error: "Unexpected verification payload" },
394
+ { status: 400, headers: { "Cache-Control": "no-store" } }
395
+ );
396
+ }
397
+ const results = [];
398
+ for (const event of resolved.events) {
399
+ const result = await options.revalidateContentModel({
400
+ request: notionWebhookEventToRevalidateRequest(event),
401
+ tokenAuthorized: true,
402
+ revalidatePath: options.revalidatePath,
403
+ contentCache: platform.keyValueCache ?? void 0,
404
+ getContentCache: () => platform.keyValueCache,
405
+ database: platform.database ?? void 0,
406
+ getDatabase: () => platform.database
407
+ });
408
+ results.push({
409
+ eventId: event.id,
410
+ eventType: event.eventType,
411
+ modelId: event.modelId,
412
+ routeId: event.routeId ?? null,
413
+ result
414
+ });
415
+ }
416
+ return NextResponse.json(
417
+ {
418
+ ok: true,
419
+ received: parsed.events.length,
420
+ results
421
+ },
422
+ { headers: { "Cache-Control": "no-store" } }
423
+ );
424
+ }
425
+ export {
426
+ createNotionWebhookRoute
427
+ };
428
+ //# sourceMappingURL=webhook.js.map