@notionx/core 0.1.0 → 0.1.2

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/notion/client.ts","../../src/notion/config.ts","../../src/notion/property-mappers.ts","../../src/notion/webhook.ts"],"sourcesContent":["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","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 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","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"],"mappings":"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","names":["readString"]}
1
+ {"version":3,"sources":["../../src/notion/client.ts","../../src/notion/config.ts","../../src/notion/property-mappers.ts","../../src/notion/webhook.ts"],"sourcesContent":["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","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","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"],"mappings":"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","names":["readString"]}
@@ -0,0 +1,117 @@
1
+ import { NotionBlock, NotionPageLike } from '../notion/types.js';
2
+
3
+ type SitePageLayout = "home" | "default" | "legal" | "content-list";
4
+ type SitePageFields = {
5
+ title: string;
6
+ key: string;
7
+ slug: string;
8
+ status: string;
9
+ layout: string;
10
+ description: string;
11
+ seoTitle: string;
12
+ seoDescription: string;
13
+ showHeader: string;
14
+ showFooter: string;
15
+ showInNav: string;
16
+ navLabel: string;
17
+ navOrder: string;
18
+ showInFooter: string;
19
+ footerLabel: string;
20
+ footerGroup: string;
21
+ footerOrder: string;
22
+ contentSource: string;
23
+ cover: string;
24
+ };
25
+ type SitePage = {
26
+ pageId: string;
27
+ key: string;
28
+ slug: string;
29
+ href: string;
30
+ title: string;
31
+ description: string;
32
+ seoTitle: string;
33
+ seoDescription: string;
34
+ layout: SitePageLayout;
35
+ published: boolean;
36
+ showHeader: boolean;
37
+ showFooter: boolean;
38
+ showInNav: boolean;
39
+ navLabel: string;
40
+ navOrder: number;
41
+ showInFooter: boolean;
42
+ footerLabel: string;
43
+ footerGroup: string;
44
+ footerOrder: number;
45
+ contentSource: string;
46
+ coverImage: string | null;
47
+ editUrl: string | null;
48
+ blocks: NotionBlock[];
49
+ };
50
+ type SitePageNavItem = {
51
+ label: string;
52
+ href: string;
53
+ order: number;
54
+ pageKey: string;
55
+ };
56
+ type SitePageFooterGroup = {
57
+ label: string;
58
+ items: SitePageNavItem[];
59
+ };
60
+ type SitePageModel = {
61
+ source: {
62
+ tokenEnv: string;
63
+ dataSourceEnv: string;
64
+ defaultDataSourceId?: string;
65
+ fields?: Partial<SitePageFields>;
66
+ query?: {
67
+ pageSize?: number;
68
+ };
69
+ };
70
+ };
71
+ type DefinedSitePageModel = SitePageModel & {
72
+ source: Omit<SitePageModel["source"], "fields" | "query"> & {
73
+ fields: SitePageFields;
74
+ query: {
75
+ pageSize: number;
76
+ };
77
+ };
78
+ };
79
+ type SitePageSourceDeps = {
80
+ queryDataSource: (input?: {
81
+ startCursor?: string;
82
+ }) => Promise<{
83
+ results?: unknown[];
84
+ has_more?: boolean;
85
+ next_cursor?: string | null;
86
+ }>;
87
+ getPageBlocks: (pageId: string) => Promise<NotionBlock[]>;
88
+ editBaseUrl?: string;
89
+ };
90
+
91
+ declare const defaultSitePageFields: SitePageFields;
92
+ declare const defaultPagesDataSourceEnv = "NOTION_PAGES_DATA_SOURCE_ID";
93
+ declare function defineSitePageModel(model: SitePageModel): DefinedSitePageModel;
94
+
95
+ declare function slugToHref(slug: string): string;
96
+ declare function normalizePageSlug(slug: string): string;
97
+ declare function mapNotionPageToSitePage(page: NotionPageLike, fields: SitePageFields, blocks?: NotionBlock[], options?: {
98
+ editBaseUrl?: string;
99
+ }): SitePage | null;
100
+ declare function createSitePageSource(modelInput: SitePageModel, deps: SitePageSourceDeps): {
101
+ listPages(): Promise<SitePage[]>;
102
+ };
103
+ declare function createSitePagesApi(input: {
104
+ model: SitePageModel;
105
+ fallbackPages?: readonly SitePage[];
106
+ }): {
107
+ listSitePages: () => Promise<SitePage[]>;
108
+ getSitePageByKey(key: string): Promise<SitePage | null>;
109
+ getSitePageBySlug(slug: string): Promise<SitePage | null>;
110
+ getSitePageForContentSource(sourceId: string): Promise<SitePage | null>;
111
+ getSiteNavigation(): Promise<SitePageNavItem[]>;
112
+ getSiteFooterGroups(): Promise<SitePageFooterGroup[]>;
113
+ };
114
+ declare function deriveSiteNavigation(pages: readonly SitePage[]): SitePageNavItem[];
115
+ declare function deriveSiteFooterGroups(pages: readonly SitePage[]): SitePageFooterGroup[];
116
+
117
+ export { type DefinedSitePageModel, type SitePage, type SitePageFields, type SitePageFooterGroup, type SitePageLayout, type SitePageModel, type SitePageNavItem, type SitePageSourceDeps, createSitePageSource, createSitePagesApi, defaultPagesDataSourceEnv, defaultSitePageFields, defineSitePageModel, deriveSiteFooterGroups, deriveSiteNavigation, mapNotionPageToSitePage, normalizePageSlug, slugToHref };