@nexpress/core 0.3.6 → 0.3.8

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 (94) hide show
  1. package/dist/{audit-43OLHR3U.js → audit-ZLNKBIDO.js} +3 -3
  2. package/dist/auth.js +4 -4
  3. package/dist/{can-UJ2NAOIR.js → can-U5F4JBZ7.js} +2 -2
  4. package/dist/{chunk-UMEFU7Y3.js → chunk-2O2KMHLO.js} +10 -10
  5. package/dist/chunk-2O2KMHLO.js.map +1 -0
  6. package/dist/{chunk-ELK6AVW5.js → chunk-2X3GBJOT.js} +2 -2
  7. package/dist/{chunk-ML2E3P3X.js → chunk-5C22NDW4.js} +2 -2
  8. package/dist/chunk-5C22NDW4.js.map +1 -0
  9. package/dist/{chunk-RKM4GDWM.js → chunk-6MRTH734.js} +1 -1
  10. package/dist/chunk-6MRTH734.js.map +1 -0
  11. package/dist/{chunk-STOLH4V2.js → chunk-6PFUXZJ6.js} +12 -12
  12. package/dist/chunk-6PFUXZJ6.js.map +1 -0
  13. package/dist/{chunk-WRDIRDH7.js → chunk-CD74WQK7.js} +76 -28
  14. package/dist/chunk-CD74WQK7.js.map +1 -0
  15. package/dist/{chunk-QBIJZZ5V.js → chunk-CGLJBRRX.js} +2 -2
  16. package/dist/chunk-CGLJBRRX.js.map +1 -0
  17. package/dist/{chunk-2VZZ7M26.js → chunk-EAYUAXW3.js} +3 -3
  18. package/dist/chunk-EAYUAXW3.js.map +1 -0
  19. package/dist/{chunk-2N53KKIL.js → chunk-EWVXP3GP.js} +2 -2
  20. package/dist/{chunk-CAS4Z6IN.js → chunk-I4FSVEJK.js} +1 -1
  21. package/dist/chunk-I4FSVEJK.js.map +1 -0
  22. package/dist/{chunk-KHTS6Y3E.js → chunk-JKTU67A7.js} +2 -2
  23. package/dist/{chunk-KHTS6Y3E.js.map → chunk-JKTU67A7.js.map} +1 -1
  24. package/dist/{chunk-LN6NTH6E.js → chunk-K4CJ3KXB.js} +3 -3
  25. package/dist/chunk-K4CJ3KXB.js.map +1 -0
  26. package/dist/{chunk-B7DTNT4O.js → chunk-MWLSXK6Y.js} +2 -2
  27. package/dist/{chunk-7KGF7JVJ.js → chunk-PPUHXOWZ.js} +2 -2
  28. package/dist/{chunk-NFHS7CFV.js → chunk-Q7MK5ZKG.js} +2 -2
  29. package/dist/{chunk-6UV2P5MW.js → chunk-TIWJVQOO.js} +3 -3
  30. package/dist/chunk-TIWJVQOO.js.map +1 -0
  31. package/dist/{chunk-L6VG7IK6.js → chunk-VBVLYFSZ.js} +2 -2
  32. package/dist/chunk-VBVLYFSZ.js.map +1 -0
  33. package/dist/{chunk-YYPSQMFY.js → chunk-VX3HM5TF.js} +2 -2
  34. package/dist/{chunk-RDTTK27V.js → chunk-XPD7EQML.js} +3 -3
  35. package/dist/chunk-XPD7EQML.js.map +1 -0
  36. package/dist/{chunk-RJ76SKWQ.js → chunk-XU2GJJ6Z.js} +1 -1
  37. package/dist/chunk-XU2GJJ6Z.js.map +1 -0
  38. package/dist/{chunk-WJJ5MBH5.js → chunk-YEOQJ7WW.js} +1 -1
  39. package/dist/chunk-YEOQJ7WW.js.map +1 -0
  40. package/dist/community.js +14 -14
  41. package/dist/{config-YFGOXHSR.js → config-2CV7KZ3D.js} +5 -5
  42. package/dist/{digest-ZODDTXA2.js → digest-IWHMJPXI.js} +4 -4
  43. package/dist/{host-SUX3SPOX.js → host-C5PGUXX7.js} +4 -4
  44. package/dist/i18n.js +2 -2
  45. package/dist/index.js +21 -21
  46. package/dist/index.js.map +1 -1
  47. package/dist/{job-log-N3IGI4NA.js → job-log-UY6ERPQZ.js} +3 -3
  48. package/dist/jobs.js +3 -3
  49. package/dist/{logger-2WUTTELV.js → logger-6ZGEKEMK.js} +2 -2
  50. package/dist/media.js +3 -3
  51. package/dist/{mentions-U4JACYI6.js → mentions-LQRZWAGO.js} +2 -2
  52. package/dist/{mutes-MNQP6ACF.js → mutes-PQA6U5X7.js} +2 -2
  53. package/dist/{notification-prefs-H4HFVCL7.js → notification-prefs-62NX2GBF.js} +2 -2
  54. package/dist/observability.js +2 -2
  55. package/dist/{reputation-ICIXDGPM.js → reputation-5DJLDBZY.js} +3 -3
  56. package/dist/{scheduled-T5WZ4I6O.js → scheduled-PF2HECSF.js} +5 -5
  57. package/dist/seo.js +4 -4
  58. package/dist/{settings-OZWM6L2K.js → settings-NBAP7E5E.js} +2 -2
  59. package/dist/{strings-4EWJYDOG.js → strings-O2M7VSKV.js} +3 -3
  60. package/package.json +1 -1
  61. package/dist/chunk-2VZZ7M26.js.map +0 -1
  62. package/dist/chunk-6UV2P5MW.js.map +0 -1
  63. package/dist/chunk-CAS4Z6IN.js.map +0 -1
  64. package/dist/chunk-L6VG7IK6.js.map +0 -1
  65. package/dist/chunk-LN6NTH6E.js.map +0 -1
  66. package/dist/chunk-ML2E3P3X.js.map +0 -1
  67. package/dist/chunk-QBIJZZ5V.js.map +0 -1
  68. package/dist/chunk-RDTTK27V.js.map +0 -1
  69. package/dist/chunk-RJ76SKWQ.js.map +0 -1
  70. package/dist/chunk-RKM4GDWM.js.map +0 -1
  71. package/dist/chunk-STOLH4V2.js.map +0 -1
  72. package/dist/chunk-UMEFU7Y3.js.map +0 -1
  73. package/dist/chunk-WJJ5MBH5.js.map +0 -1
  74. package/dist/chunk-WRDIRDH7.js.map +0 -1
  75. /package/dist/{audit-43OLHR3U.js.map → audit-ZLNKBIDO.js.map} +0 -0
  76. /package/dist/{can-UJ2NAOIR.js.map → can-U5F4JBZ7.js.map} +0 -0
  77. /package/dist/{chunk-ELK6AVW5.js.map → chunk-2X3GBJOT.js.map} +0 -0
  78. /package/dist/{chunk-2N53KKIL.js.map → chunk-EWVXP3GP.js.map} +0 -0
  79. /package/dist/{chunk-B7DTNT4O.js.map → chunk-MWLSXK6Y.js.map} +0 -0
  80. /package/dist/{chunk-7KGF7JVJ.js.map → chunk-PPUHXOWZ.js.map} +0 -0
  81. /package/dist/{chunk-NFHS7CFV.js.map → chunk-Q7MK5ZKG.js.map} +0 -0
  82. /package/dist/{chunk-YYPSQMFY.js.map → chunk-VX3HM5TF.js.map} +0 -0
  83. /package/dist/{config-YFGOXHSR.js.map → config-2CV7KZ3D.js.map} +0 -0
  84. /package/dist/{digest-ZODDTXA2.js.map → digest-IWHMJPXI.js.map} +0 -0
  85. /package/dist/{host-SUX3SPOX.js.map → host-C5PGUXX7.js.map} +0 -0
  86. /package/dist/{job-log-N3IGI4NA.js.map → job-log-UY6ERPQZ.js.map} +0 -0
  87. /package/dist/{logger-2WUTTELV.js.map → logger-6ZGEKEMK.js.map} +0 -0
  88. /package/dist/{mentions-U4JACYI6.js.map → mentions-LQRZWAGO.js.map} +0 -0
  89. /package/dist/{mutes-MNQP6ACF.js.map → mutes-PQA6U5X7.js.map} +0 -0
  90. /package/dist/{notification-prefs-H4HFVCL7.js.map → notification-prefs-62NX2GBF.js.map} +0 -0
  91. /package/dist/{reputation-ICIXDGPM.js.map → reputation-5DJLDBZY.js.map} +0 -0
  92. /package/dist/{scheduled-T5WZ4I6O.js.map → scheduled-PF2HECSF.js.map} +0 -0
  93. /package/dist/{settings-OZWM6L2K.js.map → settings-NBAP7E5E.js.map} +0 -0
  94. /package/dist/{strings-4EWJYDOG.js.map → strings-O2M7VSKV.js.map} +0 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config/types.ts","../src/config/define-config.ts","../src/themes/merge-requirements.ts","../src/config/validation.ts","../src/config/define-collection.ts","../src/content/helpers.ts","../src/theme/defaults.ts","../src/themes/registry.ts","../src/storage/local.ts","../src/storage/s3.ts","../src/storage/index.ts","../src/email/smtp.ts","../src/theme/sanitize.ts","../src/themes/requirements.ts","../src/themes/settings.ts","../src/themes/nav-locations.ts","../src/themes/error-seo.ts","../src/sites/memberships.ts","../src/plugins/persistence.ts"],"sourcesContent":["export type NpUserRole = \"admin\" | \"editor\" | \"moderator\" | \"author\" | \"viewer\";\n\nexport interface NpAuthUser {\n id: string;\n email: string;\n name: string;\n role: NpUserRole;\n tokenVersion: number;\n}\n\nexport type NpAccessFunction = (args: {\n user: NpAuthUser | null;\n doc?: Record<string, unknown>;\n data?: Record<string, unknown>;\n}) => boolean | Promise<boolean>;\n\n/**\n * Free-form predicate. Server-only — functions don't survive the\n * server→client boundary in Next.js (the framework's\n * `toClientCollectionConfig` strips them). For conditions that\n * need to run in the admin editor's browser-side renderer, use\n * `NpFieldConditionExpr` (serializable JSON shape) instead.\n */\nexport type NpFieldCondition = (\n data: Record<string, unknown>,\n siblingData: Record<string, unknown>,\n) => boolean;\n\n/**\n * Serializable condition predicate. Evaluated client-side (admin\n * editor) AND server-side (pipeline validation) from the same\n * declaration — survives the RSC serialization boundary because\n * it's plain JSON.\n *\n * Examples:\n * `{ when: \"kind\", equals: \"doc\" }`\n * `{ when: \"kind\", notEquals: \"doc\" }`\n * `{ when: \"kind\", in: [\"doc\", \"page\"] }`\n * `{ when: \"wpOriginalAuthor\", exists: true }`\n * `{ all: [{ when: \"kind\", equals: \"doc\" }, { when: \"publishedAt\", exists: true }] }`\n * `{ any: [{ when: \"kind\", equals: \"doc\" }, { when: \"kind\", equals: \"page\" }] }`\n *\n * `exists: true` returns true when the value is defined, not null,\n * not the empty string, and not an empty array. `exists: false`\n * is the inverse. `equals` / `notEquals` use strict equality.\n * `in` / `notIn` check membership against an unknown[] list.\n * `all` / `any` are AND / OR over a list of nested expressions.\n */\nexport type NpFieldConditionExpr =\n | { when: string; equals: unknown }\n | { when: string; notEquals: unknown }\n | { when: string; in: unknown[] }\n | { when: string; notIn: unknown[] }\n | { when: string; exists: boolean }\n | { all: NpFieldConditionExpr[] }\n | { any: NpFieldConditionExpr[] };\n\nexport type NpFieldValidator = (\n value: unknown,\n args: { data: Record<string, unknown>; siblingData: Record<string, unknown> },\n) => string | true | Promise<string | true>;\n\nexport type NpRichTextContent = Record<string, unknown>;\n\nexport interface NpEditorConfig {\n features?: string[];\n // Other knobs (e.g. `onUploadImage` for the Insert Image dialog\n // that landed in 9.7j) are typed in `@nexpress/editor`'s own\n // `NpEditorConfig`. Keeping core's version minimal avoids\n // dragging the DOM lib (`File`, `Blob`) into the server-evaluated\n // collection config types.\n}\n\ninterface NpFieldBase {\n name: string;\n label?: string;\n required?: boolean;\n defaultValue?: unknown;\n hidden?: boolean;\n admin?: {\n description?: string;\n placeholder?: string;\n readOnly?: boolean;\n condition?: NpFieldCondition | NpFieldConditionExpr;\n width?: string;\n /**\n * Optional override for the admin field renderer. The default\n * renderer dispatches on `type` (text → input, textarea →\n * textarea, etc.); `kind` overrides that with a specialized\n * widget.\n * - `templatePicker` (Phase 11.3) replaces the input with a\n * dropdown sourced from the active theme's\n * `templates.{collection}` registry.\n * - `title` renders a large borderless headline input that\n * sits above the rest of the form (intended for the\n * primary title of a document). The edit view skips the\n * Card wrapper around it so the title flows naturally\n * into the editor canvas underneath.\n */\n kind?: \"templatePicker\" | \"title\";\n /**\n * Where the field should land in the edit view's two-column\n * layout. Mark publishing-related metadata (SEO, template\n * choice, scheduling inputs) as `\"sidebar\"` so they group\n * with Status / Slug in the sticky right column rather than\n * competing with the primary editing surface.\n *\n * When unset, the legacy heuristic decides: `type: \"date\"`\n * fields, fields with an explicit `admin.width`, and the\n * well-known names `status` / `publishedAt` / `slug` all\n * land in the sidebar; everything else goes to main. An\n * explicit `\"main\"` overrides that heuristic — useful for\n * surfacing a date input in the primary column.\n */\n position?: \"main\" | \"sidebar\";\n /**\n * Sidebar grouping label. Sidebar fields with the same\n * `group` render together in one collapsible Card with the\n * group name as the title. Fields with no group fall into\n * the default \"Publish\" Card. Group order in the rendered\n * sidebar follows first-seen order in the collection's\n * `fields` array — operators control layout by ordering.\n *\n * Examples: `\"Publish\"`, `\"Lead\"`, `\"Taxonomy\"`, `\"Author\"`,\n * `\"Hierarchy\"`, `\"SEO\"`. The group label is the visible\n * Card title (not a slug), so it doesn't have to be\n * machine-friendly.\n *\n * Only meaningful when `position: \"sidebar\"`. Main-column\n * fields ignore this — they render in field-array order\n * without grouping.\n */\n group?: string;\n /**\n * The id of the theme whose `requires.collections.<slug>.fields`\n * contributed this field. Stamped by `mergeThemeRequirements`;\n * never set this from operator config. Same convention as\n * `admin._themeOrigin` at the collection level and per-`kinds`\n * entry: when an operator switches to a different active\n * theme, the admin filters out fields whose origin doesn't\n * match. Operator-declared fields carry no origin and always\n * pass through.\n */\n _themeOrigin?: string;\n };\n validate?: NpFieldValidator;\n}\n\nexport interface NpTextField extends NpFieldBase {\n type: \"text\";\n minLength?: number;\n maxLength?: number;\n unique?: boolean;\n}\n\nexport interface NpTextareaField extends NpFieldBase {\n type: \"textarea\";\n minLength?: number;\n maxLength?: number;\n rows?: number;\n}\n\nexport interface NpNumberField extends NpFieldBase {\n type: \"number\";\n min?: number;\n max?: number;\n step?: number;\n integerOnly?: boolean;\n}\n\nexport interface NpRichTextField extends NpFieldBase {\n type: \"richText\";\n editor?: NpEditorConfig;\n}\n\nexport interface NpBlocksField extends NpFieldBase {\n type: \"blocks\";\n allowedBlocks?: string[];\n minRows?: number;\n maxRows?: number;\n}\n\nexport interface NpCheckboxField extends NpFieldBase {\n type: \"checkbox\";\n defaultValue?: boolean;\n}\n\nexport interface NpDateField extends NpFieldBase {\n type: \"date\";\n /**\n * Default value the Drizzle schema generator emits as a SQL\n * `DEFAULT` clause. Accepted shapes:\n *\n * - `\"now\"` sentinel → `.defaultNow()` → `DEFAULT now()`\n * - A `Date` instance → `.default(new Date(\"<iso>\"))`\n * - Any ISO 8601 string → parsed via `new Date(...)` and\n * emitted identically to the Date case.\n *\n * Anything else is dropped silently at codegen time (mirroring\n * the defensive shape used for other scalar fields). Required\n * NOT NULL date columns paired with `defaultValue: \"now\"` let\n * an `ALTER TABLE … ADD COLUMN` against a populated table\n * succeed without a backfill step.\n */\n defaultValue?: Date | string;\n pickerOptions?: {\n format?: string;\n includeTime?: boolean;\n };\n}\n\nexport interface NpUploadField extends NpFieldBase {\n type: \"upload\";\n relationTo: string;\n}\n\nexport interface NpRelationshipField extends NpFieldBase {\n type: \"relationship\";\n relationTo: string | string[];\n hasMany?: boolean;\n filterOptions?: Record<string, unknown>;\n}\n\nexport interface NpSelectField extends NpFieldBase {\n type: \"select\";\n options: Array<{ label: string; value: string }>;\n hasMany?: boolean;\n}\n\nexport interface NpRadioField extends NpFieldBase {\n type: \"radio\";\n options: Array<{ label: string; value: string }>;\n}\n\nexport interface NpEmailField extends NpFieldBase {\n type: \"email\";\n}\n\nexport interface NpJsonField extends NpFieldBase {\n type: \"json\";\n}\n\nexport interface NpArrayField extends NpFieldBase {\n type: \"array\";\n fields: NpFieldConfig[];\n minRows?: number;\n maxRows?: number;\n}\n\nexport interface NpGroupField extends NpFieldBase {\n type: \"group\";\n fields: NpFieldConfig[];\n}\n\nexport interface NpRowField {\n type: \"row\";\n fields: NpFieldConfig[];\n}\n\nexport interface NpCollapsibleField {\n type: \"collapsible\";\n label: string;\n fields: NpFieldConfig[];\n}\n\nexport type NpFieldConfig =\n | NpTextField\n | NpTextareaField\n | NpNumberField\n | NpRichTextField\n | NpBlocksField\n | NpCheckboxField\n | NpDateField\n | NpUploadField\n | NpRelationshipField\n | NpSelectField\n | NpRadioField\n | NpEmailField\n | NpJsonField\n | NpArrayField\n | NpGroupField\n | NpRowField\n | NpCollapsibleField;\n\n/**\n * Polymorphic actor reference for collection hooks. Phase 9.7o\n * widened the hook surface so plugins can react to member writes,\n * not just staff writes:\n *\n * - `{ kind: \"staff\", user }` — staff-authored write; `user` is\n * the resolved staff session as before.\n * - `{ kind: \"member\", memberId }` — member-authored write\n * (`createMemberDocument` / `updateMemberDocument` /\n * `deleteMemberDocument`).\n *\n * Hooks that only care about staff identity can switch on\n * `principal.kind === \"staff\"` and read `principal.user`. The\n * top-level `user` field is also still passed (`null` for member\n * actors) so existing hooks that destructure `{ user }` keep\n * compiling — they just need to handle the null case now.\n */\n// `NpHookPrincipal` is the historical name plugin authors see in\n// hook payloads. It's the same union as `NpPrincipal`; kept as an\n// alias so existing plugin code keeps compiling (#319).\nimport type { NpPrincipal } from \"../auth/principal.js\";\nexport type { NpPrincipal, NpPrincipal as NpHookPrincipal };\ntype NpHookPrincipal = NpPrincipal;\n\nexport type NpCollectionHook = (args: {\n data: Record<string, unknown>;\n /**\n * Resolved staff session, or `null` when the actor is a member.\n * Pre-9.7o this was always non-null because member writes\n * skipped collection hooks entirely. Hooks that key off staff\n * identity should now switch on `principal.kind` instead.\n */\n user: NpAuthUser | null;\n /** Polymorphic actor — see `NpHookPrincipal`. */\n principal: NpHookPrincipal;\n collection: string;\n originalDoc?: Record<string, unknown> | null;\n}) => Record<string, unknown> | Promise<Record<string, unknown>>;\n\nexport interface NpUploadConfig {\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n imageSizes?: NpImageSize[];\n}\n\nexport interface NpImageSize {\n name: string;\n width: number;\n height?: number;\n crop?: \"center\" | \"top\" | \"bottom\" | \"left\" | \"right\";\n}\n\nexport interface NpCollectionConfig {\n slug: string;\n labels: { singular: string; plural: string };\n slugField?:\n | boolean\n | {\n useField?: string;\n unique?: boolean;\n };\n /**\n * Phase 12.1 — opt this collection into i18n. When set, the\n * codegen adds a `locale` text column and a\n * `translation_group_id` uuid column to the generated table.\n * The slug uniqueness index becomes `(locale, slug)` so the\n * same slug can appear in two locales. Fetching helpers\n * (`findDocuments`, `getDoc`) accept a `locale` option;\n * writes require a `locale` field (the pipeline rejects\n * missing-locale writes with NpValidationError).\n *\n * Requires the top-level `i18n` config to also be set.\n * Without it, `i18n: true` here errors at config validation\n * time — the framework needs to know the locale enum to\n * validate writes.\n */\n i18n?: boolean;\n fields: NpFieldConfig[];\n access?: {\n create?: NpAccessFunction;\n read?: NpAccessFunction;\n update?: NpAccessFunction;\n delete?: NpAccessFunction;\n };\n hooks?: {\n beforeCreate?: NpCollectionHook[];\n afterCreate?: NpCollectionHook[];\n beforeUpdate?: NpCollectionHook[];\n afterUpdate?: NpCollectionHook[];\n beforeDelete?: NpCollectionHook[];\n afterDelete?: NpCollectionHook[];\n beforeRead?: NpCollectionHook[];\n afterRead?: NpCollectionHook[];\n };\n versions?: {\n drafts?: boolean | { autosave?: boolean; autosaveInterval?: number };\n max?: number;\n };\n /**\n * Community features opt-in per collection. Comments are off by\n * default; flip `comments: true` to let members post comments\n * underneath this collection's documents. Reactions ride on the\n * comment surface — sites enable reactions by enabling comments;\n * a per-collection reactions toggle isn't needed today.\n *\n * `memberWrite.create` (9.7a) lets logged-in members create\n * documents in this collection without needing a staff role.\n * `memberWrite.update` / `memberWrite.delete` (9.7b) extend the\n * member-write surface with owner-only edit / delete (the row's\n * `member_author_id` must match the caller). The staff\n * `access.create` / `access.delete` functions are bypassed on\n * the member path — gating is `assertNotBanned(memberId)` plus\n * the opt-in flag plus the ownership check, not the staff\n * access tree. Member-authored docs default to\n * `_status = \"published\"` and members CANNOT change status via\n * update; those transitions remain admin-side affordances\n * (a configurable default-status / moderation gate lands in a\n * follow-up).\n */\n community?: {\n comments?: boolean;\n memberWrite?: {\n create?: boolean;\n update?: boolean;\n delete?: boolean;\n /**\n * Status that member-authored creates land in by default.\n * Defaults to `\"published\"` (a member's thread is live as\n * soon as it's submitted). Set to `\"pending\"` to require a\n * mod to promote the row before it shows up on the public\n * site — a flag-on-write moderation gate without writing a\n * spam adapter. The spam adapter, if installed, can also\n * downgrade an individual row to `pending` regardless of\n * this default (`flag` verdict).\n */\n defaultStatus?: \"published\" | \"pending\";\n };\n };\n /**\n * SEO configuration. Phase 10 introduced this surface for the\n * sitemap / RSS / OG metadata pipeline. The contract is\n * opt-in: a collection appears in `/sitemap.xml` iff it\n * declares `seo.urlPath`, which maps a document row to its\n * public URL path (e.g. `(doc) => \"/blog/\" + doc.slug`).\n * Collections without `seo.urlPath` are assumed to be admin-\n * internal or rendered through a custom route the framework\n * can't introspect.\n */\n seo?: {\n /**\n * Maps a document row to the public URL path the row is\n * served at, or `null` to skip the row (e.g. a draft / a\n * row whose URL is computed dynamically and shouldn't be\n * indexed). Returned paths must start with `/`. The host\n * comes from `SITE_URL` at sitemap-build time.\n */\n urlPath?: (doc: Record<string, unknown>) => string | null;\n /**\n * Hint for sitemap consumers about how often this\n * collection's content changes. Optional — Google now\n * largely ignores it but other crawlers still honor it.\n */\n changefreq?: \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n /**\n * Sitemap priority hint, 0.0–1.0. Optional, same caveat as\n * changefreq.\n */\n priority?: number;\n };\n timestamps?: boolean;\n admin?: {\n listColumns?: string[];\n defaultSort?: string;\n group?: string;\n hidden?: boolean;\n description?: string;\n components?: {\n listView?: string;\n editView?: string;\n createView?: string;\n };\n /**\n * Opts the collection's edit view into the \"In navigation\"\n * side panel. Documents in this collection are addressable\n * from the nav editor's `type: \"page\"` picker via the\n * membership endpoint, so the operator can add/remove the\n * doc from any nav location without leaving the page.\n *\n * Defaults to `false`. The reference `pages` collection in\n * `apps/web` flips it on; sites with a `static-pages` or\n * `landing-pages` collection that should also surface in nav\n * can opt in here too.\n */\n navMembership?: boolean;\n /**\n * Lucide icon name for the admin sidebar entry. Defaults to\n * `FileText` when unset or unrecognized. Examples:\n * `\"Newspaper\"` for posts, `\"FileStack\"` for pages,\n * `\"FolderTree\"` for categories, `\"Tag\"` for tags.\n *\n * Resolved client-side by `admin-shell.tsx` against a small\n * lucide-react registry; unknown names fall back to the\n * default so a typo can't break the sidebar render.\n */\n icon?: string;\n /**\n * Framework-set. Stamped by `mergeThemeRequirements` on\n * collections it synthesised via a theme's\n * `requires.collections.<slug>.createIfAbsent: true`. The\n * admin sidebar uses it to hide collections whose owning\n * theme isn't active — the bundled-themes prebake puts\n * EVERY built-in theme's `createIfAbsent` slug into the\n * schema so swap-from-admin is migration-free, but the\n * operator shouldn't see `authors` in the sidebar while\n * running the docs theme.\n *\n * NEVER set this by hand from operator config. The\n * underscore is intentional — it marks \"this is the\n * framework's view of the config, not the operator's\n * intent\". Operator-declared collections (slug exists\n * before merge) keep this unset and always show.\n */\n _themeOrigin?: string;\n /**\n * Framework-set. Stamped by `mergeThemeRequirements` from\n * `theme.manifest.requires.collections.<slug>.kinds`,\n * unioned across all registered themes. The admin sidebar\n * walks this map to render per-kind entries under \"Content\"\n * (universal-content-model #748).\n *\n * Keyed by the discriminator value declared on the\n * `kind` field's options. Empty / missing → admin shows a\n * single collection entry like before.\n */\n kinds?: Record<string, NpThemeCollectionKind>;\n /**\n * Visual metadata for sidebar field groups. Keyed by the\n * `admin.group` label used on individual fields. The editor's\n * `SidebarGroupCard` reads this to render an icon next to\n * the group title and optionally surface a description.\n *\n * Operator-declared collections set this directly. Themes\n * contribute their own group icons via\n * `requires.collections.<slug>.groupMeta` (merged through\n * `mergeThemeRequirements`, unioned across themes with\n * last-write-wins on per-key props).\n *\n * Groups without an entry render without an icon — same\n * behavior as before this surface existed.\n */\n groupMeta?: Record<string, NpAdminGroupMeta>;\n };\n upload?: NpUploadConfig;\n}\n\nexport interface NpBlockConfig {\n slug: string;\n labels: { singular: string; plural: string };\n fields: NpFieldConfig[];\n imageUrl?: string;\n}\n\nexport type NpBlockInstance = {\n blockType: string;\n [key: string]: unknown;\n};\n\nexport interface NpPluginConfig {\n id: string;\n name: string;\n init?: (ctx: NpPluginContext) => void | Promise<void>;\n}\n\n/**\n * Structural shape accepted by `loadPlugins()` for SDK-built plugins.\n * Declared here rather than imported from `@nexpress/plugin-sdk` to avoid a\n * dependency cycle (plugin-sdk already depends on core).\n */\nexport interface NpResolvedPluginLike {\n manifest: {\n id: string;\n name: string;\n version?: string;\n description?: string;\n capabilities: readonly string[];\n };\n hooks?: Record<string, unknown>;\n routes?: ReadonlyArray<{\n path: string;\n method: string;\n handler: unknown;\n description?: string;\n auth?: boolean;\n }>;\n /**\n * Phase 12.5 — optional UI string bundles per locale. Keys\n * are plugin-namespaced strings the plugin's own templates /\n * routes / admin pages call `t()` against. The host merges\n * every plugin's bundle into the global registry at boot;\n * later plugins overwrite earlier ones on key collision so\n * sites can layer overrides via plugin order.\n */\n i18n?: Record<string, Record<string, string>>;\n /**\n * Phase 14.5 — page templates the plugin contributes to the\n * shared template registry. Same shape as a theme's\n * `impl.templates`: keyed by collection slug, then by\n * template id, with `{ label, description?, component }`\n * values. The plugin host merges these at boot;\n * `getThemeTemplateSummaries` returns plugin templates +\n * theme templates as a union, with theme entries winning\n * id collisions (the active theme is the site's design\n * authority).\n *\n * templates: {\n * pages: {\n * docs: { label: \"Documentation\", component: DocsTemplate },\n * },\n * }\n */\n templates?: Record<string, Record<string, unknown>>;\n /**\n * Plugin page routes (#623). React-free shape — the framework\n * narrows `component` to `ComponentType<NpRouteRenderProps>`\n * at the dispatcher site. See\n * `docs/design/plugin-routes.md` for the contract +\n * precedence rules.\n */\n pageRoutes?: ReadonlyArray<{\n pattern: string;\n component: unknown;\n metadata?: unknown;\n surface?: \"site\" | \"member\";\n locale?: \"auto\" | \"none\";\n }>;\n}\n\nexport interface NpPluginContext {\n addCollection: (config: NpCollectionConfig) => void;\n addBlock: (config: NpBlockConfig) => void;\n addHook: (collection: string, event: string, hook: NpCollectionHook) => void;\n}\n\nexport interface NpNavItem {\n id: string;\n label: string;\n type: \"link\" | \"collection\" | \"page\";\n url?: string;\n collection?: string;\n /**\n * Set when `type === \"page\"` to record which collection the\n * referenced doc lives in. Defaults to `\"pages\"` when absent so\n * existing nav rows keep resolving against the reference page\n * collection unchanged. The URL resolver walks the doc through\n * the collection's `seo.urlPath` to produce the public path.\n *\n * The editor doesn't expose this as an editable field — the\n * panel that adds the item knows its source collection and\n * stamps it at write time.\n */\n collectionSlug?: string;\n pageId?: string;\n children?: NpNavItem[];\n}\n\n/**\n * Phase 11.1 — theme manifest. Pure metadata, kept React-free\n * so it can live in `@nexpress/core` (which is server-only and\n * intentionally has no React peer). The full theme — shell,\n * slots, templates with React component types — lives in\n * `@nexpress/theme` via `defineTheme()`. The registry stores\n * `NpRegisteredTheme` instances; `impl` is opaque to core but\n * typed for consumers downstream.\n */\nexport interface NpThemeManifest {\n id: string;\n name: string;\n version: string;\n description?: string;\n author?: { name: string; url?: string };\n /** Optional minimum NexPress version this theme requires. */\n nexpress?: { minVersion?: string };\n /**\n * Phase F.1 (theme v0.2) — declared data-shape requirements.\n *\n * Themes whose components assume specific collection fields\n * (e.g. magazine theme reads `posts.featured`) declare them\n * here. Two consumers read this:\n *\n * 1. `defineConfig` calls `mergeThemeRequirements` to UNION\n * declared fields into the operator's `collections` array\n * at config-resolution time. Operator-authored fields with\n * the same name always win, so the merge is non-\n * destructive. The framework's codegen then picks up the\n * union shape; the operator's next `pnpm db:generate &&\n * pnpm db:migrate` materialises the columns. Operators\n * add a theme via `pnpm nexpress theme add <pkg>` — there\n * is no longer a `theme:install` AST-patcher that touches\n * `src/collections/*.ts` (that flow was retired alongside\n * the auto-merge).\n * 2. Admin theme switcher: compares against the resolved\n * collections at activation time and surfaces residual\n * mismatches — chiefly TYPE conflicts where the operator\n * declared a field with the same name but a different\n * `type` and the merge respected the operator's choice.\n */\n requires?: {\n collections?: Record<string, NpThemeCollectionRequirement>;\n };\n /**\n * Phase F.3 (theme v0.2) — operator-tunable theme options.\n *\n * A Zod schema describing settings the admin should expose as\n * a form. The framework generates the form fields from the\n * schema (no per-theme admin UI code), persists submissions in\n * `np_settings` keyed by `theme.settings:<themeId>`, and\n * exposes the parsed value to theme components via\n * `getThemeSettings()`.\n *\n * Supported field types in v0.2:\n * - z.string() / z.string().url() / z.string().regex(...)\n * - z.number().int().min().max()\n * - z.boolean()\n * - z.enum([...])\n * - z.array(z.object({...}))\n * - z.object({...})\n *\n * Use `.default(value)` for initial form values and\n * `.describe(\"Help text\")` for the field label/description\n * the admin auto-form picks up.\n *\n * Typed as `unknown` here so `@nexpress/core` doesn't have to\n * re-export Zod type unions through every public surface;\n * theme authors writing `defineTheme({ manifest: { ... } })`\n * still get the proper Zod typing because they construct the\n * schema with `z.object(...)` themselves. The framework\n * narrows back to `ZodTypeAny` at the call site that runs\n * introspection / validation.\n */\n settingsSchema?: unknown;\n /**\n * v0.3 (D) — settings schema version, used by the migration\n * pipeline to detect when stored settings need upgrading.\n *\n * Theme authors bump this whenever `settingsSchema` changes\n * shape in a non-additive way (renaming a field, removing one,\n * tightening a default). Adding a NEW optional field is\n * compatible without bumping — Zod fills the missing key with\n * the field's default on parse.\n *\n * The framework treats absent / undefined as `1` (the v0.2\n * baseline). Themes that never bump stay forever at v1, no\n * migration ever runs.\n */\n settingsVersion?: number;\n /**\n * v0.3 (D) — migration function that brings a value persisted\n * under an older `settingsVersion` up to the current shape.\n *\n * Called on read when stored version < `settingsVersion`. The\n * function receives the OLD value (whatever shape v(N-1) had)\n * and the version it came from (so multi-step migrations can\n * branch). Returns a value that matches the CURRENT\n * `settingsSchema`. The framework re-parses the result and\n * falls back to schema defaults if the migration's output\n * still doesn't validate (defensive — a buggy migrate fn\n * shouldn't blow up the public site).\n *\n * The framework persists the migrated value back on the\n * operator's NEXT save through the admin form. Read paths\n * don't auto-write; the migration is recomputed on each read\n * until the operator triggers a save. That keeps read paths\n * pure (matches every other cached read in the framework).\n *\n * Example for a `accent` → `accentColor` rename at v2:\n *\n * ```ts\n * defineTheme({\n * manifest: {\n * settingsSchema: z.object({\n * accentColor: z.string().regex(...).optional(),\n * ...\n * }),\n * settingsVersion: 2,\n * settingsMigrate: (old, from) => {\n * if (from === 1) {\n * const o = old as { accent?: string };\n * return { ...o, accentColor: o.accent };\n * }\n * return old;\n * },\n * }\n * })\n * ```\n */\n settingsMigrate?: (old: unknown, fromVersion: number) => unknown;\n}\n\n/**\n * One collection's worth of theme requirements. The collection\n * may exist (the framework's auto-merge appends fields to the\n * existing array) or not (the merge skips it unless\n * `createIfAbsent` is set, in which case a minimal collection is\n * synthesised on the resolved config).\n */\nexport interface NpThemeCollectionRequirement {\n fields?: Record<string, NpThemeFieldRequirement>;\n /** True → the framework's `mergeThemeRequirements` step in\n * `defineConfig` synthesises a minimal collection (slug +\n * labels + the declared fields) when no collection with this\n * slug is registered. Operator-authored collections of the\n * same slug always take precedence. */\n createIfAbsent?: boolean;\n /**\n * Per-kind metadata for the `kind` discriminator field\n * (universal-content-model #748). Themes contribute one entry\n * per kind they author content for; the framework's auto-merge\n * unions entries across registered themes so the admin sidebar\n * and the public-site router both see one canonical map.\n *\n * Keyed by the option value declared on `fields.kind.options`\n * (e.g. `kinds.doc` matches the option whose `value: \"doc\"`).\n * The collection slug remains `posts`; kinds are a presentation\n * split, not a separate table.\n *\n * The `kind` field itself doesn't have to live on this\n * collection's `fields` for the metadata to apply — themes that\n * extend a single kind (`fields.kind.options: [{value:\"doc\"}]`\n * + `kinds.doc: {...}`) ship both together and the merge unions\n * them with whatever other themes declare. A `kinds` block on a\n * collection without a corresponding select field is a no-op\n * (the admin shows the regular collection list view).\n */\n kinds?: Record<string, NpThemeCollectionKind>;\n /**\n * Sidebar group metadata the theme contributes. Keyed by the\n * `admin.group` label the theme uses on its contributed fields\n * (e.g. theme-magazine contributes `Magazine: { icon: \"Newspaper\" }`).\n * Merged into the collection's `admin.groupMeta` via\n * last-write-wins union — two themes claiming the same group\n * label get the later theme's icon / description.\n *\n * Declaring a group key without contributing fields with the\n * same `admin.group` is allowed (the entry is unused but\n * harmless) — useful for overriding a framework default's\n * icon without adding any new fields.\n */\n groupMeta?: Record<string, NpAdminGroupMeta>;\n}\n\n/**\n * One kind entry — admin nav + public URL metadata for a single\n * discriminator value on a `select` field (typically `posts.kind`).\n *\n * Field merge: two themes declaring the same kind value get\n * last-wins on every property. Operators rarely need to redefine\n * a kind their theme already ships.\n */\n/**\n * Per-group sidebar metadata. Resolves at runtime in the admin\n * edit view; not codegen'd into the DB schema.\n */\nexport interface NpAdminGroupMeta {\n /**\n * Lucide icon name (no `Icon` suffix) shown next to the\n * group title in the editor sidebar. Examples: `\"Calendar\"`,\n * `\"BookOpen\"`, `\"Briefcase\"`. Resolved client-side; unknown\n * names render no icon (silent fallback, no warning).\n */\n icon?: string;\n /**\n * One-line description shown beneath the group title. Useful\n * for operator hints like \"Search-result preview + social\n * card.\" Truncated by the admin if it's long.\n */\n description?: string;\n}\n\nexport interface NpThemeCollectionKind {\n /** Singular human label — \"Doc\", \"Project\", \"Article\". */\n label: string;\n /** Plural label for the admin sidebar entry — \"Documentation\". */\n labelPlural: string;\n /** Lucide icon name (no `Icon` suffix) — \"BookOpen\", \"Briefcase\". */\n icon?: string;\n /**\n * Public-site URL pattern. `:slug` is the only supported param;\n * the catch-all router (`apps/web/src/app/(site)/[[...slug]]/page.tsx`)\n * matches the path, extracts the slug, and queries the host\n * collection with `where: { kind: \"<this-key>\", slug: \"<match>\" }`.\n *\n * Omit to fall back to the framework default (`/<collection-slug>/<slug>`,\n * shared with the kind=null catch-all path). Two kinds declaring\n * the same urlPattern collide and the first wins; the admin\n * surfaces this via the requirements diff.\n */\n urlPattern?: string;\n /**\n * True → admin's per-kind list view surfaces `parent` + `order`\n * controls and renders rows as a tree. Themes with hierarchical\n * content (docs, sections) opt in; flat kinds leave it false.\n */\n hierarchical?: boolean;\n /**\n * Framework-set. Stamped by `mergeThemeRequirements` with the\n * id of the theme whose `requires.collections.<slug>.kinds`\n * contributed this entry. The admin sidebar reads it to gate\n * per-kind nav entries on the active theme — the bundled-themes\n * prebake unions every built-in's kinds onto the schema, but\n * only the active theme's kinds deserve sidebar real estate.\n *\n * NEVER set this by hand from operator config. The underscore\n * is intentional — same convention as `admin._themeOrigin` at\n * the collection level.\n */\n _themeOrigin?: string;\n}\n\n/**\n * One field's requirement. The `type` matches an `NpFieldConfig`\n * variant's `type` string exactly so the activation check can\n * compare without translation.\n */\nexport interface NpThemeFieldRequirement {\n type:\n | \"text\"\n | \"textarea\"\n | \"richText\"\n | \"number\"\n | \"checkbox\"\n | \"date\"\n | \"select\"\n | \"upload\"\n | \"relationship\"\n | \"blocks\";\n /** For `relationship` — the collection slug it points to. */\n relationTo?: string | string[];\n /** For `relationship` / `select` — accepts list values. */\n hasMany?: boolean;\n required?: boolean;\n /**\n * Default `true`. Set `false` for \"nice to have, theme degrades\n * gracefully without it\" — admin warning shows but at lower\n * severity, and a future F.8 may treat it as opt-in patch.\n */\n hard?: boolean;\n /**\n * For `select` only — extra options to union into the existing\n * select field. Two themes can contribute disjoint option sets\n * (e.g. theme-docs adds `kind=\"doc\"`, theme-portfolio adds\n * `kind=\"project\"`); the merge dedupes on `value` and last-wins\n * on `label`. Universal-content-model Phase U.1 (#748).\n *\n * Ignored when the merge can't find an existing select with the\n * same `name`; theme authors that need a brand-new select can't\n * synthesise one through requirements (`NpThemeFieldRequirement`\n * doesn't carry enough to construct a valid `NpSelectField`).\n */\n options?: Array<{ label: string; value: string }>;\n /**\n * Optional admin hints forwarded onto the synthesised field's\n * `admin` slot. Themes use these to bucket their contributed\n * fields into the right sidebar group and hide fields when\n * irrelevant to the active kind.\n *\n * `group` — sidebar Card grouping label\n * (e.g. `\"Media\"`, `\"SEO\"`).\n * `condition` — runtime visibility gate. Either a function\n * (server-only — stripped at the RSC boundary)\n * or a serializable expression (works in both\n * environments). Prefer the expression form so\n * the admin's client renderer can re-evaluate\n * on live form values:\n * `{ when: \"kind\", equals: \"doc\" }`\n * `{ when: \"kind\", notEquals: \"doc\" }`\n * `{ when: \"kind\", in: [\"doc\", \"page\"] }`\n * `{ when: \"wpOriginalAuthor\", exists: true }`\n * `position` — main vs sidebar column. Defaults to the\n * framework's `isSidebarField` heuristic.\n */\n admin?: {\n group?: string;\n condition?: NpFieldCondition | NpFieldConditionExpr;\n position?: \"main\" | \"sidebar\";\n };\n}\n\nexport interface NpRegisteredTheme {\n manifest: NpThemeManifest;\n /**\n * The theme's runtime implementation — shell component,\n * slot components, page templates, default tokens.\n * `@nexpress/theme` types this; core treats it as opaque so\n * the React peer dependency stays out of this package.\n */\n impl: unknown;\n}\n\nexport interface NpI18nConfig {\n /**\n * Locales this site supports. Order matters only insofar as\n * the first locale becomes the default when `defaultLocale`\n * isn't explicitly set. Locale strings are passed through to\n * BCP-47 consumers (HTML `lang` attribute, hreflang) so\n * conventional codes are recommended (`en`, `en-US`, `ko`,\n * `pt-BR`).\n */\n locales: string[];\n /**\n * Locale used when the caller doesn't specify one — drives\n * default writes and fallback reads. Must appear in `locales`.\n */\n defaultLocale: string;\n}\n\nexport interface NpConfig {\n site: {\n name: string;\n url: string;\n };\n db: {\n connectionString: string;\n pool?: { max?: number };\n };\n storage?: {\n adapter: \"local\" | \"s3\";\n local?: { directory: string; baseUrl: string };\n s3?: { bucket: string; region: string; endpoint?: string };\n };\n collections: NpCollectionConfig[];\n blocks?: NpBlockConfig[];\n editor?: NpEditorConfig;\n /**\n * Phase 11.1 — multi-theme registry. Sites declare every\n * theme they want available; admins switch between them\n * via the settings UI without rebuilding. The first theme\n * in the array is the default-active until an admin sets\n * a different one (`np_settings.activeTheme`).\n */\n themes?: NpRegisteredTheme[];\n /**\n * Phase 12.1 — i18n config. Sites that want multi-language\n * content declare every locale they intend to support here.\n * Per-collection opt-in via `defineCollection({ i18n: true })`\n * is required: only collections that declare `i18n` get the\n * `locale` / `translation_group_id` columns codegen'd onto\n * their generated table. Sites with no i18n config (or that\n * opt no collections in) keep the existing single-locale\n * shape — i18n is purely additive.\n *\n * i18n: { locales: [\"en\", \"ko\", \"ja\"], defaultLocale: \"en\" }\n *\n * `defaultLocale` is what new docs land in when the caller\n * doesn't pass an explicit locale, and what the framework\n * falls back to when a translation is missing for a requested\n * locale (the public site renders a 404 only when the doc\n * doesn't exist in any locale).\n */\n i18n?: NpI18nConfig;\n images?: {\n sizes?: NpImageSize[];\n format?: \"webp\" | \"avif\" | \"jpeg\" | \"png\";\n quality?: number;\n };\n auth?: {\n secret: string;\n tokenExpiration?: number;\n refreshTokenExpiration?: number;\n maxLoginAttempts?: number;\n lockoutDuration?: number;\n };\n plugins?: Array<NpPluginConfig | NpResolvedPluginLike>;\n typescript?: {\n outputFile?: string;\n };\n /**\n * Phase 23.5 — operational thresholds and policies for the job\n * queue. Currently only carries the stuck-job thresholds the\n * admin Jobs widget compares against; future entries land\n * additively.\n */\n jobs?: {\n /**\n * Per-state count thresholds for the admin stuck-job widget.\n * When the live + archive UNION count for a state exceeds the\n * configured value the widget shows a warning indicator. Unset\n * values fall back to sensible defaults applied by the widget\n * itself (currently `failed: 10`, `expired: 50`).\n */\n stuckThreshold?: {\n failed?: number;\n expired?: number;\n };\n };\n}\n\nexport type NpJobType =\n | \"content:afterSave\"\n | \"content:afterDelete\"\n | \"content:publishScheduled\"\n | \"media:processImage\"\n | \"media:cleanup\"\n | \"plugin:scheduledTask\"\n | \"system:revisionPrune\"\n | \"system:sessionCleanup\"\n | \"system:jobLogPrune\"\n | \"auth:sendPasswordReset\"\n | \"members:sendVerifyEmail\"\n | \"members:sendPasswordReset\"\n | \"notifications:sendDigest\";\n\n/**\n * System-level filters that aren't part of any collection's\n * document shape but still belong on the `where` clause: tenant\n * scoping, visibility gating, locale narrowing. Kept separate\n * from the document type so `Partial<T>` can stay tight while\n * advanced callers (admin queries, bulk exports) can pass these\n * escape-hatch tokens.\n */\nexport interface NpFindWhereSystemTokens {\n /**\n * Multi-site scoping. Defaults to the resolved current site.\n * Pass `\"*\"` to query across every site (admin / migration\n * use only — leaks cross-site rows).\n */\n siteId?: string;\n /**\n * Visibility gate. Anonymous traffic is auto-restricted to\n * `\"public\"`. Pass `\"*\"` to bypass (the pipeline drops the\n * filter when a user is also passed).\n */\n visibility?: \"public\" | \"private\" | \"*\";\n /**\n * `where: { locale: \"ko\" }` is equivalent to the top-level\n * `locale` option. Listed here so a typed where clause can\n * still pass it without the document type having to declare\n * a `locale` field (only i18n-enabled collections do).\n */\n locale?: string;\n /**\n * Lifecycle status filter. Every collection in the framework\n * carries a `status` column (codegen-enforced), so exposing it\n * here lets a typed `where` clause filter to published rows\n * without the doc type having to redeclare it. Accepts a\n * single value or an array (IN match).\n */\n status?: NpDocumentStatus | NpDocumentStatus[];\n}\n\n/**\n * Strip `null` and unwrap arrays so a hasMany field like\n * `categories: string[] | null` reads as a `string` for the\n * single-target filter case.\n */\ntype NpFindWhereUnwrap<V> = V extends (infer U)[] | null\n ? U\n : V extends (infer U)[]\n ? U\n : V extends infer U | null\n ? U\n : V;\n\n/**\n * The accepted value shape for a single where field. Either the\n * unwrapped scalar (single match) or an array (IN match). Array\n * with zero elements short-circuits the query to no rows; the\n * pipeline guards against the SQL syntax error this would\n * otherwise produce.\n */\ntype NpFindWhereValue<V> = NpFindWhereUnwrap<V> | NpFindWhereUnwrap<V>[];\n\n/**\n * Per-row filter. With the default `T = Record<string, unknown>`,\n * any keys are allowed (back-compat). With a typed `T` (the\n * generated wrapper functions pass their `${Pascal}Document`\n * here), only document fields plus the system tokens above are\n * accepted — typos against field names become compile errors.\n *\n * Each field accepts a single value (matched with `=`) or an\n * array (matched with `IN (...)`). For hasMany relationships\n * (where the document's field type is `string[] | null`), the\n * single-value form is the common case — \"posts in this one\n * category\" — and the array form picks up the `OR` semantics\n * across multiple targets — \"posts in any of these categories\".\n */\nexport type NpFindWhere<T extends object = Record<string, unknown>> = {\n [K in keyof T]?: NpFindWhereValue<T[K]>;\n} & {\n [K in keyof NpFindWhereSystemTokens]?: NpFindWhereSystemTokens[K];\n};\n\nexport interface NpFindOptions<T extends object = Record<string, unknown>> {\n page?: number;\n limit?: number;\n sort?: string;\n search?: string;\n where?: NpFindWhere<T>;\n /**\n * Phase 12.1 — restrict the result set to one locale on\n * i18n-enabled collections. Equivalent to passing\n * `where: { locale }`, but kept top-level for ergonomics\n * (callers don't have to know it's a column). Ignored on\n * non-i18n collections (no `locale` column to match).\n */\n locale?: string;\n}\n\nexport interface NpFindResult<T = Record<string, unknown>> {\n docs: T[];\n totalDocs: number;\n totalPages: number;\n page: number;\n limit: number;\n hasNextPage: boolean;\n hasPrevPage: boolean;\n}\n\n/**\n * Document lifecycle status. `pending` (Phase 9.7c) is a moderation\n * holding pen for member-authored docs that haven't cleared review\n * — flagged by the spam adapter or sent there because the\n * collection set `community.memberWrite.defaultStatus = \"pending\"`.\n * Public listings filter to `published`, so pending rows are\n * invisible to anonymous and non-staff members until a mod\n * promotes them.\n */\nexport type NpDocumentStatus = \"draft\" | \"scheduled\" | \"published\" | \"archived\" | \"pending\";\n\nexport interface NpSaveOptions {\n status?: NpDocumentStatus;\n /**\n * Caller-owned Drizzle transaction handle. When provided, all\n * reads + writes in the save flow run against this transaction\n * instead of opening a private one — useful for batching many\n * saves (the seed loop) so a mid-batch failure rolls back the\n * pending writes as a unit.\n *\n * Typed as `unknown` here to avoid a circular import between\n * `config/types.ts` and `collections/pipeline.ts` (where the\n * structural type lives). Callers pass the `NpTransaction`\n * value exported from `@nexpress/core`; the pipeline narrows\n * with an internal cast at the boundary.\n *\n * Pre-write hooks still fire OUTSIDE the tx (they shouldn't see\n * pending state). Post-commit hooks (`content:afterSave` job,\n * plugin `content:afterCreate` / `content:afterUpdate`) fire\n * after the inner persist block but before the caller's outer\n * tx commits — their side effects can diverge from final DB\n * state on rollback. Same trade-off as `deleteDocument({ tx })`.\n */\n tx?: unknown;\n}\n\nexport interface NpSaveResult {\n doc: Record<string, unknown>;\n operation: \"create\" | \"update\";\n}\n\n/**\n * Numeric ranking of staff roles, retained for the few non-capability\n * call sites that still need to compare role rank — chiefly\n * `hasRoleOnSite()` in `sites/memberships.ts`, which evaluates a\n * per-site membership row's role against the user's. `moderator`\n * shares author-rank because the two are parallel tracks\n * (community-mod vs. content-author authority); the rank is meaningful\n * only on the content-authoring axis.\n *\n * For staff-user authorization, use `can(user, capability)` from\n * `auth/capabilities.ts` (#273) — this hierarchy is no longer the\n * primary check.\n */\nexport const ROLE_HIERARCHY: Record<NpUserRole, number> = {\n viewer: 0,\n author: 1,\n moderator: 1,\n editor: 2,\n admin: 3,\n};\n","import { ZodError } from \"zod\";\n\nimport { mergeThemeRequirements } from \"../themes/merge-requirements.js\";\nimport { type NpConfig } from \"./types.js\";\nimport { npConfigSchema } from \"./validation.js\";\n\n/**\n * Validates the project's NpConfig against the declarative schema and returns\n * it unchanged on success. Catches common mistakes (bad collection slug,\n * missing auth.secret, malformed storage adapter, etc.) at module-eval time\n * with a clear message instead of a cryptic runtime failure once the app\n * tries to boot.\n *\n * The most common boot trip-up by far is \"auth.secret\" / \"site.url\" /\n * \"db.connectionString\" missing on a fresh install. We translate Zod's raw\n * `String must contain at least 1 character` style messages into actionable\n * \"set NP_SECRET in .env, or run `pnpm run setup`\" hints so the new operator\n * isn't googling Zod path strings.\n *\n * Unknown plugin entries are accepted here — the plugin loader does the\n * deeper validation of manifests against @nexpress/plugin-sdk.\n *\n * After validation, theme requirements are auto-merged into the\n * `collections` array via `mergeThemeRequirements`. Operators no\n * longer need to AST-patch `src/collections/*.ts` when adopting a\n * theme — adding the theme to `themes: [...]` is enough; the next\n * `pnpm db:generate && pnpm db:migrate` picks up the new\n * theme-declared columns.\n */\nexport function defineConfig(config: NpConfig): NpConfig {\n try {\n npConfigSchema.parse(config);\n } catch (err) {\n if (err instanceof ZodError) {\n throw new Error(formatConfigError(err), { cause: err });\n }\n throw err;\n }\n\n // Phase 12.1 cross-field check — a collection can only opt\n // into i18n if the top-level i18n config is set. The schema\n // can't express this with `.refine()` cleanly because it\n // would force every collection to know the parent config.\n if (config.i18n === undefined) {\n const localized = config.collections.find((c) => c.i18n === true);\n if (localized) {\n throw new Error(\n `Collection \"${localized.slug}\" sets i18n: true but the top-level config has no \\`i18n\\` block. Add \\`i18n: { locales: [...], defaultLocale: \"...\" }\\` to nexpress.config.ts.`,\n );\n }\n }\n\n // Theme auto-merge. Non-destructive: operator-authored fields\n // are never overwritten, and a no-op when no themes (or no\n // themes with `requires`) are registered. Returns the input\n // array unchanged in that case, so the equality semantics\n // existing callers rely on hold.\n const mergedCollections = mergeThemeRequirements(config.collections, config.themes);\n if (mergedCollections === config.collections) {\n return config;\n }\n return { ...config, collections: mergedCollections };\n}\n\nconst FRIENDLY_HINTS: Record<string, string> = {\n \"auth.secret\":\n \"Set `NP_SECRET` in `.env` (≥32 random chars) — `pnpm run setup` will generate one for you.\",\n \"site.url\":\n \"Set `SITE_URL` in `.env` to your public origin — `pnpm run setup` collects it.\",\n \"db.connectionString\":\n \"Set `DATABASE_URL` in `.env` to your Postgres connection string — `pnpm run setup` will write it.\",\n \"storage.s3.bucket\": \"Set `NP_S3_BUCKET` in `.env` (or switch storage to local).\",\n \"storage.s3.region\": \"Set `NP_S3_REGION` in `.env`.\",\n};\n\nfunction formatConfigError(err: ZodError): string {\n const lines = err.issues.map((issue) => {\n const path = issue.path.join(\".\");\n const hint = FRIENDLY_HINTS[path];\n if (hint) return ` • ${path}: ${hint}`;\n return ` • ${path || \"<root>\"}: ${issue.message}`;\n });\n return [\n \"Invalid NexPress config — boot aborted before any service starts.\",\n \"\",\n ...lines,\n \"\",\n \"If this is your first run, `pnpm run setup` writes a working `.env`.\",\n ].join(\"\\n\");\n}\n","import type {\n NpCollectionConfig,\n NpFieldConfig,\n NpRegisteredTheme,\n NpThemeCollectionKind,\n NpThemeCollectionRequirement,\n NpThemeFieldRequirement,\n} from \"../config/types.js\";\nimport { getScopedLogger } from \"../observability/logger.js\";\n\n/**\n * Auto-merge themes' `manifest.requires.collections` into the\n * operator-authored `collections` array at config-resolution\n * time.\n *\n * Why this exists: the v0.2 theme contract lets themes declare\n * the collection fields their components depend on\n * (`requires.collections.<slug>.fields.<name>`). Pre-#F-track this\n * was wired up exclusively through `pnpm nexpress theme:install`,\n * which AST-patched the operator's `src/collections/*.ts`. That\n * \"code-write\" felt heavyweight for what is, conceptually, a\n * package add — and surfaced bugs (#604, #605, #606) at the\n * intersection of file naming, registration, and missing\n * directories.\n *\n * The framework-side merge replaces it: as soon as the operator\n * adds a theme to `themes: [...]`, `defineConfig` walks each\n * theme's `requires.collections` and:\n *\n * - For each existing collection slug, appends the theme's\n * fields to that collection's `fields` array (only when the\n * operator hasn't already declared a field with the same\n * name — operator wins).\n * - For each slug that doesn't yet exist AND the theme set\n * `createIfAbsent: true`, synthesises a minimal\n * `NpCollectionConfig` and pushes it onto `collections`.\n *\n * The merge is non-destructive: operator-authored fields are\n * never overwritten or reshaped. Theme-injected fields default\n * to `required: false` regardless of `req.required` because they\n * land on existing rows that don't have a value yet — a hard NOT\n * NULL constraint would block the very next migration. Theme\n * authors that NEED a value can validate in their template /\n * read path. Inheriting `hard: false` from the requirement just\n * means \"lower-severity admin warning\"; field-level `required`\n * is the runtime/codegen concern.\n *\n * The next `pnpm db:generate && pnpm db:migrate` picks up the\n * injected columns. From the operator's perspective: add a theme\n * to `themes:`, run the two-command migration, done.\n */\n\n/**\n * Resolved lazily so `setLogger()` calls made AFTER this module\n * is first imported (e.g. tests, host apps that swap to pino at\n * boot) still feed the merge's warnings. The scoped bindings\n * are constant, so re-creating per call is cheap.\n */\nfunction log(): ReturnType<typeof getScopedLogger> {\n return getScopedLogger({ component: \"config:theme-merge\" });\n}\n\n/**\n * Field requirement types we know how to synthesise into a\n * concrete `NpFieldConfig` at merge time. `select` is excluded:\n * `NpThemeFieldRequirement` has no `options` array, and the\n * validation schema (`fieldSchema`) requires `options.min(1)`,\n * so a synthesised select would crash boot. `upload` lands here\n * but requires `relationTo` on the requirement itself; we skip\n * with a warning when it's absent.\n */\nconst SUPPORTED_REQUIREMENT_TYPES = new Set<NpThemeFieldRequirement[\"type\"]>([\n \"text\",\n \"textarea\",\n \"richText\",\n \"number\",\n \"checkbox\",\n \"date\",\n \"upload\",\n \"relationship\",\n \"blocks\",\n]);\n\n/**\n * Translate a single field requirement into a concrete\n * `NpFieldConfig`. Returns null when the requirement can't be\n * sensibly materialised without further input (e.g. `select`\n * without options, `upload` without `relationTo`). The caller\n * logs a warning so the operator sees what was skipped.\n */\n/**\n * Build the `admin` slot for a synthesised field from the theme\n * requirement's `admin` hints. Only forwards keys that the\n * field-base type accepts so a theme-author typo doesn't\n * silently land in the runtime config.\n *\n * Typed as `Record<string, unknown>` because `NpFieldConfig` is\n * a discriminated union with at least one variant (row /\n * collapsible) that lacks the `admin` slot — the union type\n * `NpFieldConfig[\"admin\"]` doesn't resolve. The runtime shape is\n * the same `NpFieldBase.admin` object for every field that\n * accepts it; we're just side-stepping a TypeScript union\n * narrowing limitation.\n */\nfunction buildAdminSlot(\n req: NpThemeFieldRequirement,\n themeId: string,\n): Record<string, unknown> | undefined {\n const out: Record<string, unknown> = { _themeOrigin: themeId };\n if (req.admin?.group !== undefined) out.group = req.admin.group;\n if (req.admin?.condition !== undefined) out.condition = req.admin.condition;\n if (req.admin?.position !== undefined) out.position = req.admin.position;\n // `_themeOrigin` always present — the admin gates fields by it\n // so the bundled-themes prebake doesn't surface Portfolio's\n // sidebar groups when Magazine is the active theme. Tag every\n // theme-contributed field, even ones the operator's config\n // didn't ask for a separate admin slot.\n return out;\n}\n\nfunction requirementToField(\n name: string,\n req: NpThemeFieldRequirement,\n themeId: string,\n): NpFieldConfig | null {\n const admin = buildAdminSlot(req, themeId);\n switch (req.type) {\n case \"text\":\n case \"textarea\":\n case \"richText\":\n case \"number\":\n case \"checkbox\":\n case \"date\":\n case \"blocks\":\n // Plain scalar/blob fields — type + name is enough. We\n // deliberately do NOT forward `req.required: true` onto\n // the field. Theme-injected fields land on existing rows\n // that have no value; making them NOT NULL blocks the\n // very next migration. Theme authors that need a non-null\n // guarantee enforce it in their template/read path.\n return admin ? { type: req.type, name, admin } : { type: req.type, name };\n case \"upload\": {\n if (!req.relationTo || Array.isArray(req.relationTo)) {\n log().warn(\n \"Skipping theme-required upload field: requirement is missing a scalar `relationTo`.\",\n { field: name, relationTo: req.relationTo },\n );\n return null;\n }\n const base = {\n type: \"upload\" as const,\n name,\n relationTo: req.relationTo,\n };\n return admin ? { ...base, admin } : base;\n }\n case \"relationship\": {\n if (!req.relationTo) {\n log().warn(\n \"Skipping theme-required relationship field: requirement is missing `relationTo`.\",\n { field: name },\n );\n return null;\n }\n const baseField = {\n type: \"relationship\" as const,\n name,\n relationTo: req.relationTo,\n };\n const withMany = req.hasMany ? { ...baseField, hasMany: true } : baseField;\n return admin ? { ...withMany, admin } : withMany;\n }\n case \"select\": {\n // A theme can synthesise a select when it provides at least\n // one option on the requirement (universal-content-model\n // Phase U.1 #748). Without options the field schema would\n // reject the synthesised config (`options.min(1)`), so a\n // contribution with no options is still a no-op + warning.\n // Themes that contribute options to an EXISTING select don't\n // come through here — that path unions options inside\n // `mergeThemeRequirements`.\n if (!req.options || req.options.length === 0) {\n log().warn(\n \"Skipping theme-required select field: requirement is missing an `options` list.\",\n { field: name },\n );\n return null;\n }\n const base = {\n type: \"select\" as const,\n name,\n options: [...req.options],\n };\n return admin ? { ...base, admin } : base;\n }\n default: {\n // Exhaustiveness — adding a requirement type forces an\n // update here. Cast through `never` so a missed case is a\n // compile error, not a silent skip.\n const _exhaustive: never = req.type;\n void _exhaustive;\n log().warn(\"Unknown theme field requirement type; skipping.\", {\n field: name,\n type: req.type as unknown as string,\n });\n return null;\n }\n }\n}\n\n/**\n * Universal-content-model Phase U.1 (#748): union two select\n * option lists. Dedupe on `value` (so two themes contributing\n * the same kind don't double up); last-wins on `label` (theme B\n * loaded after theme A can re-label the shared option).\n *\n * Returns the original `base` array unchanged when the\n * contribution is empty or adds no new values — callers use\n * referential equality to decide whether to clone the field.\n */\nfunction mergeSelectOptions(\n base: ReadonlyArray<{ label: string; value: string }>,\n contribution: ReadonlyArray<{ label: string; value: string }>,\n): Array<{ label: string; value: string }> | ReadonlyArray<{ label: string; value: string }> {\n if (contribution.length === 0) return base;\n const byValue = new Map(base.map((o) => [o.value, o]));\n let changed = false;\n for (const opt of contribution) {\n const existing = byValue.get(opt.value);\n if (!existing) {\n byValue.set(opt.value, opt);\n changed = true;\n } else if (existing.label !== opt.label) {\n // Last-wins on label — keep value stable, refresh label.\n byValue.set(opt.value, opt);\n changed = true;\n }\n }\n if (!changed) return base;\n return Array.from(byValue.values());\n}\n\n/**\n * Set of names already declared on a collection. Walks\n * containers (`row`, `collapsible`) like the requirement\n * checker does so a field declared inside a row counts as\n * present. `array` / `group` are NOT walked — theme requirements\n * address top-level fields by name and shouldn't merge into\n * nested record scopes.\n */\nfunction collectExistingFieldNames(fields: NpFieldConfig[]): Set<string> {\n const names = new Set<string>();\n for (const f of fields) {\n if (f.type === \"row\" || f.type === \"collapsible\") {\n for (const name of collectExistingFieldNames(f.fields)) {\n names.add(name);\n }\n continue;\n }\n if (\"name\" in f && typeof f.name === \"string\") {\n names.add(f.name);\n }\n }\n return names;\n}\n\nfunction titleCase(s: string): string {\n return s\n .replace(/[-_]+/g, \" \")\n .replace(/\\b\\w/g, (ch) => ch.toUpperCase());\n}\n\nfunction synthesiseCollection(\n slug: string,\n requirement: NpThemeCollectionRequirement,\n injectedNames: Set<string>,\n themeId: string,\n): NpCollectionConfig | null {\n const fields: NpFieldConfig[] = [];\n for (const [fieldName, fieldReq] of Object.entries(requirement.fields ?? {})) {\n if (!SUPPORTED_REQUIREMENT_TYPES.has(fieldReq.type)) {\n // requirementToField will log the skip — call through so\n // the warning fires from a single place.\n const synth = requirementToField(fieldName, fieldReq, themeId);\n if (synth) fields.push(synth);\n continue;\n }\n const synth = requirementToField(fieldName, fieldReq, themeId);\n if (synth) {\n fields.push(synth);\n injectedNames.add(fieldName);\n }\n }\n if (fields.length === 0) {\n log().warn(\n \"Theme requested createIfAbsent for a collection with no synthesisable fields; skipping.\",\n { slug },\n );\n return null;\n }\n const titled = titleCase(slug);\n const singular = slug.endsWith(\"s\") ? titleCase(slug.slice(0, -1)) : titled;\n // `_themeOrigin` is the admin's signal that this collection\n // only exists because the named theme's `createIfAbsent`\n // synthesised it. The admin sidebar hides such entries when\n // the owning theme isn't the active one — the bundled-themes\n // prebake materialises every built-in's createIfAbsent slug,\n // but only the active theme's deserve sidebar real estate.\n return {\n slug,\n labels: { singular, plural: titled },\n fields,\n admin: { _themeOrigin: themeId },\n };\n}\n\ninterface MergeStats {\n /** Names injected per slug for telemetry / debugging. */\n injected: Map<string, Set<string>>;\n createdSlugs: Set<string>;\n}\n\n/**\n * Walks every theme on `themes` and unions its\n * `manifest.requires.collections` into `collections`. Returns a\n * NEW array (and new nested objects where modified) so the\n * function is safe to feed a frozen / re-used config object.\n *\n * Conflict resolution:\n * - Operator-declared field with the same name → keep the\n * operator's; do NOT overwrite.\n * - Two themes contribute the same field name on the same\n * slug → first theme wins, subsequent themes log a\n * `config:theme-merge` warning and skip.\n * - Theme declares a slug that doesn't exist and DOESN'T set\n * `createIfAbsent` → no-op (admin still surfaces the\n * mismatch via `checkThemeRequirements` so the operator can\n * decide whether to add the collection manually).\n */\nexport function mergeThemeRequirements(\n collections: NpCollectionConfig[],\n themes: NpRegisteredTheme[] | undefined,\n): NpCollectionConfig[] {\n if (!themes || themes.length === 0) return collections;\n\n const stats: MergeStats = {\n injected: new Map(),\n createdSlugs: new Set(),\n };\n // Work on a shallow-copied array; we clone individual\n // collections only when we need to mutate their `fields`.\n const merged: NpCollectionConfig[] = collections.slice();\n const indexBySlug = new Map<string, number>(\n merged.map((c, i) => [c.slug, i]),\n );\n // Track which fields already exist (operator-declared OR\n // earlier-theme-injected) per slug. Initialise with the\n // operator's view so we never overwrite a user-authored field.\n const existingFieldsBySlug = new Map<string, Set<string>>(\n merged.map((c) => [c.slug, collectExistingFieldNames(c.fields)]),\n );\n\n for (const theme of themes) {\n const requires = theme.manifest.requires?.collections;\n if (!requires) continue;\n for (const [slug, req] of Object.entries(requires)) {\n const existingIndex = indexBySlug.get(slug);\n if (existingIndex === undefined) {\n if (!req.createIfAbsent) continue;\n const injectedNames = new Set<string>();\n const synth = synthesiseCollection(slug, req, injectedNames, theme.manifest.id);\n if (!synth) continue;\n merged.push(synth);\n indexBySlug.set(slug, merged.length - 1);\n existingFieldsBySlug.set(slug, injectedNames);\n stats.createdSlugs.add(slug);\n stats.injected.set(slug, injectedNames);\n continue;\n }\n\n // Existing collection — append the theme's fields that\n // aren't already present (or, for select fields, union the\n // options into the existing select), and stamp the kinds\n // metadata. A theme that contributes only `kinds` (no\n // `fields`) still gets the merge — the early skip on\n // missing fields would have dropped it pre-#750.\n const reqFields = req.fields;\n const reqKinds = req.kinds;\n if (!reqFields && !reqKinds) continue;\n\n const alreadyDeclared = existingFieldsBySlug.get(slug) ?? new Set<string>();\n const target = merged[existingIndex];\n if (!target) continue; // defensive; the index was just looked up\n let nextFields: NpFieldConfig[] = target.fields;\n let fieldsCloned = false;\n const ensureCloned = (): NpFieldConfig[] => {\n if (!fieldsCloned) {\n nextFields = [...nextFields];\n fieldsCloned = true;\n }\n return nextFields;\n };\n\n for (const [fieldName, fieldReq] of Object.entries(reqFields ?? {})) {\n if (alreadyDeclared.has(fieldName)) {\n // Select-options union (universal-content-model Phase U.1).\n // When both sides are `select`, the requirement's\n // `options` add to the existing field's options instead\n // of being skipped. Two themes contributing disjoint\n // option sets (e.g. `kind=doc` + `kind=project`) is\n // exactly the case this enables.\n if (fieldReq.type === \"select\" && fieldReq.options && fieldReq.options.length > 0) {\n const idx = nextFields.findIndex(\n (f) => \"name\" in f && f.name === fieldName,\n );\n const existing = idx >= 0 ? nextFields[idx] : undefined;\n if (existing && existing.type === \"select\") {\n const merged = mergeSelectOptions(existing.options, fieldReq.options);\n if (merged !== existing.options) {\n const list = ensureCloned();\n // Spread copy ensures the result is a fresh mutable\n // array even when `mergeSelectOptions` returned the\n // input untouched in a code path the caller doesn't\n // see; cheap, removes the readonly union in the type.\n list[idx] = { ...existing, options: [...merged] };\n }\n continue;\n }\n // Falls through to the same-name warning path when the\n // existing field isn't a select — a select requirement\n // can't union into a text / number / etc.\n }\n\n // If this name was injected by an earlier theme on\n // this same merge pass, surface that explicitly —\n // operators reading the log can tell \"operator wins\"\n // (silent) apart from \"theme A wins over theme B\"\n // (warned). Operator-declared field collisions are\n // expected and unsurprising; theme-vs-theme collisions\n // are usually a misconfiguration.\n const injectedHere = stats.injected.get(slug);\n if (injectedHere?.has(fieldName)) {\n log().warn(\n \"Two themes contribute the same field on the same collection; keeping the first.\",\n { slug, field: fieldName, theme: theme.manifest.id },\n );\n }\n continue;\n }\n const synth = requirementToField(fieldName, fieldReq, theme.manifest.id);\n if (!synth) continue;\n ensureCloned().push(synth);\n alreadyDeclared.add(fieldName);\n let injectedHere = stats.injected.get(slug);\n if (!injectedHere) {\n injectedHere = new Set<string>();\n stats.injected.set(slug, injectedHere);\n }\n injectedHere.add(fieldName);\n }\n\n // Kinds metadata (universal-content-model #748). Themes\n // contribute one entry per kind they author; the merge\n // unions across themes and stamps the result onto\n // `target.admin.kinds`. Last-write-wins on per-kind props\n // (label, icon, urlPattern, …) — two themes claiming the\n // same kind value is unusual and the second theme's\n // description wins.\n //\n // Each merged kind carries a `_themeOrigin` tag so the\n // admin sidebar can hide kinds whose contributing theme\n // isn't active. Without this, the bundled-themes prebake\n // would surface every built-in theme's kinds on every\n // operator's sidebar — an operator on `default` would see\n // a \"Documentation\" entry contributed by `theme-docs`\n // (#754 follow-up).\n let nextAdmin = target.admin;\n let adminCloned = false;\n if (reqKinds && Object.keys(reqKinds).length > 0) {\n const existingKinds = target.admin?.kinds ?? {};\n const mergedKinds = { ...existingKinds };\n for (const [kindValue, kindMeta] of Object.entries(reqKinds)) {\n mergedKinds[kindValue] = {\n ...(mergedKinds[kindValue] ?? {}),\n ...kindMeta,\n _themeOrigin: theme.manifest.id,\n };\n }\n nextAdmin = { ...(target.admin ?? {}), kinds: mergedKinds };\n adminCloned = true;\n }\n\n // Group metadata (#8 of the editor track). Themes contribute\n // icon / description per sidebar group label. Merged\n // last-wins on per-key props. Unlike `kinds`, groupMeta has\n // no `_themeOrigin` gate — the metadata is purely\n // presentational, and showing an icon contributed by an\n // inactive theme is harmless (the group itself only renders\n // when an active field declares `admin.group: <name>`).\n const reqGroupMeta = req.groupMeta;\n if (reqGroupMeta && Object.keys(reqGroupMeta).length > 0) {\n const existingMeta = nextAdmin?.groupMeta ?? target.admin?.groupMeta ?? {};\n const mergedMeta = { ...existingMeta };\n for (const [groupName, meta] of Object.entries(reqGroupMeta)) {\n mergedMeta[groupName] = {\n ...(mergedMeta[groupName] ?? {}),\n ...meta,\n };\n }\n nextAdmin = { ...(nextAdmin ?? target.admin ?? {}), groupMeta: mergedMeta };\n adminCloned = true;\n }\n\n if (!fieldsCloned && !adminCloned) continue;\n\n // Clone the collection record + its fields / admin so we\n // don't mutate the operator's defineCollection() output.\n // Mutating the caller's array would surprise consumers that\n // re-use collection objects (tests, multi-site sandboxes).\n merged[existingIndex] = {\n ...target,\n ...(fieldsCloned ? { fields: nextFields } : {}),\n ...(adminCloned ? { admin: nextAdmin } : {}),\n };\n existingFieldsBySlug.set(slug, alreadyDeclared);\n }\n }\n\n return merged;\n}\n\n/**\n * Type re-export — `NpThemeCollectionKind` is imported as a value\n * here only so consumers of `mergeThemeRequirements` don't have to\n * pull the same type from `../config/types.js` separately.\n */\nexport type { NpThemeCollectionKind };\n","import { z } from \"zod\";\n\nconst functionSchema = z.custom<(...args: unknown[]) => unknown>(\n (value) => typeof value === \"function\",\n);\n\n/**\n * Serializable condition predicate (NpFieldConditionExpr) — the\n * Zod mirror of the type defined in `config/types.ts`. Permissive\n * (no exhaustive variant validation) because adding new operators\n * to the type union shouldn't force a schema bump. `z.unknown()`\n * on the value slots tolerates the wide payload shapes the editor\n * uses; the runtime evaluator (`evaluateFieldCondition`)\n * fail-opens on malformed expressions.\n */\nconst conditionExprSchema: z.ZodType = z.lazy(() =>\n z.union([\n z.object({ when: z.string().min(1), equals: z.unknown() }),\n z.object({ when: z.string().min(1), notEquals: z.unknown() }),\n z.object({ when: z.string().min(1), in: z.array(z.unknown()) }),\n z.object({ when: z.string().min(1), notIn: z.array(z.unknown()) }),\n z.object({ when: z.string().min(1), exists: z.boolean() }),\n z.object({ all: z.array(conditionExprSchema) }),\n z.object({ any: z.array(conditionExprSchema) }),\n ]),\n);\n\nconst fieldBaseSchema = z.object({\n name: z.string().min(1),\n label: z.string().min(1).optional(),\n required: z.boolean().optional(),\n defaultValue: z.unknown().optional(),\n hidden: z.boolean().optional(),\n admin: z\n .object({\n description: z.string().min(1).optional(),\n placeholder: z.string().optional(),\n readOnly: z.boolean().optional(),\n // Accepts either the legacy function form (server-only, stripped\n // at the RSC boundary) or the serializable expression form\n // (#764). The runtime evaluator handles both.\n condition: z.union([functionSchema, conditionExprSchema]).optional(),\n width: z.string().optional(),\n })\n .optional(),\n validate: functionSchema.optional(),\n});\n\nconst optionSchema = z.object({\n label: z.string().min(1),\n value: z.string().min(1),\n});\n\nconst fieldSchema: z.ZodType = z.lazy(() =>\n z.discriminatedUnion(\"type\", [\n fieldBaseSchema.extend({\n type: z.literal(\"text\"),\n minLength: z.number().int().nonnegative().optional(),\n maxLength: z.number().int().nonnegative().optional(),\n unique: z.boolean().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"textarea\"),\n minLength: z.number().int().nonnegative().optional(),\n maxLength: z.number().int().nonnegative().optional(),\n rows: z.number().int().positive().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"number\"),\n min: z.number().optional(),\n max: z.number().optional(),\n step: z.number().positive().optional(),\n integerOnly: z.boolean().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"richText\"),\n editor: z.object({ features: z.array(z.string()).optional() }).optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"blocks\"),\n allowedBlocks: z.array(z.string().min(1)).optional(),\n minRows: z.number().int().nonnegative().optional(),\n maxRows: z.number().int().nonnegative().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"checkbox\"),\n defaultValue: z.boolean().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"date\"),\n pickerOptions: z\n .object({\n format: z.string().optional(),\n includeTime: z.boolean().optional(),\n })\n .optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"upload\"),\n relationTo: z.string().min(1),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"relationship\"),\n relationTo: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]),\n hasMany: z.boolean().optional(),\n filterOptions: z.record(z.string(), z.unknown()).optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"select\"),\n options: z.array(optionSchema).min(1),\n hasMany: z.boolean().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"radio\"),\n options: z.array(optionSchema).min(1),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"email\"),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"json\"),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"array\"),\n fields: z.array(fieldSchema).min(1),\n minRows: z.number().int().nonnegative().optional(),\n maxRows: z.number().int().nonnegative().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"group\"),\n fields: z.array(fieldSchema).min(1),\n }),\n z.object({\n type: z.literal(\"row\"),\n fields: z.array(fieldSchema).min(1),\n }),\n z.object({\n type: z.literal(\"collapsible\"),\n label: z.string().min(1),\n fields: z.array(fieldSchema).min(1),\n }),\n ]),\n);\n\nconst imageSizeSchema = z.object({\n name: z.string().min(1),\n width: z.number().positive(),\n height: z.number().positive().optional(),\n crop: z.enum([\"center\", \"top\", \"bottom\", \"left\", \"right\"]).optional(),\n});\n\n// Discriminated union ties `adapter` to its required backend block —\n// previously both `local` and `s3` were optional regardless of the\n// adapter choice, so a config with `{ adapter: \"s3\" }` and no `s3`\n// block passed validation and only blew up at runtime when the storage\n// factory tried to read the missing block. (#64)\nconst storageSchema = z.discriminatedUnion(\"adapter\", [\n z.object({\n adapter: z.literal(\"local\"),\n local: z.object({\n directory: z.string().min(1),\n baseUrl: z.string().min(1),\n }),\n }),\n z.object({\n adapter: z.literal(\"s3\"),\n s3: z.object({\n bucket: z.string().min(1),\n region: z.string().min(1),\n endpoint: z.string().url().optional(),\n }),\n }),\n]);\n\n// Plugins are a mix of legacy NpPluginConfig (object with optional init fn)\n// and SDK-built NpResolvedPluginLike (object with manifest). Parse with\n// `z.unknown()` — deeper validation happens when loadPlugins() runs.\nconst pluginEntrySchema = z.unknown();\n\nexport const npConfigSchema = z.object({\n site: z.object({\n name: z.string().min(1),\n url: z.string().url(),\n }),\n db: z.object({\n connectionString: z.string(),\n pool: z\n .object({\n max: z.number().int().positive().optional(),\n })\n .optional(),\n }),\n storage: storageSchema.optional(),\n collections: z.array(z.lazy((): z.ZodType => collectionConfigSchema)),\n blocks: z.array(z.unknown()).optional(),\n editor: z\n .object({\n features: z.array(z.string().min(1)).optional(),\n })\n .optional(),\n images: z\n .object({\n sizes: z.array(imageSizeSchema).optional(),\n format: z.enum([\"webp\", \"avif\", \"jpeg\", \"png\"]).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .optional(),\n auth: z\n .object({\n secret: z.string().min(1),\n tokenExpiration: z.number().int().positive().optional(),\n refreshTokenExpiration: z.number().int().positive().optional(),\n maxLoginAttempts: z.number().int().positive().optional(),\n lockoutDuration: z.number().int().positive().optional(),\n })\n .optional(),\n plugins: z.array(pluginEntrySchema).optional(),\n typescript: z.unknown().optional(),\n themes: z.array(z.unknown()).optional(),\n i18n: z\n .object({\n locales: z.array(z.string().min(1).max(35)).min(1),\n defaultLocale: z.string().min(1),\n })\n .refine((val) => val.locales.includes(val.defaultLocale), {\n message: \"defaultLocale must be one of the declared locales\",\n path: [\"defaultLocale\"],\n })\n .optional(),\n});\n\nexport const collectionConfigSchema = z.object({\n slug: z.string().min(1).max(63).regex(/^[a-z][a-z0-9-]*$/),\n labels: z.object({\n singular: z.string().min(1),\n plural: z.string().min(1),\n }),\n slugField: z\n .union([\n z.boolean(),\n z.object({\n useField: z.string().min(1).optional(),\n unique: z.boolean().optional(),\n }),\n ])\n .optional(),\n i18n: z.boolean().optional(),\n fields: z.array(fieldSchema).min(1),\n access: z\n .object({\n create: functionSchema.optional(),\n read: functionSchema.optional(),\n update: functionSchema.optional(),\n delete: functionSchema.optional(),\n })\n .optional(),\n hooks: z\n .object({\n beforeCreate: z.array(functionSchema).optional(),\n afterCreate: z.array(functionSchema).optional(),\n beforeUpdate: z.array(functionSchema).optional(),\n afterUpdate: z.array(functionSchema).optional(),\n beforeDelete: z.array(functionSchema).optional(),\n afterDelete: z.array(functionSchema).optional(),\n beforeRead: z.array(functionSchema).optional(),\n afterRead: z.array(functionSchema).optional(),\n })\n .optional(),\n versions: z\n .object({\n drafts: z\n .union([\n z.boolean(),\n z.object({\n autosave: z.boolean().optional(),\n autosaveInterval: z.number().int().positive().optional(),\n }),\n ])\n .optional(),\n max: z.number().int().positive().optional(),\n })\n .optional(),\n community: z\n .object({\n comments: z.boolean().optional(),\n memberWrite: z\n .object({\n create: z.boolean().optional(),\n update: z.boolean().optional(),\n delete: z.boolean().optional(),\n defaultStatus: z.enum([\"published\", \"pending\"]).optional(),\n })\n .optional(),\n })\n .optional(),\n timestamps: z.boolean().optional(),\n admin: z\n .object({\n listColumns: z.array(z.string().min(1)).optional(),\n defaultSort: z.string().min(1).optional(),\n group: z.string().min(1).optional(),\n hidden: z.boolean().optional(),\n description: z.string().optional(),\n components: z\n .object({\n listView: z.string().optional(),\n editView: z.string().optional(),\n createView: z.string().optional(),\n })\n .optional(),\n })\n .optional(),\n upload: z\n .object({\n maxFileSize: z.number().positive().optional(),\n allowedMimeTypes: z.array(z.string().min(1)).optional(),\n imageSizes: z\n .array(\n z.object({\n name: z.string().min(1),\n width: z.number().positive(),\n height: z.number().positive().optional(),\n crop: z.enum([\"center\", \"top\", \"bottom\", \"left\", \"right\"]).optional(),\n }),\n )\n .optional(),\n })\n .optional(),\n});\n","import { type NpCollectionConfig } from \"./types.js\";\n\nexport function defineCollection(config: NpCollectionConfig): NpCollectionConfig {\n return config;\n}\n","import { and, desc, eq } from \"drizzle-orm\";\n\nimport { npSettings } from \"../db/schema/system.js\";\nimport { npNavigation, npSlugHistory } from \"../db/schema/system.js\";\nimport type { NpThemeTokens, NpThemeTokensOverlay } from \"../theme/types.js\";\nimport type { NpNavItem, NpFindOptions, NpFindResult, NpAuthUser } from \"../config/types.js\";\nimport { DEFAULT_THEME } from \"../theme/defaults.js\";\nimport { findDocuments, getCollectionConfig, getDb } from \"../collections/index.js\";\nimport { getCurrentSiteId } from \"../sites/context.js\";\nimport { NP_DEFAULT_SITE_ID } from \"../sites/registry.js\";\nimport { getActiveTheme } from \"../themes/registry.js\";\n\n/**\n * Phase 15.4 — every settings/navigation read scopes by the\n * current site id so each tenant gets its own theme tokens,\n * navigation menus, and arbitrary settings. The resolver\n * falls back to the default site when no request context is\n * set (background workers, scripts, tests with no resolver\n * wired) so existing single-tenant code keeps working\n * unchanged.\n */\nasync function resolveSiteId(): Promise<string> {\n return (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n}\n\n/**\n * Resolves the effective theme tokens for the current site by\n * layering three sources, last-writer-wins:\n *\n * 1. `DEFAULT_THEME` — framework baseline (always full).\n * 2. The active theme's `impl.tokens` — author-shipped defaults\n * that distinguish a theme from the framework baseline (e.g.\n * magazine's warm cream palette, portfolio's dark surface).\n * 3. The DB row in `np_settings.theme` — admin overrides via the\n * theme settings tab.\n *\n * Each layer is `NpThemeTokensOverlay` (sub-tree-Partial) — themes\n * only declare the keys they care about, admins only save deltas\n * they edit. The merge ensures every emitted token has a value.\n *\n * This is the canonical token resolver — `apps/web`'s preview\n * route calls it directly so the page-builder iframe matches what\n * the public render produces for the same active theme.\n */\nexport async function getTheme(): Promise<NpThemeTokens> {\n const db = getDb();\n const siteId = await resolveSiteId();\n const rows = await db\n .select()\n .from(npSettings)\n .where(and(eq(npSettings.siteId, siteId), eq(npSettings.key, \"theme\")))\n .limit(1);\n\n // `impl` is opaque to core (declared as `unknown` so React types\n // don't leak into this package). Cast narrowly to read the only\n // field we need; everything else stays opaque.\n const active = await getActiveTheme();\n const themeOverlay = (\n active?.impl as { tokens?: NpThemeTokensOverlay } | null | undefined\n )?.tokens;\n // Admin's theme settings tab historically saved a full\n // `NpThemeTokens` shape, but accept partial overlays too — same\n // sub-tree-aware merge applies, so a future admin UI that only\n // edits a handful of fields doesn't have to round-trip the entire\n // token tree on every save.\n const dbOverlay = rows[0]?.value as NpThemeTokensOverlay | undefined;\n\n if (!themeOverlay && !dbOverlay) return DEFAULT_THEME;\n return mergeThemeTokens(DEFAULT_THEME, themeOverlay, dbOverlay);\n}\n\n/**\n * Layered token merge. Each subsequent overlay wins on key\n * collision. Sub-objects merge field-by-field so a theme that only\n * sets `colors.primary` doesn't blow away the rest of the colors\n * sub-tree.\n */\nfunction mergeThemeTokens(\n base: NpThemeTokens,\n ...overlays: Array<NpThemeTokensOverlay | undefined>\n): NpThemeTokens {\n const result: NpThemeTokens = {\n colors: { ...base.colors },\n typography: { ...base.typography },\n shape: { ...base.shape },\n };\n for (const overlay of overlays) {\n if (!overlay) continue;\n if (overlay.colors) Object.assign(result.colors, overlay.colors);\n if (overlay.typography) Object.assign(result.typography, overlay.typography);\n if (overlay.shape) Object.assign(result.shape, overlay.shape);\n }\n return result;\n}\n\nexport async function getNavigation(\n location: string = \"header\",\n): Promise<NpNavItem[]> {\n const db = getDb();\n const siteId = await resolveSiteId();\n const rows = await db\n .select()\n .from(npNavigation)\n .where(\n and(eq(npNavigation.siteId, siteId), eq(npNavigation.location, location)),\n )\n .limit(1);\n\n if (rows.length === 0 || !rows[0]) {\n return [];\n }\n\n return resolveNavItemUrls(rows[0].items);\n}\n\n/**\n * Replaces `url` on dynamic nav items with values derived from\n * the underlying record:\n *\n * - `type: \"page\"` + `pageId` (+ optional `collectionSlug`,\n * defaults to `\"pages\"`) → the URL the source collection's\n * `seo.urlPath` produces from the linked doc. Lets the page-\n * edit \"In navigation\" panel (#436) work for any page-shaped\n * collection, not just `pages`.\n * - `type: \"collection\"` + `collection` → the conventional\n * collection-list URL (`/{collection-slug}`). The convention\n * is editor-side only; themes that route a collection\n * elsewhere (e.g. /blog for posts) should keep using\n * `type: \"link\"` with an explicit URL until a per-collection\n * route helper lands.\n *\n * Themes still render `<a href={item.url}>` and need the resolved\n * URL handed to them. Items whose underlying record disappeared\n * (doc unpublished, collection unregistered, no `seo.urlPath` on\n * the collection) fall through to `#` so the rendered output stays\n * stable across status flips — dropping the item would invalidate\n * the cache shape every time.\n */\nasync function resolveNavItemUrls(items: NpNavItem[]): Promise<NpNavItem[]> {\n // Group page-typed refs by source collection so we issue one\n // batch of lookups per collection. Items missing `collectionSlug`\n // default to `\"pages\"` so existing nav rows keep resolving\n // unchanged — that's the v1 wire format.\n const refsByCollection = collectPageRefs(items);\n\n // Map keyed by `${collection}\\0${docId}` so doc ids don't collide\n // across collections (different collections can technically share\n // the same uuid namespace).\n const docByKey = new Map<string, Record<string, unknown>>();\n\n await Promise.all(\n [...refsByCollection.entries()].map(async ([collection, ids]) => {\n try {\n await Promise.all(\n ids.map(async (id) => {\n const result = await findDocuments(collection, {\n where: { id, status: \"published\" },\n limit: 1,\n });\n const doc = result.docs[0];\n if (doc) docByKey.set(`${collection}\\0${id}`, doc);\n }),\n );\n } catch {\n // Collection isn't registered (was renamed, removed, or\n // never existed). Items pointing at it just fall through\n // to \"#\" — same fate as items pointing at unpublished docs.\n }\n }),\n );\n\n return items.map((item) => mapNavItem(item, docByKey));\n}\n\nfunction collectPageRefs(items: NpNavItem[]): Map<string, string[]> {\n const out = new Map<string, string[]>();\n const walk = (arr: NpNavItem[]): void => {\n for (const item of arr) {\n if (item.type === \"page\" && item.pageId) {\n const slug = item.collectionSlug ?? \"pages\";\n const ids = out.get(slug) ?? [];\n ids.push(item.pageId);\n out.set(slug, ids);\n }\n if (item.children) walk(item.children);\n }\n };\n walk(items);\n return out;\n}\n\nfunction mapNavItem(\n item: NpNavItem,\n docByKey: Map<string, Record<string, unknown>>,\n): NpNavItem {\n const children = item.children\n ? item.children.map((child) => mapNavItem(child, docByKey))\n : undefined;\n const withChildren = children ? { ...item, children } : item;\n\n if (item.type === \"page\" && item.pageId) {\n const collection = item.collectionSlug ?? \"pages\";\n const doc = docByKey.get(`${collection}\\0${item.pageId}`);\n let url = \"#\";\n if (doc) {\n try {\n const config = getCollectionConfig(collection);\n const path = config.seo?.urlPath?.(doc);\n if (path) url = path;\n } catch {\n // Collection un-registered between fetch and lookup — keep\n // the \"#\" fallback so rendering doesn't blow up.\n }\n }\n return { ...withChildren, url };\n }\n\n if (item.type === \"collection\" && item.collection) {\n const slug = item.collection.replace(/^\\/+/, \"\");\n const url = slug ? `/${slug}` : \"#\";\n return { ...withChildren, url };\n }\n\n return withChildren;\n}\n\nexport async function getPageBySlug(\n slug: string,\n options?: { draft?: boolean; locale?: string },\n): Promise<Record<string, unknown> | null> {\n const where: Record<string, unknown> = { slug: slug || \"/\" };\n\n if (!options?.draft) {\n where.status = \"published\";\n }\n\n // Locale-scoped lookup — when the caller supplies a locale, the\n // findDocuments query restricts to rows in that locale (matches\n // the `(site_id, locale, slug)` unique index on i18n collections).\n // Single-locale collections ignore the option, so callers can\n // pass it unconditionally.\n const result = await findDocuments(\"pages\", {\n where,\n locale: options?.locale,\n limit: 1,\n });\n\n return result.docs[0] ?? null;\n}\n\nexport async function getPostBySlug(\n slug: string,\n options?: { draft?: boolean },\n): Promise<Record<string, unknown> | null> {\n const where: Record<string, unknown> = { slug };\n\n if (!options?.draft) {\n where.status = \"published\";\n }\n\n const result = await findDocuments(\"posts\", {\n where,\n limit: 1,\n });\n\n return result.docs[0] ?? null;\n}\n\nexport async function findPosts(\n options: NpFindOptions,\n user?: NpAuthUser,\n): Promise<NpFindResult> {\n return findDocuments(\"posts\", options, user);\n}\n\nexport async function getAllPageSlugs(): Promise<string[]> {\n const result = await findDocuments(\"pages\", {\n limit: 10000,\n });\n\n return result.docs\n .map((doc) => doc.slug as string)\n .filter(Boolean);\n}\n\n/**\n * When a slug-having collection's row gets renamed (`/old-page` →\n * `/new-page`), the public-site catch-all should 301 the old URL\n * to the new one instead of returning 404. This helper walks the\n * `np_slug_history` chain for the given collection + slug and\n * returns the most recent target.\n *\n * Chain example: A → B → C (renamed twice). Looking up A walks\n * `A → B → C` and returns C. Capped at 5 hops to bound work and\n * defend against pathological cycles (shouldn't happen but cheap\n * to enforce). Returns null when no redirect target exists or\n * the chain ends in the input slug itself.\n */\nconst SLUG_REDIRECT_MAX_HOPS = 5;\n\nexport async function findSlugRedirect(\n collection: string,\n oldSlug: string,\n): Promise<string | null> {\n if (!oldSlug || oldSlug.length === 0) return null;\n const db = getDb();\n const siteId = await resolveSiteId();\n\n const seen = new Set<string>([oldSlug]);\n let currentOld = oldSlug;\n let resolved: string | null = null;\n for (let hop = 0; hop < SLUG_REDIRECT_MAX_HOPS; hop++) {\n // Take the most recently written row for this `(site, collection,\n // oldSlug)` triple — a slug can be reused over time (a doc renamed\n // away from \"X\" later, another doc renamed *to* \"X\", then \"X\" gets\n // renamed again). The newest record is the operator's intent.\n const [latest] = await db\n .select()\n .from(npSlugHistory)\n .where(\n and(\n eq(npSlugHistory.siteId, siteId),\n eq(npSlugHistory.collection, collection),\n eq(npSlugHistory.oldSlug, currentOld),\n ),\n )\n .orderBy(desc(npSlugHistory.createdAt))\n .limit(1);\n if (!latest) break;\n const next = latest.newSlug;\n if (next === oldSlug || seen.has(next)) {\n // Cycle (A→B→A) — surface as \"no redirect\". Defensive; the\n // pipeline only writes new history rows on actual changes,\n // so this is unreachable in normal operation.\n break;\n }\n resolved = next;\n seen.add(next);\n currentOld = next;\n }\n return resolved;\n}\n\nexport async function getSetting<T = unknown>(key: string): Promise<T | null> {\n const db = getDb();\n const siteId = await resolveSiteId();\n const rows = await db\n .select()\n .from(npSettings)\n .where(and(eq(npSettings.siteId, siteId), eq(npSettings.key, key)))\n .limit(1);\n\n if (rows.length === 0 || !rows[0]) {\n return null;\n }\n\n return rows[0].value as T;\n}\n","import type { NpThemeTokens } from \"./types.js\";\n\nexport const DEFAULT_THEME: NpThemeTokens = {\n colors: {\n primary: \"oklch(0.546 0.245 262.881)\",\n primaryForeground: \"oklch(0.985 0.001 106.423)\",\n background: \"oklch(1 0 0)\",\n foreground: \"oklch(0.145 0.004 285.823)\",\n muted: \"oklch(0.967 0.001 286.375)\",\n mutedForeground: \"oklch(0.556 0.007 286.618)\",\n border: \"oklch(0.922 0.004 286.32)\",\n card: \"oklch(1 0 0)\",\n cardForeground: \"oklch(0.145 0.004 285.823)\",\n accent: \"oklch(0.967 0.001 286.375)\",\n accentForeground: \"oklch(0.145 0.004 285.823)\",\n destructive: \"oklch(0.577 0.245 27.325)\",\n destructiveForeground: \"oklch(0.985 0.001 106.423)\",\n },\n typography: {\n fontHeading: \"system-ui, -apple-system, sans-serif\",\n fontBody: \"system-ui, -apple-system, sans-serif\",\n fontMono: \"ui-monospace, 'Cascadia Code', monospace\",\n fontSizeBase: \"1rem\",\n lineHeight: \"1.625\",\n fontSizeSm: \"0.875rem\",\n fontSizeLg: \"1.125rem\",\n fontSizeXl: \"1.25rem\",\n fontSize2xl: \"1.5rem\",\n fontSize3xl: \"1.875rem\",\n fontSize4xl: \"2.25rem\",\n },\n shape: {\n radiusSm: \"0.25rem\",\n radiusMd: \"0.375rem\",\n radiusLg: \"0.5rem\",\n radiusFull: \"9999px\",\n shadowSm: \"0 1px 2px 0 rgb(0 0 0 / 0.05)\",\n shadowMd: \"0 4px 6px -1px rgb(0 0 0 / 0.1)\",\n shadowLg: \"0 10px 15px -3px rgb(0 0 0 / 0.1)\",\n },\n};\n","import { and, eq } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npSettings } from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\nimport { addStrings } from \"../i18n/strings.js\";\nimport { getCurrentSiteId } from \"../sites/context.js\";\nimport { NP_DEFAULT_SITE_ID as DEFAULT_SITE } from \"../sites/registry.js\";\nimport type { NpRegisteredTheme } from \"../config/types.js\";\n\n/**\n * Phase 11.1 — theme registry. Sites declare an array of themes\n * in `nexpress.config.ts`; the framework registers them once at\n * boot. The active theme id lives in `np_settings.activeTheme`,\n * so admins can switch between installed themes via the admin UI\n * without a redeploy. New theme INSTALLATION still requires a\n * rebuild (Next.js bundles the components) — same constraint\n * WordPress has with file uploads on the server.\n *\n * The registry is a process-level Map; same lifetime as the\n * collection / plugin registries. Re-registration overwrites\n * by `manifest.id` so a hot-reload during dev doesn't accumulate\n * stale entries.\n */\nconst registry = new Map<string, NpRegisteredTheme>();\n\n/**\n * Idempotent — call once at boot from the framework's\n * bootstrap, again from a hot-reload, etc. Themes are matched\n * by `manifest.id`; later registrations replace earlier ones.\n */\nexport function registerThemes(themes: NpRegisteredTheme[]): void {\n for (const theme of themes) {\n if (!theme?.manifest?.id) {\n throw new Error(\"Theme is missing manifest.id\");\n }\n registry.set(theme.manifest.id, theme);\n\n // Phase 12.5 — themes can ship UI-string bundles via\n // `impl.i18n: { locale: { key: value } }`. Merging happens\n // here (alongside theme registration) rather than in the\n // bootstrap so live theme swaps in dev pick up updated\n // strings without a restart.\n const impl = theme.impl as { i18n?: Record<string, Record<string, string>> };\n if (impl?.i18n && typeof impl.i18n === \"object\") {\n for (const [locale, bundle] of Object.entries(impl.i18n)) {\n if (bundle && typeof bundle === \"object\") {\n addStrings(locale, bundle);\n }\n }\n }\n }\n}\n\nexport function getRegisteredThemes(): NpRegisteredTheme[] {\n return Array.from(registry.values());\n}\n\nexport function getThemeById(id: string): NpRegisteredTheme | undefined {\n return registry.get(id);\n}\n\n/** Tests use this between cases; production callers should never need it. */\nexport function resetThemes(): void {\n registry.clear();\n}\n\n/**\n * Reads the persisted active-theme id from `np_settings` for\n * the current site. Returns `null` when no row exists —\n * caller's job to decide the fallback (typically the first\n * registered theme).\n *\n * Phase 15.4 — scoped by current site. Single-tenant\n * deployments leave every row at `site_id = 'default'`, so\n * the lookup behaves identically to the pre-15.4 global\n * version.\n */\nexport async function getActiveThemeId(): Promise<string | null> {\n const db = getDb();\n const siteId = (await getCurrentSiteId()) ?? DEFAULT_SITE;\n const rows = (await db\n .select()\n .from(npSettings)\n .where(and(eq(npSettings.siteId, siteId), eq(npSettings.key, \"activeTheme\")))\n .limit(1)) as Array<{ value: unknown }>;\n const row = rows[0];\n if (!row) return null;\n return typeof row.value === \"string\" ? row.value : null;\n}\n\n/**\n * Resolves the active theme to render. Looks up the persisted id\n * in the registry; falls back to the first registered theme when\n * the id is unset, missing, or points at a theme that's no\n * longer in the registry (e.g. it was removed from\n * `nexpress.config.ts` between deploys). Returns `null` only\n * when the registry is completely empty.\n */\nexport async function getActiveTheme(): Promise<NpRegisteredTheme | null> {\n const id = await getActiveThemeId();\n if (id) {\n const theme = registry.get(id);\n if (theme) return theme;\n }\n // Registry preserves insertion order; the first registered\n // theme is the implicit default.\n const first = registry.values().next();\n return first.done ? null : first.value;\n}\n\n/**\n * Persist the active theme. Validates the id is registered so\n * an admin can't pick a string that doesn't resolve to anything\n * (which would silently fall back to the default and confuse\n * the operator).\n *\n * Accepts an optional outer transaction so the active-theme flip\n * can sit inside the same atomic scope as a wipe + seed batch\n * (see `wipeSeededContent` / `seedAll`'s `tx` option). Without\n * an outer tx, the write runs against the pool handle and\n * commits standalone.\n */\nexport async function setActiveThemeId(\n id: string,\n updatedBy: string | null = null,\n options: { tx?: unknown } = {},\n): Promise<void> {\n if (!registry.has(id)) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"themeId\",\n message: `Unknown theme '${id}'. Register it in nexpress.config.ts first.`,\n },\n ]);\n }\n // `options.tx` is typed `unknown` here to avoid pulling Drizzle\n // internals onto the public registry surface. Callers thread\n // the NpTransaction value they received from\n // `db.transaction(async (tx) => …)`; structurally it has the\n // same `.insert(table)` chain we rely on.\n const dbHandle = (options.tx ?? getDb()) as ReturnType<typeof getDb>;\n const now = new Date();\n const siteId = (await getCurrentSiteId()) ?? DEFAULT_SITE;\n // Phase 15.4 — composite (site_id, key) PK.\n await dbHandle\n .insert(npSettings)\n .values({ siteId, key: \"activeTheme\", value: id, updatedAt: now, updatedBy })\n .onConflictDoUpdate({\n target: [npSettings.siteId, npSettings.key],\n set: { value: id, updatedAt: now, updatedBy },\n });\n}\n\n/**\n * Phase 11.3 — surface the active theme's templates for a\n * given collection so admin pickers and the catch-all renderer\n * can introspect what's available without reaching into the\n * opaque `impl` themselves. The result is sanitized for serial-\n * ization (no React component refs leak through this path) so\n * the same shape is safe to send over the API to the admin UI.\n */\nexport interface NpThemeTemplateSummary {\n id: string;\n label: string;\n description?: string;\n}\n\nexport async function getThemeTemplateSummaries(\n collectionSlug: string,\n): Promise<NpThemeTemplateSummary[]> {\n const summaries = new Map<string, NpThemeTemplateSummary>();\n\n // Phase 14.5 — start with plugin-contributed templates so\n // theme entries naturally overwrite them on id collision.\n // Lazy import keeps the registry → plugin coupling one-way\n // (plugins know about themes' template shape; themes don't\n // depend on plugins at type-import time).\n const { getPluginTemplatesForCollection } = await import(\n \"../plugins/templates.js\"\n );\n for (const [id, value] of getPluginTemplatesForCollection(collectionSlug)) {\n const def = value as { label?: unknown; description?: unknown };\n summaries.set(id, {\n id,\n label: typeof def.label === \"string\" ? def.label : id,\n description:\n typeof def.description === \"string\" ? def.description : undefined,\n });\n }\n\n const active = await getActiveTheme();\n if (active) {\n const impl = active.impl as {\n templates?: Record<\n string,\n Record<string, { label?: string; description?: string }>\n >;\n };\n const set = impl.templates?.[collectionSlug];\n if (set) {\n for (const [id, def] of Object.entries(set)) {\n summaries.set(id, {\n id,\n label: typeof def.label === \"string\" ? def.label : id,\n description:\n typeof def.description === \"string\" ? def.description : undefined,\n });\n }\n }\n }\n\n return [...summaries.values()];\n}\n\n/**\n * Phase 14.5 — resolve a template's render component for the\n * given collection + template id. Looks up theme first\n * (theme always wins), falls back to plugin-contributed\n * templates. Returns the opaque value (the catch-all casts to\n * `{ component }` at the render boundary).\n */\nexport async function resolveTemplateComponent(\n collectionSlug: string,\n templateId: string,\n): Promise<unknown> {\n const active = await getActiveTheme();\n if (active) {\n const impl = active.impl as {\n templates?: Record<string, Record<string, unknown>>;\n };\n const themeEntry = impl.templates?.[collectionSlug]?.[templateId];\n if (themeEntry) return themeEntry;\n }\n\n const { getPluginTemplatesForCollection } = await import(\n \"../plugins/templates.js\"\n );\n const pluginEntry =\n getPluginTemplatesForCollection(collectionSlug).get(templateId);\n return pluginEntry ?? null;\n}\n","import { createReadStream, createWriteStream } from \"node:fs\";\nimport { access, mkdir, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { ReadableStream } from \"node:stream/web\";\n\nimport type { NpFileMetadata, NpStorageAdapter } from \"./types.js\";\n\nexport interface LocalStorageAdapterConfig {\n directory: string;\n baseUrl: string;\n}\n\nexport class LocalStorageAdapter implements NpStorageAdapter {\n constructor(private readonly config: LocalStorageAdapterConfig) {}\n\n async upload(\n key: string,\n data: Buffer | ReadableStream,\n _: NpFileMetadata,\n ): Promise<void> {\n const filePath = this.resolvePath(key);\n\n await mkdir(dirname(filePath), { recursive: true });\n\n if (Buffer.isBuffer(data)) {\n await writeFile(filePath, data);\n return;\n }\n\n await pipeline(Readable.fromWeb(data), createWriteStream(filePath));\n }\n\n getStream(key: string): Promise<ReadableStream> {\n return Promise.resolve(\n Readable.toWeb(createReadStream(this.resolvePath(key))),\n );\n }\n\n getUrl(key: string): Promise<string> {\n // `baseUrl` is commonly a relative path (the default is\n // `\"/uploads\"`) — `new URL(key, \"/uploads/\")` throws because\n // the URL constructor requires an absolute base. Concatenate\n // for relative baseUrls and let the URL constructor handle\n // absolute ones (full origin, S3-style, etc.).\n const base = this.normalizeBaseUrl(this.config.baseUrl);\n if (base.startsWith(\"/\")) {\n return Promise.resolve(`${base}/${key}`);\n }\n return Promise.resolve(new URL(key, `${base}/`).toString());\n }\n\n async delete(key: string): Promise<void> {\n await unlink(this.resolvePath(key));\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await access(this.resolvePath(key));\n return true;\n } catch {\n return false;\n }\n }\n\n private resolvePath(key: string): string {\n return join(this.config.directory, key);\n }\n\n private normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.endsWith(\"/\") ? baseUrl.slice(0, -1) : baseUrl;\n }\n}\n","import { Readable } from \"node:stream\";\nimport type { ReadableStream } from \"node:stream/web\";\n\nimport type { S3Client } from \"@aws-sdk/client-s3\";\n// `import type *` keeps this a compile-time alias only —\n// the actual `import(\"@aws-sdk/client-s3\")` happens lazily\n// in `s3ModulePromise` below, so apps that don't use S3\n// don't pay the import cost.\nimport type * as awsS3 from \"@aws-sdk/client-s3\";\n\nimport type { NpFileMetadata, NpStorageAdapter } from \"./types.js\";\n\nexport interface S3StorageAdapterConfig {\n bucket: string;\n region: string;\n endpoint?: string;\n credentials?: {\n accessKeyId: string;\n secretAccessKey: string;\n };\n}\n\ntype S3Module = typeof awsS3;\n\nlet s3ModulePromise: Promise<S3Module> | null = null;\n\nexport class S3StorageAdapter implements NpStorageAdapter {\n private clientPromise: Promise<S3Client> | null = null;\n\n constructor(private readonly config: S3StorageAdapterConfig) {}\n\n async upload(\n key: string,\n data: Buffer | ReadableStream,\n metadata: NpFileMetadata,\n ): Promise<void> {\n const [{ PutObjectCommand }, client] = await Promise.all([\n loadS3Module(),\n this.getClient(),\n ]);\n\n await client.send(\n new PutObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n Body: Buffer.isBuffer(data) ? data : Readable.fromWeb(data),\n ContentType: metadata.contentType,\n ContentLength: metadata.contentLength,\n Metadata: {\n originalFilename: metadata.originalFilename,\n },\n }),\n );\n }\n\n async getStream(key: string): Promise<ReadableStream> {\n const [{ GetObjectCommand }, client] = await Promise.all([\n loadS3Module(),\n this.getClient(),\n ]);\n const response = await client.send(\n new GetObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n }),\n );\n\n return toReadableStream(response.Body);\n }\n\n getUrl(key: string): Promise<string> {\n if (this.config.endpoint) {\n return Promise.resolve(\n new URL(\n key,\n `${normalizeUrl(this.config.endpoint)}/${this.config.bucket}/`,\n ).toString(),\n );\n }\n\n return Promise.resolve(\n new URL(\n key,\n `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/`,\n ).toString(),\n );\n }\n\n async delete(key: string): Promise<void> {\n const [{ DeleteObjectCommand }, client] = await Promise.all([\n loadS3Module(),\n this.getClient(),\n ]);\n\n await client.send(\n new DeleteObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n }),\n );\n }\n\n async exists(key: string): Promise<boolean> {\n const [{ HeadObjectCommand }, client] = await Promise.all([\n loadS3Module(),\n this.getClient(),\n ]);\n\n try {\n await client.send(\n new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n }),\n );\n return true;\n } catch (error) {\n if (isNotFoundError(error)) {\n return false;\n }\n\n throw error;\n }\n }\n\n private getClient(): Promise<S3Client> {\n if (!this.clientPromise) {\n this.clientPromise = this.createClient();\n }\n\n return this.clientPromise;\n }\n\n private async createClient(): Promise<S3Client> {\n const { S3Client } = await loadS3Module();\n\n return new S3Client({\n region: this.config.region,\n endpoint: this.config.endpoint,\n credentials: this.config.credentials,\n forcePathStyle: Boolean(this.config.endpoint),\n });\n }\n}\n\nasync function loadS3Module(): Promise<S3Module> {\n s3ModulePromise ??= import(\"@aws-sdk/client-s3\");\n return s3ModulePromise;\n}\n\nfunction toReadableStream(body: unknown): ReadableStream {\n if (hasTransformToWebStream(body)) {\n return body.transformToWebStream();\n }\n\n if (body instanceof Readable) {\n return Readable.toWeb(body);\n }\n\n throw new Error(\"S3 object body is not a readable stream.\");\n}\n\nfunction hasTransformToWebStream(\n value: unknown,\n): value is { transformToWebStream(): ReadableStream } {\n return typeof value === \"object\"\n && value !== null\n && \"transformToWebStream\" in value\n && typeof value.transformToWebStream === \"function\";\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n return typeof error === \"object\"\n && error !== null\n && ((\"name\" in error && error.name === \"NotFound\")\n || (\"$metadata\" in error\n && typeof error.$metadata === \"object\"\n && error.$metadata !== null\n && \"httpStatusCode\" in error.$metadata\n && error.$metadata.httpStatusCode === 404));\n}\n\nfunction normalizeUrl(value: string): string {\n return value.endsWith(\"/\") ? value.slice(0, -1) : value;\n}\n","import type { NpConfig } from \"../config/types.js\";\nimport { LocalStorageAdapter } from \"./local.js\";\nimport { S3StorageAdapter } from \"./s3.js\";\nimport type { NpStorageAdapter } from \"./types.js\";\n\nexport type { NpFileMetadata, NpStorageAdapter } from \"./types.js\";\nexport { LocalStorageAdapter } from \"./local.js\";\nexport { S3StorageAdapter } from \"./s3.js\";\n\ntype NpStorageConfig = NonNullable<NpConfig[\"storage\"]>;\n\nexport function createStorageAdapter(config: NpConfig[\"storage\"]): NpStorageAdapter {\n if (!config) {\n throw new Error(\"Storage configuration is required.\");\n }\n\n if (config.adapter === \"local\") {\n if (!config.local) {\n throw new Error(\"Local storage configuration is required.\");\n }\n\n return new LocalStorageAdapter(config.local);\n }\n\n if (!config.s3) {\n throw new Error(\"S3 storage configuration is required.\");\n }\n\n const s3Config = config.s3 as NpStorageConfig[\"s3\"] & {\n credentials?: {\n accessKeyId: string;\n secretAccessKey: string;\n };\n };\n\n return new S3StorageAdapter(s3Config);\n}\n","import { NpError } from \"../errors.js\";\nimport type { NpEmailAdapter, NpEmailMessage } from \"./types.js\";\n\nexport interface SmtpEmailAdapterOptions {\n host: string;\n port: number;\n user?: string;\n pass?: string;\n /** Default `From` header when a message doesn't override. */\n from: string;\n /**\n * Use implicit TLS (port 465) when `true`. When `false`, STARTTLS is\n * negotiated (port 587 / 25). Defaults to `port === 465`.\n */\n secure?: boolean;\n}\n\n// Narrow structural type for the nodemailer transporter we use. Declared\n// locally so core doesn't import @types/nodemailer just for one method.\ninterface NodemailerTransporterLike {\n sendMail(opts: {\n from: string;\n to: string;\n subject: string;\n text: string;\n html?: string;\n }): Promise<unknown>;\n verify?: () => Promise<unknown>;\n}\n\n/**\n * Nodemailer-backed SMTP adapter. Works with any SMTP-speaking provider\n * (Resend, SES, Mailgun, Postmark, Gmail, Zoho, custom relays).\n *\n * `nodemailer` is loaded dynamically on first send so that apps that don't\n * use the SMTP adapter (noop or custom adapter) never pay its import cost.\n */\nexport class SmtpEmailAdapter implements NpEmailAdapter {\n readonly kind = \"smtp\";\n private readonly options: SmtpEmailAdapterOptions;\n private transporter: NodemailerTransporterLike | null = null;\n\n constructor(options: SmtpEmailAdapterOptions) {\n this.options = options;\n }\n\n private async ensureTransporter(): Promise<NodemailerTransporterLike> {\n if (this.transporter) return this.transporter;\n\n let nodemailer: {\n createTransport: (cfg: unknown) => NodemailerTransporterLike;\n };\n try {\n // Indirect specifier so TypeScript doesn't try to\n // resolve `nodemailer` at compile time —\n // `@nexpress/core` doesn't depend on it. Apps using the\n // noop or a custom adapter never pay the import cost.\n const moduleId: string = \"nodemailer\";\n nodemailer = (await import(moduleId)) as typeof nodemailer;\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n throw new NpError(\n `Could not load \\`nodemailer\\` — add it to the app's dependencies to use the SMTP adapter. Cause: ${cause}`,\n \"EMAIL_ADAPTER_MISSING_DEPENDENCY\",\n 500,\n );\n }\n\n const { host, port, user, pass } = this.options;\n const secure = this.options.secure ?? port === 465;\n\n this.transporter = nodemailer.createTransport({\n host,\n port,\n secure,\n auth: user && pass ? { user, pass } : undefined,\n });\n return this.transporter;\n }\n\n async send(message: NpEmailMessage): Promise<void> {\n const transporter = await this.ensureTransporter();\n try {\n await transporter.sendMail({\n from: message.from ?? this.options.from,\n to: message.to,\n subject: message.subject,\n text: message.text,\n html: message.html,\n });\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n throw new NpError(\n `Failed to deliver email via SMTP: ${cause}`,\n \"EMAIL_DELIVERY_FAILED\",\n 502,\n );\n }\n }\n}\n","export function sanitizeTokenValue(value: string): string {\n return value\n .replace(/[;{}]/g, \"\")\n .replace(/url\\s*\\(/gi, \"\")\n .replace(/expression\\s*\\(/gi, \"\")\n .replace(/@import/gi, \"\")\n .slice(0, 200);\n}\n","import type {\n NpCollectionConfig,\n NpFieldConfig,\n NpRelationshipField,\n NpThemeFieldRequirement,\n NpThemeManifest,\n} from \"../config/types.js\";\n\n/**\n * Phase F.1 — theme requirement check.\n *\n * Compares a theme's `manifest.requires` against the site's\n * registered collections and reports mismatches. The result\n * drives admin warnings on theme activation; F.8 will reuse\n * the same shape to drive the CLI patcher.\n *\n * \"Hard\" requirements (default) are normal mismatches the\n * operator should resolve. \"Soft\" requirements (`hard: false`)\n * are recorded separately so admin can show them at lower\n * severity — the theme renders without them but with degraded\n * behavior.\n */\n\nexport interface NpThemeRequirementMissingField {\n collection: string;\n field: string;\n expected: NpThemeFieldRequirement;\n hard: boolean;\n}\n\nexport interface NpThemeRequirementTypeConflict {\n collection: string;\n field: string;\n expected: NpThemeFieldRequirement[\"type\"];\n actual: string;\n hard: boolean;\n}\n\nexport interface NpThemeRequirementRelationConflict {\n collection: string;\n field: string;\n expected: NonNullable<NpThemeFieldRequirement[\"relationTo\"]>;\n actual: string | string[];\n hard: boolean;\n}\n\nexport interface NpThemeRequirementResult {\n themeId: string;\n hasMismatches: boolean;\n /** Has at least one HARD mismatch — operator should resolve before activation. */\n hasHardMismatches: boolean;\n missingCollections: Array<{\n collection: string;\n createIfAbsent: boolean;\n }>;\n missingFields: NpThemeRequirementMissingField[];\n typeConflicts: NpThemeRequirementTypeConflict[];\n relationConflicts: NpThemeRequirementRelationConflict[];\n}\n\n/**\n * Walk a collection config's field tree and produce a flat\n * `name → field` map. Theme requirements address top-level\n * fields by name; the walker handles `row` / `collapsible`\n * containers (which inline their children) but not `array` /\n * `group` (which scope their children inside a sub-record —\n * theme requirements don't reach into those, by design).\n */\nfunction flattenTopLevelFields(\n fields: NpFieldConfig[],\n): Map<string, NpFieldConfig> {\n const out = new Map<string, NpFieldConfig>();\n for (const f of fields) {\n if (f.type === \"row\" || f.type === \"collapsible\") {\n for (const [name, child] of flattenTopLevelFields(f.fields)) {\n out.set(name, child);\n }\n continue;\n }\n if (\"name\" in f && typeof f.name === \"string\") {\n out.set(f.name, f);\n }\n }\n return out;\n}\n\nfunction relationToMatches(\n expected: NonNullable<NpThemeFieldRequirement[\"relationTo\"]>,\n actual: NpRelationshipField[\"relationTo\"],\n): boolean {\n const expectedList = Array.isArray(expected) ? expected : [expected];\n const actualList = Array.isArray(actual) ? actual : [actual];\n // Theme expects every declared target to exist on the actual\n // field. Actual may include extras (theme isn't picky about\n // those) but must cover the expected set.\n return expectedList.every((e) => actualList.includes(e));\n}\n\nexport function checkThemeRequirements(\n manifest: NpThemeManifest,\n collections: NpCollectionConfig[],\n): NpThemeRequirementResult {\n const result: NpThemeRequirementResult = {\n themeId: manifest.id,\n hasMismatches: false,\n hasHardMismatches: false,\n missingCollections: [],\n missingFields: [],\n typeConflicts: [],\n relationConflicts: [],\n };\n\n const requires = manifest.requires?.collections;\n if (!requires) return result;\n\n const bySlug = new Map(collections.map((c) => [c.slug, c]));\n\n for (const [slug, req] of Object.entries(requires)) {\n const collection = bySlug.get(slug);\n if (!collection) {\n result.missingCollections.push({\n collection: slug,\n createIfAbsent: req.createIfAbsent ?? false,\n });\n continue;\n }\n if (!req.fields) continue;\n\n const fieldMap = flattenTopLevelFields(collection.fields);\n for (const [fieldName, fieldReq] of Object.entries(req.fields)) {\n const hard = fieldReq.hard ?? true;\n const actual = fieldMap.get(fieldName);\n if (!actual) {\n result.missingFields.push({\n collection: slug,\n field: fieldName,\n expected: fieldReq,\n hard,\n });\n continue;\n }\n if (actual.type !== fieldReq.type) {\n result.typeConflicts.push({\n collection: slug,\n field: fieldName,\n expected: fieldReq.type,\n actual: actual.type,\n hard,\n });\n continue;\n }\n if (\n fieldReq.type === \"relationship\" &&\n fieldReq.relationTo &&\n actual.type === \"relationship\"\n ) {\n if (\n !relationToMatches(\n fieldReq.relationTo,\n actual.relationTo,\n )\n ) {\n result.relationConflicts.push({\n collection: slug,\n field: fieldName,\n expected: fieldReq.relationTo,\n actual: actual.relationTo,\n hard,\n });\n }\n }\n }\n }\n\n const hardMismatches =\n result.missingCollections.length > 0 ||\n result.missingFields.some((m) => m.hard) ||\n result.typeConflicts.some((c) => c.hard) ||\n result.relationConflicts.some((c) => c.hard);\n const anyMismatches =\n result.missingCollections.length > 0 ||\n result.missingFields.length > 0 ||\n result.typeConflicts.length > 0 ||\n result.relationConflicts.length > 0;\n\n result.hasHardMismatches = hardMismatches;\n result.hasMismatches = anyMismatches;\n return result;\n}\n","import { and, eq } from \"drizzle-orm\";\nimport type { ZodTypeAny } from \"zod\";\n\nimport type { NpThemeManifest } from \"../config/types.js\";\nimport { getDb } from \"../db/index.js\";\nimport { npSettings } from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\nimport { getCurrentSiteId } from \"../sites/context.js\";\nimport { getActiveTheme, getThemeById } from \"./registry.js\";\nimport {\n introspectThemeSettingsSchema,\n type NpThemeSettingsField,\n} from \"./settings-schema.js\";\n\nconst DEFAULT_SITE = \"default\";\n\n/**\n * Phase F.3 — per-theme operator settings.\n *\n * Stored at `np_settings.(site_id, key=\"theme.settings:<themeId>\")`\n * with the value being the parsed `z.infer<typeof\n * settingsSchema>`. Reuses the existing `nx:theme:<siteId>`\n * cache tag (see design doc §5.3) — settings live on the same\n * read paths as tokens / active id, so a shared bust avoids\n * fragmenting the tag namespace.\n */\n\nfunction settingsKey(themeId: string): string {\n return `theme.settings:${themeId}`;\n}\n\n/**\n * v0.3 (D) — versioned envelope for persisted theme settings.\n *\n * Sentinel keys (`__npVersion`, `__npSettings`) avoid collision\n * with theme-owned setting fields (themes rarely choose names\n * starting with `__np`; a `version` / `value` heuristic was\n * considered but rejected because both names are plausible\n * theme-author choices for actual settings).\n *\n * Legacy unwrapped values (written by v0.2 before this PR) are\n * detected by the absence of the sentinel keys and migrated\n * in-place to v1 → current.\n */\n/** Internal — exported for unit tests only. */\nexport interface NpVersionedSettings {\n __npVersion: number;\n __npSettings: unknown;\n}\n\n/** Internal — exported for unit tests only. */\nexport function isVersionedSettings(value: unknown): value is NpVersionedSettings {\n if (!value || typeof value !== \"object\") return false;\n const candidate = value as Partial<NpVersionedSettings>;\n // Number.isFinite rejects NaN / Infinity / non-numbers — a\n // hand-crafted or corrupted DB value with `__npVersion: NaN`\n // would otherwise pass typeof and trip the migration path's\n // `>=` comparisons (NaN >= N always false).\n return (\n typeof candidate.__npVersion === \"number\" &&\n Number.isFinite(candidate.__npVersion) &&\n \"__npSettings\" in candidate\n );\n}\n\n/** Run the theme's `settingsMigrate` from `from` to current\n * schema version. No-op when versions match (or when the theme\n * doesn't declare a migrator). Defensive try/catch — a buggy\n * migrate fn shouldn't blow up the read path; we fall back to\n * the original value and let `safeParse` decide. */\n/** Internal — exported for unit tests only. */\nexport function applyMigration(\n manifest: NpThemeManifest,\n rawValue: unknown,\n fromVersion: number,\n): unknown {\n const target = manifest.settingsVersion ?? 1;\n if (fromVersion >= target) return rawValue;\n const migrate = manifest.settingsMigrate;\n if (typeof migrate !== \"function\") return rawValue;\n try {\n return migrate(rawValue, fromVersion);\n } catch {\n return rawValue;\n }\n}\n\nfunction defaultsFrom(fields: NpThemeSettingsField[]): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const f of fields) {\n if (f.default !== undefined) {\n out[f.name] = f.default;\n continue;\n }\n if (f.type === \"object\") {\n out[f.name] = defaultsFrom(f.fields);\n }\n if (f.type === \"array\") {\n out[f.name] = [];\n }\n }\n return out;\n}\n\n/**\n * Read the persisted settings row for a theme and parse it via\n * the theme's schema. Returns the parsed value when valid;\n * falls back to schema defaults on parse failure (with the\n * failure recorded for the admin to surface, see\n * `getThemeSettingsWithStatus`).\n *\n * `themeId` defaults to the active theme. Pass an explicit id\n * to read another installed theme's settings (used by the\n * admin settings page).\n *\n * Return type is `unknown` because core can't type-narrow to\n * a specific theme's `z.infer<typeof schema>` — the schema\n * lives in the theme package, not in core. Theme components\n * should cast at the call site, ideally against an exported\n * type alias from the theme package itself:\n *\n * // packages/themes/magazine/src/index.ts\n * export const settingsSchema = z.object({ ... });\n * export type MagazineSettings = z.infer<typeof settingsSchema>;\n *\n * // a theme component\n * const settings = (await getThemeSettings()) as MagazineSettings;\n */\nexport async function getThemeSettings(\n themeId?: string,\n): Promise<unknown> {\n const result = await getThemeSettingsWithStatus(themeId);\n return result.value;\n}\n\nexport interface NpThemeSettingsResult {\n themeId: string | null;\n /** Parsed settings or schema defaults (never null when a theme\n * has a schema; empty object when the theme has no schema). */\n value: unknown;\n /** True when there's a stored row, regardless of whether it\n * passed validation. */\n hasPersisted: boolean;\n /** Set when the persisted value failed `schema.parse()`. The\n * admin surface uses this to show a \"values reset to\n * defaults\" banner. */\n parseError?: string;\n}\n\nexport async function getThemeSettingsWithStatus(\n themeId?: string,\n): Promise<NpThemeSettingsResult> {\n const theme = themeId ? getThemeById(themeId) : await getActiveTheme();\n if (!theme) {\n return { themeId: null, value: {}, hasPersisted: false };\n }\n const schema = theme.manifest.settingsSchema as ZodTypeAny | undefined;\n if (!schema) {\n return { themeId: theme.manifest.id, value: {}, hasPersisted: false };\n }\n\n const db = getDb();\n const siteId = (await getCurrentSiteId()) ?? DEFAULT_SITE;\n const rows = (await db\n .select()\n .from(npSettings)\n .where(\n and(\n eq(npSettings.siteId, siteId),\n eq(npSettings.key, settingsKey(theme.manifest.id)),\n ),\n )\n .limit(1)) as Array<{ value: unknown }>;\n\n const fields = introspectThemeSettingsSchema(schema);\n const defaults = defaultsFrom(fields);\n\n const row = rows[0];\n if (!row) {\n // No row stored yet — first access returns schema defaults.\n // We don't write a row eagerly; the operator's first save\n // creates one.\n const parsed = schema.safeParse(defaults);\n return {\n themeId: theme.manifest.id,\n value: parsed.success ? parsed.data : defaults,\n hasPersisted: false,\n };\n }\n\n // v0.3 (D) — version detection + migration.\n //\n // Storage shape options:\n // - `{ __npVersion: N, __npSettings: ... }` (v0.3+ wrapped)\n // - bare value (legacy v0.2 unwrapped — treated as v1)\n //\n // When stored version < manifest.settingsVersion, run the\n // migrator. Re-parse the result; on parse failure (buggy\n // migrate, or schema drift the migrator doesn't cover), fall\n // back to defaults with parseError so the admin surfaces it.\n const versioned = isVersionedSettings(row.value) ? row.value : null;\n const storedVersion = versioned ? versioned.__npVersion : 1;\n const rawValue = versioned ? versioned.__npSettings : row.value;\n const valueToParse = applyMigration(theme.manifest, rawValue, storedVersion);\n\n const parsed = schema.safeParse(valueToParse);\n if (parsed.success) {\n return {\n themeId: theme.manifest.id,\n value: parsed.data,\n hasPersisted: true,\n };\n }\n\n // Schema mismatch (theme upgrade changed the shape and either\n // declared no migrator, or the migrator didn't cover this path).\n // Fall back to defaults and surface the error; the admin\n // renders a \"settings were reset\" banner.\n return {\n themeId: theme.manifest.id,\n value: defaults,\n hasPersisted: true,\n parseError: parsed.error.message,\n };\n}\n\n/**\n * Validate and persist a theme's settings. Throws\n * `NpValidationError` when `value` doesn't pass the schema —\n * the admin form must surface field-level errors before\n * calling this.\n *\n * **Cache invalidation is the caller's responsibility.** This\n * function writes to `np_settings` only; it doesn't import\n * `next/cache`. The admin API route (`PUT\n * /api/admin/themes/[id]/settings`) busts `nx:theme:<siteId>`\n * (and `nx:sitemap:*` / `nx:feed:*` when `impl.seo` is\n * declared) after a successful write. Other callers — jobs,\n * scripts, server actions — must do the same to avoid stale\n * cached reads.\n */\nexport async function setThemeSettings(\n themeId: string,\n value: unknown,\n updatedBy: string | null = null,\n): Promise<unknown> {\n const theme = getThemeById(themeId);\n if (!theme) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"themeId\",\n message: `Unknown theme '${themeId}'. Register it in nexpress.config.ts first.`,\n },\n ]);\n }\n const schema = theme.manifest.settingsSchema as ZodTypeAny | undefined;\n if (!schema) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"themeId\",\n message: `Theme '${themeId}' does not declare a settingsSchema.`,\n },\n ]);\n }\n\n const parsed = schema.safeParse(value);\n if (!parsed.success) {\n throw new NpValidationError(\n \"Settings failed validation\",\n parsed.error.issues.map((i) => ({\n field: i.path.join(\".\"),\n message: i.message,\n })),\n );\n }\n\n // v0.3 (D) — wrap in the versioned envelope so future schema\n // changes can detect what version produced this row. Themes\n // that haven't declared `settingsVersion` get `1` (the v0.2\n // baseline) for forward-compat with the migration pipeline.\n const wrapped: NpVersionedSettings = {\n __npVersion: theme.manifest.settingsVersion ?? 1,\n __npSettings: parsed.data,\n };\n\n const db = getDb();\n const now = new Date();\n const siteId = (await getCurrentSiteId()) ?? DEFAULT_SITE;\n await db\n .insert(npSettings)\n .values({\n siteId,\n key: settingsKey(themeId),\n value: wrapped,\n updatedAt: now,\n updatedBy,\n })\n .onConflictDoUpdate({\n target: [npSettings.siteId, npSettings.key],\n set: { value: wrapped, updatedAt: now, updatedBy },\n });\n\n return parsed.data;\n}\n\n/**\n * Whether the active theme contributes SEO hooks. The settings\n * save path consults this to decide whether to additionally\n * invalidate `nx:sitemap:*` / `nx:feed:*` tags alongside the\n * always-busted `nx:theme:<siteId>`. Implemented here (in core)\n * so the API layer in `apps/web` doesn't need to duck-type the\n * theme `impl`.\n */\nexport async function activeThemeContributesSeo(): Promise<boolean> {\n const theme = await getActiveTheme();\n if (!theme) return false;\n // `impl` is opaque to core; we do a structural check.\n const impl = theme.impl as\n | { seo?: { sitemapEntries?: unknown; feedEntries?: unknown } }\n | undefined;\n if (!impl?.seo) return false;\n return Boolean(impl.seo.sitemapEntries || impl.seo.feedEntries);\n}\n","import { getActiveTheme } from \"./registry.js\";\n\n/**\n * Phase F.6 — extract the active theme's declared nav\n * locations as plain JSON metadata, narrowed structurally\n * because core treats `theme.impl` as opaque (`unknown`).\n *\n * Used by:\n * - The admin nav editor's location-dropdown endpoint\n * (`/api/navigation/locations`) so the dropdown surfaces\n * theme-named slots with friendly labels.\n * - Future cookbook recipes that surface \"this theme\n * expects you to fill in these menus\" guidance.\n *\n * Returns `[]` when no theme is active or the active theme\n * doesn't declare any locations.\n */\n\nexport interface NpThemeNavLocationDescriptor {\n /** Location key, e.g. `\"primary\"`. Used as the `location`\n * argument to `getNavigation(...)` and the database row's\n * `(siteId, location)` lookup. */\n key: string;\n label: string;\n description?: string;\n maxItems?: number;\n}\n\ninterface ImplShape {\n navLocations?: Record<\n string,\n {\n label?: unknown;\n description?: unknown;\n maxItems?: unknown;\n }\n >;\n}\n\n/**\n * Pure extractor — narrows a theme `impl` (opaque from core's\n * perspective) to a flat list of validated location\n * descriptors. Exported for unit testability without the DB\n * roundtrip that `getActiveThemeNavLocations` does.\n */\nexport function extractNavLocationsFromImpl(\n impl: unknown,\n): NpThemeNavLocationDescriptor[] {\n const shape = impl as ImplShape | undefined;\n const declared = shape?.navLocations;\n if (!declared || typeof declared !== \"object\") return [];\n\n const out: NpThemeNavLocationDescriptor[] = [];\n for (const [key, raw] of Object.entries(declared)) {\n if (!raw || typeof raw !== \"object\") continue;\n if (typeof raw.label !== \"string\") continue;\n out.push({\n key,\n label: raw.label,\n description:\n typeof raw.description === \"string\" ? raw.description : undefined,\n maxItems:\n typeof raw.maxItems === \"number\" ? raw.maxItems : undefined,\n });\n }\n return out;\n}\n\nexport async function getActiveThemeNavLocations(): Promise<\n NpThemeNavLocationDescriptor[]\n> {\n const theme = await getActiveTheme();\n if (!theme) return [];\n return extractNavLocationsFromImpl(theme.impl);\n}\n","import type { NpFeedEntry, NpSitemapEntry } from \"../seo/index.js\";\nimport { getActiveTheme } from \"./registry.js\";\n\n/**\n * Phase F.7 — pure structural narrowers for theme `notFound`,\n * `error`, and `seo` contributions. Core treats `theme.impl`\n * as opaque (`unknown`); these helpers do the duck-typing in\n * one place so the consuming routes stay readable.\n *\n * `getActiveThemeNotFoundComponent` / `…ErrorComponent` return\n * the React component refs unchanged — typing as `unknown`\n * here, the consumers (in `apps/web`) cast to ComponentType\n * at the JSX site. Core can't import `react` directly without\n * dragging the peer into a server-only package, so the cast\n * is delegated.\n */\n\nexport interface NpThemeSeoHooksExtracted {\n sitemapEntries?: () => Promise<NpSitemapEntry[]> | NpSitemapEntry[];\n feedEntries?: () => Promise<NpFeedEntry[]> | NpFeedEntry[];\n robotsTxt?: () => string | Promise<string>;\n}\n\ninterface ImplShape {\n notFound?: unknown;\n error?: unknown;\n members?: {\n notFound?: unknown;\n error?: unknown;\n };\n seo?: NpThemeSeoHooksExtracted;\n}\n\nexport function extractNotFoundComponent(impl: unknown): unknown {\n const shape = impl as ImplShape | undefined;\n return typeof shape?.notFound === \"function\" ? shape.notFound : null;\n}\n\nexport function extractErrorComponent(impl: unknown): unknown {\n const shape = impl as ImplShape | undefined;\n return typeof shape?.error === \"function\" ? shape.error : null;\n}\n\n/**\n * Phase M.3 — member-tree 404 component, with fallback to the\n * top-level `impl.notFound`. Returns `null` when neither the\n * member-specific nor the top-level component is declared (the\n * caller renders the framework default). Same opacity contract\n * as `extractNotFoundComponent` — typed as `unknown` here, the\n * consumer in `apps/web/src/app/(member)/not-found.tsx` casts\n * to `ComponentType` at the JSX site.\n */\nexport function extractMembersNotFoundComponent(impl: unknown): unknown {\n const shape = impl as ImplShape | undefined;\n const memberLevel = shape?.members?.notFound;\n if (typeof memberLevel === \"function\") return memberLevel;\n return typeof shape?.notFound === \"function\" ? shape.notFound : null;\n}\n\nexport function extractSeoHooks(impl: unknown): NpThemeSeoHooksExtracted {\n const shape = impl as ImplShape | undefined;\n const seo = shape?.seo;\n if (!seo || typeof seo !== \"object\") return {};\n const out: NpThemeSeoHooksExtracted = {};\n if (typeof seo.sitemapEntries === \"function\") {\n out.sitemapEntries = seo.sitemapEntries;\n }\n if (typeof seo.feedEntries === \"function\") {\n out.feedEntries = seo.feedEntries;\n }\n if (typeof seo.robotsTxt === \"function\") {\n out.robotsTxt = seo.robotsTxt;\n }\n return out;\n}\n\n/**\n * Async sugar over the active theme. Each helper returns a\n * fresh resolution per call; multi-site safety comes from\n * `getActiveTheme()` reading per-request site context.\n */\n\nexport async function getActiveThemeNotFound(): Promise<unknown> {\n const theme = await getActiveTheme();\n if (!theme) return null;\n return extractNotFoundComponent(theme.impl);\n}\n\nexport async function getActiveThemeError(): Promise<unknown> {\n const theme = await getActiveTheme();\n if (!theme) return null;\n return extractErrorComponent(theme.impl);\n}\n\n/**\n * Phase M.3 — async sugar for the member-tree 404. Returns the\n * theme's `impl.members.notFound` when declared, falling back\n * to its `impl.notFound` (top-level), then `null`.\n */\nexport async function getActiveThemeMembersNotFound(): Promise<unknown> {\n const theme = await getActiveTheme();\n if (!theme) return null;\n return extractMembersNotFoundComponent(theme.impl);\n}\n\nexport async function getActiveThemeSeoHooks(): Promise<NpThemeSeoHooksExtracted> {\n const theme = await getActiveTheme();\n if (!theme) return {};\n return extractSeoHooks(theme.impl);\n}\n","import { and, eq } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npSiteMemberships, npUsers } from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\nimport type { NpAuthUser, NpUserRole } from \"../config/types.js\";\n\nimport { getCurrentSiteId } from \"./context.js\";\nimport { NP_DEFAULT_SITE_ID } from \"./registry.js\";\n\n/**\n * Phase 15.5 — per-site role memberships.\n *\n * `npUsers.role` stays the \"global default role\" (used by\n * existing single-tenant code and as a fallback when no\n * explicit membership exists for a given site). New\n * multi-tenant deployments grant explicit memberships via\n * the helpers here; the `isSuperAdmin` flag (also new in\n * 15.5) bypasses membership checks entirely so a super-admin\n * can administer every site without having to be enrolled\n * on each one individually.\n *\n * The framework's existing `hasRole(user, minRole)` keeps\n * working as a global check. New site-scoped checks should\n * use `hasRoleOnSite(user, siteId, minRole)`.\n */\n\nexport interface SiteMembership {\n siteId: string;\n userId: string;\n role: NpUserRole;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport async function listSiteMemberships(\n siteId: string,\n): Promise<SiteMembership[]> {\n const db = getDb();\n const rows = await db\n .select()\n .from(npSiteMemberships)\n .where(eq(npSiteMemberships.siteId, siteId));\n return rows.map((row) => ({\n siteId: row.siteId,\n userId: row.userId,\n role: row.role as NpUserRole,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n}\n\nexport async function listMembershipsForUser(\n userId: string,\n): Promise<SiteMembership[]> {\n const db = getDb();\n const rows = await db\n .select()\n .from(npSiteMemberships)\n .where(eq(npSiteMemberships.userId, userId));\n return rows.map((row) => ({\n siteId: row.siteId,\n userId: row.userId,\n role: row.role as NpUserRole,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n}\n\nexport async function getMembership(\n siteId: string,\n userId: string,\n): Promise<SiteMembership | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSiteMemberships)\n .where(\n and(\n eq(npSiteMemberships.siteId, siteId),\n eq(npSiteMemberships.userId, userId),\n ),\n )\n .limit(1);\n if (!row) return null;\n return {\n siteId: row.siteId,\n userId: row.userId,\n role: row.role as NpUserRole,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport async function grantSiteMembership(\n siteId: string,\n userId: string,\n role: NpUserRole,\n): Promise<SiteMembership> {\n const db = getDb();\n const now = new Date();\n const [row] = await db\n .insert(npSiteMemberships)\n .values({ siteId, userId, role, createdAt: now, updatedAt: now })\n .onConflictDoUpdate({\n target: [npSiteMemberships.siteId, npSiteMemberships.userId],\n set: { role, updatedAt: now },\n })\n .returning();\n if (!row) throw new Error(\"Failed to grant membership\");\n return {\n siteId: row.siteId,\n userId: row.userId,\n role: row.role as NpUserRole,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport async function revokeSiteMembership(\n siteId: string,\n userId: string,\n): Promise<void> {\n const db = getDb();\n await db\n .delete(npSiteMemberships)\n .where(\n and(\n eq(npSiteMemberships.siteId, siteId),\n eq(npSiteMemberships.userId, userId),\n ),\n );\n}\n\n/**\n * Promote / demote a user's super-admin status. Super-admins\n * bypass per-site membership checks; this is the framework's\n * \"I can do anything\" gate so it should be granted sparingly\n * (one or two operators per deployment, typically).\n */\nexport async function setSuperAdmin(\n userId: string,\n isSuperAdmin: boolean,\n): Promise<void> {\n const db = getDb();\n const result = await db\n .update(npUsers)\n .set({ isSuperAdmin, updatedAt: new Date() })\n .where(eq(npUsers.id, userId))\n .returning({ id: npUsers.id });\n if (result.length === 0) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"userId\", message: `User \"${userId}\" not found` },\n ]);\n }\n}\n\nconst ROLE_RANK: Record<NpUserRole, number> = {\n viewer: 0,\n author: 1,\n moderator: 2,\n editor: 3,\n admin: 4,\n};\n\n/**\n * Resolve a user's effective role on a specific site:\n * 1. Super-admins always get `admin`.\n * 2. Explicit site membership wins over the global role.\n * 3. Fallback: the user's global `npUsers.role` (preserves\n * single-tenant behavior).\n *\n * Use this rather than `user.role` for any check that should\n * respect tenant boundaries.\n */\nexport async function resolveUserRoleOnSite(\n user: NpAuthUser,\n siteId: string,\n): Promise<NpUserRole> {\n // Super-admin shortcut — read from npUsers (the JWT may\n // not carry the flag).\n const db = getDb();\n const [row] = await db\n .select({ isSuperAdmin: npUsers.isSuperAdmin, role: npUsers.role })\n .from(npUsers)\n .where(eq(npUsers.id, user.id))\n .limit(1);\n if (!row) return user.role;\n if (row.isSuperAdmin) return \"admin\";\n\n // Explicit membership for this site?\n const membership = await getMembership(siteId, user.id);\n if (membership) return membership.role;\n\n // Fallback to global default role.\n return row.role as NpUserRole;\n}\n\n/**\n * Site-scoped variant of `hasRole`. Resolves the user's\n * effective role on the site (super-admin → admin, explicit\n * membership → that role, otherwise global default) and\n * compares against `minRole` using the same rank order\n * `hasRole` uses.\n *\n * Defaults to the current request site (or the framework's\n * `default` site when no resolver is wired) so callers don't\n * have to thread siteId everywhere.\n */\nexport async function hasRoleOnSite(\n user: NpAuthUser,\n minRole: NpUserRole,\n siteId?: string,\n): Promise<boolean> {\n const targetSite =\n siteId ?? (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const role = await resolveUserRoleOnSite(user, targetSite);\n return ROLE_RANK[role] >= ROLE_RANK[minRole];\n}\n\n/**\n * Quick boolean check for super-admin status. Cheaper than\n * `resolveUserRoleOnSite` when the caller only needs to know\n * \"can this user manage the framework as a whole?\".\n */\nexport async function isSuperAdmin(user: NpAuthUser): Promise<boolean> {\n const db = getDb();\n const [row] = await db\n .select({ isSuperAdmin: npUsers.isSuperAdmin })\n .from(npUsers)\n .where(eq(npUsers.id, user.id))\n .limit(1);\n return Boolean(row?.isSuperAdmin);\n}\n","import { eq } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { npPlugins } from \"../db/schema/system.js\";\nimport { invalidatePluginEnabled } from \"./enabled-gate.js\";\n\n/**\n * G.1 — `np_plugins` is now a lean meta row: `(id, enabled,\n * installed_at, updated_at)`. The legacy `config` jsonb column was\n * dropped in favor of `np_settings` rows keyed by `plugin.config:<id>`\n * (see `packages/core/src/plugins/config.ts` and decision E in\n * `docs/design/plugin-config-auto-form.md`). Read / write plugin\n * config through `getPluginConfig` / `setPluginConfig` from the\n * config module — `getPluginState` only knows about the enable flag.\n */\nexport interface NpPluginState {\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n}\n\nexport interface NpPluginStateUpdate {\n enabled?: boolean;\n}\n\ninterface DrizzleDb {\n select: NodePgDatabase<Record<string, unknown>>[\"select\"];\n insert: NodePgDatabase<Record<string, unknown>>[\"insert\"];\n update: NodePgDatabase<Record<string, unknown>>[\"update\"];\n}\n\nfunction toState(row: {\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n}): NpPluginState {\n return {\n id: row.id,\n enabled: row.enabled,\n installedAt: row.installedAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport async function listPluginStates(\n db: NodePgDatabase<Record<string, unknown>>,\n): Promise<NpPluginState[]> {\n const rows = (await (db as unknown as DrizzleDb).select().from(npPlugins)) as Array<{\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n }>;\n\n return rows.map(toState);\n}\n\nexport async function getPluginState(\n db: NodePgDatabase<Record<string, unknown>>,\n id: string,\n): Promise<NpPluginState | null> {\n const rows = (await (db as unknown as DrizzleDb)\n .select()\n .from(npPlugins)\n .where(eq(npPlugins.id, id))\n .limit(1)) as Array<{\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n }>;\n\n return rows[0] ? toState(rows[0]) : null;\n}\n\n/**\n * Ensures every known plugin id has a row in `np_plugins`. Missing rows are\n * inserted with `enabled=true`. Existing rows are never touched — this is\n * called on boot and must not clobber operator edits.\n *\n * Uses a single INSERT … ON CONFLICT DO NOTHING so concurrent boots (multi-\n * process deployments) can all race safely without unique-key violations.\n */\nexport async function syncPluginRegistrations(\n db: NodePgDatabase<Record<string, unknown>>,\n pluginIds: readonly string[],\n): Promise<void> {\n if (pluginIds.length === 0) return;\n\n const now = new Date();\n await db\n .insert(npPlugins)\n .values(\n pluginIds.map((id) => ({\n id,\n enabled: true,\n installedAt: now,\n updatedAt: now,\n })),\n )\n .onConflictDoNothing({ target: npPlugins.id });\n}\n\nexport async function updatePluginState(\n db: NodePgDatabase<Record<string, unknown>>,\n id: string,\n patch: NpPluginStateUpdate,\n): Promise<NpPluginState | null> {\n const values: Record<string, unknown> = {\n updatedAt: new Date(),\n };\n\n if (patch.enabled !== undefined) {\n values.enabled = patch.enabled;\n }\n\n const rows = (await (db as unknown as DrizzleDb)\n .update(npPlugins)\n .set(values)\n .where(eq(npPlugins.id, id))\n .returning()) as Array<{\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n }>;\n\n // Drop the cached enabled flag so the very next dispatch re-reads the row\n // instead of waiting out the TTL. Without this, a toggle from the admin UI\n // would feel laggy for up to 5s.\n if (patch.enabled !== undefined) {\n invalidatePluginEnabled(id);\n }\n\n return rows[0] ? toState(rows[0]) : null;\n}\n"],"mappings":"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","names":["synth","merged","injectedHere","and","eq","getPluginTemplatesForCollection","and","eq","Readable","and","eq","and","eq","parsed","and","eq","eq","and","isSuperAdmin","eq","eq"]}
1
+ {"version":3,"sources":["../src/config/types.ts","../src/config/define-config.ts","../src/themes/merge-requirements.ts","../src/config/validation.ts","../src/config/define-collection.ts","../src/content/helpers.ts","../src/theme/defaults.ts","../src/themes/registry.ts","../src/storage/local.ts","../src/storage/s3.ts","../src/storage/index.ts","../src/email/smtp.ts","../src/theme/sanitize.ts","../src/themes/requirements.ts","../src/themes/settings.ts","../src/themes/nav-locations.ts","../src/themes/error-seo.ts","../src/sites/memberships.ts","../src/plugins/persistence.ts"],"sourcesContent":["export type NpUserRole = \"admin\" | \"editor\" | \"moderator\" | \"author\" | \"viewer\";\n\nexport interface NpAuthUser {\n id: string;\n email: string;\n name: string;\n role: NpUserRole;\n tokenVersion: number;\n}\n\nexport type NpAccessFunction = (args: {\n user: NpAuthUser | null;\n doc?: Record<string, unknown>;\n data?: Record<string, unknown>;\n}) => boolean | Promise<boolean>;\n\n/**\n * Free-form predicate. Server-only — functions don't survive the\n * server→client boundary in Next.js (the framework's\n * `toClientCollectionConfig` strips them). For conditions that\n * need to run in the admin editor's browser-side renderer, use\n * `NpFieldConditionExpr` (serializable JSON shape) instead.\n */\nexport type NpFieldCondition = (\n data: Record<string, unknown>,\n siblingData: Record<string, unknown>,\n) => boolean;\n\n/**\n * Serializable condition predicate. Evaluated client-side (admin\n * editor) AND server-side (pipeline validation) from the same\n * declaration — survives the RSC serialization boundary because\n * it's plain JSON.\n *\n * Examples:\n * `{ when: \"kind\", equals: \"doc\" }`\n * `{ when: \"kind\", notEquals: \"doc\" }`\n * `{ when: \"kind\", in: [\"doc\", \"page\"] }`\n * `{ when: \"wpOriginalAuthor\", exists: true }`\n * `{ all: [{ when: \"kind\", equals: \"doc\" }, { when: \"publishedAt\", exists: true }] }`\n * `{ any: [{ when: \"kind\", equals: \"doc\" }, { when: \"kind\", equals: \"page\" }] }`\n *\n * `exists: true` returns true when the value is defined, not null,\n * not the empty string, and not an empty array. `exists: false`\n * is the inverse. `equals` / `notEquals` use strict equality.\n * `in` / `notIn` check membership against an unknown[] list.\n * `all` / `any` are AND / OR over a list of nested expressions.\n */\nexport type NpFieldConditionExpr =\n | { when: string; equals: unknown }\n | { when: string; notEquals: unknown }\n | { when: string; in: unknown[] }\n | { when: string; notIn: unknown[] }\n | { when: string; exists: boolean }\n | { all: NpFieldConditionExpr[] }\n | { any: NpFieldConditionExpr[] };\n\nexport type NpFieldValidator = (\n value: unknown,\n args: { data: Record<string, unknown>; siblingData: Record<string, unknown> },\n) => string | true | Promise<string | true>;\n\nexport type NpRichTextContent = Record<string, unknown>;\n\nexport interface NpEditorConfig {\n features?: string[];\n // Other knobs (e.g. `onUploadImage` for the Insert Image dialog\n // that landed in 9.7j) are typed in `@nexpress/editor`'s own\n // `NpEditorConfig`. Keeping core's version minimal avoids\n // dragging the DOM lib (`File`, `Blob`) into the server-evaluated\n // collection config types.\n}\n\ninterface NpFieldBase {\n name: string;\n label?: string;\n required?: boolean;\n defaultValue?: unknown;\n hidden?: boolean;\n admin?: {\n description?: string;\n placeholder?: string;\n readOnly?: boolean;\n condition?: NpFieldCondition | NpFieldConditionExpr;\n width?: string;\n /**\n * Optional override for the admin field renderer. The default\n * renderer dispatches on `type` (text → input, textarea →\n * textarea, etc.); `kind` overrides that with a specialized\n * widget.\n * - `templatePicker` (Phase 11.3) replaces the input with a\n * dropdown sourced from the active theme's\n * `templates.{collection}` registry.\n * - `title` renders a large borderless headline input that\n * sits above the rest of the form (intended for the\n * primary title of a document). The edit view skips the\n * Card wrapper around it so the title flows naturally\n * into the editor canvas underneath.\n */\n kind?: \"templatePicker\" | \"title\";\n /**\n * Where the field should land in the edit view's two-column\n * layout. Mark publishing-related metadata (SEO, template\n * choice, scheduling inputs) as `\"sidebar\"` so they group\n * with Status / Slug in the sticky right column rather than\n * competing with the primary editing surface.\n *\n * When unset, the legacy heuristic decides: `type: \"date\"`\n * fields, fields with an explicit `admin.width`, and the\n * well-known names `status` / `publishedAt` / `slug` all\n * land in the sidebar; everything else goes to main. An\n * explicit `\"main\"` overrides that heuristic — useful for\n * surfacing a date input in the primary column.\n */\n position?: \"main\" | \"sidebar\";\n /**\n * Sidebar grouping label. Sidebar fields with the same\n * `group` render together in one collapsible Card with the\n * group name as the title. Fields with no group fall into\n * the default \"Publish\" Card. Group order in the rendered\n * sidebar follows first-seen order in the collection's\n * `fields` array — operators control layout by ordering.\n *\n * Examples: `\"Publish\"`, `\"Lead\"`, `\"Taxonomy\"`, `\"Author\"`,\n * `\"Hierarchy\"`, `\"SEO\"`. The group label is the visible\n * Card title (not a slug), so it doesn't have to be\n * machine-friendly.\n *\n * Only meaningful when `position: \"sidebar\"`. Main-column\n * fields ignore this — they render in field-array order\n * without grouping.\n */\n group?: string;\n /**\n * The id of the theme whose `requires.collections.<slug>.fields`\n * contributed this field. Stamped by `mergeThemeRequirements`;\n * never set this from operator config. Same convention as\n * `admin._themeOrigin` at the collection level and per-`kinds`\n * entry: when an operator switches to a different active\n * theme, the admin filters out fields whose origin doesn't\n * match. Operator-declared fields carry no origin and always\n * pass through.\n */\n _themeOrigin?: string;\n };\n validate?: NpFieldValidator;\n}\n\nexport interface NpTextField extends NpFieldBase {\n type: \"text\";\n minLength?: number;\n maxLength?: number;\n unique?: boolean;\n}\n\nexport interface NpTextareaField extends NpFieldBase {\n type: \"textarea\";\n minLength?: number;\n maxLength?: number;\n rows?: number;\n}\n\nexport interface NpNumberField extends NpFieldBase {\n type: \"number\";\n min?: number;\n max?: number;\n step?: number;\n integerOnly?: boolean;\n}\n\nexport interface NpRichTextField extends NpFieldBase {\n type: \"richText\";\n editor?: NpEditorConfig;\n}\n\nexport interface NpBlocksField extends NpFieldBase {\n type: \"blocks\";\n allowedBlocks?: string[];\n minRows?: number;\n maxRows?: number;\n}\n\nexport interface NpCheckboxField extends NpFieldBase {\n type: \"checkbox\";\n defaultValue?: boolean;\n}\n\nexport interface NpDateField extends NpFieldBase {\n type: \"date\";\n /**\n * Default value the Drizzle schema generator emits as a SQL\n * `DEFAULT` clause. Accepted shapes:\n *\n * - `\"now\"` sentinel → `.defaultNow()` → `DEFAULT now()`\n * - A `Date` instance → `.default(new Date(\"<iso>\"))`\n * - Any ISO 8601 string → parsed via `new Date(...)` and\n * emitted identically to the Date case.\n *\n * Anything else is dropped silently at codegen time (mirroring\n * the defensive shape used for other scalar fields). Required\n * NOT NULL date columns paired with `defaultValue: \"now\"` let\n * an `ALTER TABLE … ADD COLUMN` against a populated table\n * succeed without a backfill step.\n */\n defaultValue?: Date | string;\n pickerOptions?: {\n format?: string;\n includeTime?: boolean;\n };\n}\n\nexport interface NpUploadField extends NpFieldBase {\n type: \"upload\";\n relationTo: string;\n}\n\nexport interface NpRelationshipField extends NpFieldBase {\n type: \"relationship\";\n relationTo: string | string[];\n hasMany?: boolean;\n filterOptions?: Record<string, unknown>;\n}\n\nexport interface NpSelectField extends NpFieldBase {\n type: \"select\";\n options: Array<{ label: string; value: string }>;\n hasMany?: boolean;\n}\n\nexport interface NpRadioField extends NpFieldBase {\n type: \"radio\";\n options: Array<{ label: string; value: string }>;\n}\n\nexport interface NpEmailField extends NpFieldBase {\n type: \"email\";\n}\n\nexport interface NpJsonField extends NpFieldBase {\n type: \"json\";\n}\n\nexport interface NpArrayField extends NpFieldBase {\n type: \"array\";\n fields: NpFieldConfig[];\n minRows?: number;\n maxRows?: number;\n}\n\nexport interface NpGroupField extends NpFieldBase {\n type: \"group\";\n fields: NpFieldConfig[];\n}\n\nexport interface NpRowField {\n type: \"row\";\n fields: NpFieldConfig[];\n}\n\nexport interface NpCollapsibleField {\n type: \"collapsible\";\n label: string;\n fields: NpFieldConfig[];\n}\n\nexport type NpFieldConfig =\n | NpTextField\n | NpTextareaField\n | NpNumberField\n | NpRichTextField\n | NpBlocksField\n | NpCheckboxField\n | NpDateField\n | NpUploadField\n | NpRelationshipField\n | NpSelectField\n | NpRadioField\n | NpEmailField\n | NpJsonField\n | NpArrayField\n | NpGroupField\n | NpRowField\n | NpCollapsibleField;\n\n/**\n * Polymorphic actor reference for collection hooks. Phase 9.7o\n * widened the hook surface so plugins can react to member writes,\n * not just staff writes:\n *\n * - `{ kind: \"staff\", user }` — staff-authored write; `user` is\n * the resolved staff session as before.\n * - `{ kind: \"member\", memberId }` — member-authored write\n * (`createMemberDocument` / `updateMemberDocument` /\n * `deleteMemberDocument`).\n *\n * Hooks that only care about staff identity can switch on\n * `principal.kind === \"staff\"` and read `principal.user`. The\n * top-level `user` field is also still passed (`null` for member\n * actors) so existing hooks that destructure `{ user }` keep\n * compiling — they just need to handle the null case now.\n */\n// `NpHookPrincipal` is the historical name plugin authors see in\n// hook payloads. It's the same union as `NpPrincipal`; kept as an\n// alias so existing plugin code keeps compiling (#319).\nimport type { NpPrincipal } from \"../auth/principal.js\";\nexport type { NpPrincipal, NpPrincipal as NpHookPrincipal };\ntype NpHookPrincipal = NpPrincipal;\n\nexport type NpCollectionHook = (args: {\n data: Record<string, unknown>;\n /**\n * Resolved staff session, or `null` when the actor is a member.\n * Pre-9.7o this was always non-null because member writes\n * skipped collection hooks entirely. Hooks that key off staff\n * identity should now switch on `principal.kind` instead.\n */\n user: NpAuthUser | null;\n /** Polymorphic actor — see `NpHookPrincipal`. */\n principal: NpHookPrincipal;\n collection: string;\n originalDoc?: Record<string, unknown> | null;\n}) => Record<string, unknown> | Promise<Record<string, unknown>>;\n\nexport interface NpUploadConfig {\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n imageSizes?: NpImageSize[];\n}\n\nexport interface NpImageSize {\n name: string;\n width: number;\n height?: number;\n crop?: \"center\" | \"top\" | \"bottom\" | \"left\" | \"right\";\n}\n\nexport interface NpCollectionConfig {\n slug: string;\n labels: { singular: string; plural: string };\n slugField?:\n | boolean\n | {\n useField?: string;\n unique?: boolean;\n };\n /**\n * Phase 12.1 — opt this collection into i18n. When set, the\n * codegen adds a `locale` text column and a\n * `translation_group_id` uuid column to the generated table.\n * The slug uniqueness index becomes `(locale, slug)` so the\n * same slug can appear in two locales. Fetching helpers\n * (`findDocuments`, `getDoc`) accept a `locale` option;\n * writes require a `locale` field (the pipeline rejects\n * missing-locale writes with NpValidationError).\n *\n * Requires the top-level `i18n` config to also be set.\n * Without it, `i18n: true` here errors at config validation\n * time — the framework needs to know the locale enum to\n * validate writes.\n */\n i18n?: boolean;\n fields: NpFieldConfig[];\n access?: {\n create?: NpAccessFunction;\n read?: NpAccessFunction;\n update?: NpAccessFunction;\n delete?: NpAccessFunction;\n };\n hooks?: {\n beforeCreate?: NpCollectionHook[];\n afterCreate?: NpCollectionHook[];\n beforeUpdate?: NpCollectionHook[];\n afterUpdate?: NpCollectionHook[];\n beforeDelete?: NpCollectionHook[];\n afterDelete?: NpCollectionHook[];\n beforeRead?: NpCollectionHook[];\n afterRead?: NpCollectionHook[];\n };\n versions?: {\n drafts?: boolean | { autosave?: boolean; autosaveInterval?: number };\n max?: number;\n };\n /**\n * Community features opt-in per collection. Comments are off by\n * default; flip `comments: true` to let members post comments\n * underneath this collection's documents. Reactions ride on the\n * comment surface — sites enable reactions by enabling comments;\n * a per-collection reactions toggle isn't needed today.\n *\n * `memberWrite.create` (9.7a) lets logged-in members create\n * documents in this collection without needing a staff role.\n * `memberWrite.update` / `memberWrite.delete` (9.7b) extend the\n * member-write surface with owner-only edit / delete (the row's\n * `member_author_id` must match the caller). The staff\n * `access.create` / `access.delete` functions are bypassed on\n * the member path — gating is `assertNotBanned(memberId)` plus\n * the opt-in flag plus the ownership check, not the staff\n * access tree. Member-authored docs default to\n * `_status = \"published\"` and members CANNOT change status via\n * update; those transitions remain admin-side affordances\n * (a configurable default-status / moderation gate lands in a\n * follow-up).\n */\n community?: {\n comments?: boolean;\n memberWrite?: {\n create?: boolean;\n update?: boolean;\n delete?: boolean;\n /**\n * Status that member-authored creates land in by default.\n * Defaults to `\"published\"` (a member's thread is live as\n * soon as it's submitted). Set to `\"pending\"` to require a\n * mod to promote the row before it shows up on the public\n * site — a flag-on-write moderation gate without writing a\n * spam adapter. The spam adapter, if installed, can also\n * downgrade an individual row to `pending` regardless of\n * this default (`flag` verdict).\n */\n defaultStatus?: \"published\" | \"pending\";\n };\n };\n /**\n * SEO configuration. Phase 10 introduced this surface for the\n * sitemap / RSS / OG metadata pipeline. The contract is\n * opt-in: a collection appears in `/sitemap.xml` iff it\n * declares `seo.urlPath`, which maps a document row to its\n * public URL path (e.g. `(doc) => \"/blog/\" + doc.slug`).\n * Collections without `seo.urlPath` are assumed to be admin-\n * internal or rendered through a custom route the framework\n * can't introspect.\n */\n seo?: {\n /**\n * Maps a document row to the public URL path the row is\n * served at, or `null` to skip the row (e.g. a draft / a\n * row whose URL is computed dynamically and shouldn't be\n * indexed). Returned paths must start with `/`. The host\n * comes from `SITE_URL` at sitemap-build time.\n */\n urlPath?: (doc: Record<string, unknown>) => string | null;\n /**\n * Hint for sitemap consumers about how often this\n * collection's content changes. Optional — Google now\n * largely ignores it but other crawlers still honor it.\n */\n changefreq?: \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n /**\n * Sitemap priority hint, 0.0–1.0. Optional, same caveat as\n * changefreq.\n */\n priority?: number;\n };\n timestamps?: boolean;\n admin?: {\n listColumns?: string[];\n defaultSort?: string;\n group?: string;\n hidden?: boolean;\n description?: string;\n components?: {\n listView?: string;\n editView?: string;\n createView?: string;\n };\n /**\n * Opts the collection's edit view into the \"In navigation\"\n * side panel. Documents in this collection are addressable\n * from the nav editor's `type: \"page\"` picker via the\n * membership endpoint, so the operator can add/remove the\n * doc from any nav location without leaving the page.\n *\n * Defaults to `false`. The reference `pages` collection in\n * `apps/web` flips it on; sites with a `static-pages` or\n * `landing-pages` collection that should also surface in nav\n * can opt in here too.\n */\n navMembership?: boolean;\n /**\n * Lucide icon name for the admin sidebar entry. Defaults to\n * `FileText` when unset or unrecognized. Examples:\n * `\"Newspaper\"` for posts, `\"FileStack\"` for pages,\n * `\"FolderTree\"` for categories, `\"Tag\"` for tags.\n *\n * Resolved client-side by `admin-shell.tsx` against a small\n * lucide-react registry; unknown names fall back to the\n * default so a typo can't break the sidebar render.\n */\n icon?: string;\n /**\n * Framework-set. Stamped by `mergeThemeRequirements` on\n * collections it synthesised via a theme's\n * `requires.collections.<slug>.createIfAbsent: true`. The\n * admin sidebar uses it to hide collections whose owning\n * theme isn't active — the bundled-themes prebake puts\n * EVERY built-in theme's `createIfAbsent` slug into the\n * schema so swap-from-admin is migration-free, but the\n * operator shouldn't see `authors` in the sidebar while\n * running the docs theme.\n *\n * NEVER set this by hand from operator config. The\n * underscore is intentional — it marks \"this is the\n * framework's view of the config, not the operator's\n * intent\". Operator-declared collections (slug exists\n * before merge) keep this unset and always show.\n */\n _themeOrigin?: string;\n /**\n * Framework-set. Stamped by `mergeThemeRequirements` from\n * `theme.manifest.requires.collections.<slug>.kinds`,\n * unioned across all registered themes. The admin sidebar\n * walks this map to render per-kind entries under \"Content\"\n * (universal-content-model #748).\n *\n * Keyed by the discriminator value declared on the\n * `kind` field's options. Empty / missing → admin shows a\n * single collection entry like before.\n */\n kinds?: Record<string, NpThemeCollectionKind>;\n /**\n * Visual metadata for sidebar field groups. Keyed by the\n * `admin.group` label used on individual fields. The editor's\n * `SidebarGroupCard` reads this to render an icon next to\n * the group title and optionally surface a description.\n *\n * Operator-declared collections set this directly. Themes\n * contribute their own group icons via\n * `requires.collections.<slug>.groupMeta` (merged through\n * `mergeThemeRequirements`, unioned across themes with\n * last-write-wins on per-key props).\n *\n * Groups without an entry render without an icon — same\n * behavior as before this surface existed.\n */\n groupMeta?: Record<string, NpAdminGroupMeta>;\n };\n upload?: NpUploadConfig;\n}\n\nexport interface NpBlockConfig {\n slug: string;\n labels: { singular: string; plural: string };\n fields: NpFieldConfig[];\n imageUrl?: string;\n}\n\nexport type NpBlockInstance = {\n blockType: string;\n [key: string]: unknown;\n};\n\nexport interface NpPluginConfig {\n id: string;\n name: string;\n init?: (ctx: NpPluginContext) => void | Promise<void>;\n}\n\n/**\n * Structural shape accepted by `loadPlugins()` for SDK-built plugins.\n * Declared here rather than imported from `@nexpress/plugin-sdk` to avoid a\n * dependency cycle (plugin-sdk already depends on core).\n */\nexport interface NpResolvedPluginLike {\n manifest: {\n id: string;\n name: string;\n version?: string;\n description?: string;\n capabilities: readonly string[];\n };\n hooks?: Record<string, unknown>;\n routes?: ReadonlyArray<{\n path: string;\n method: string;\n handler: unknown;\n description?: string;\n auth?: boolean;\n }>;\n /**\n * Phase 12.5 — optional UI string bundles per locale. Keys\n * are plugin-namespaced strings the plugin's own templates /\n * routes / admin pages call `t()` against. The host merges\n * every plugin's bundle into the global registry at boot;\n * later plugins overwrite earlier ones on key collision so\n * sites can layer overrides via plugin order.\n */\n i18n?: Record<string, Record<string, string>>;\n /**\n * Phase 14.5 — page templates the plugin contributes to the\n * shared template registry. Same shape as a theme's\n * `impl.templates`: keyed by collection slug, then by\n * template id, with `{ label, description?, component }`\n * values. The plugin host merges these at boot;\n * `getThemeTemplateSummaries` returns plugin templates +\n * theme templates as a union, with theme entries winning\n * id collisions (the active theme is the site's design\n * authority).\n *\n * templates: {\n * pages: {\n * docs: { label: \"Documentation\", component: DocsTemplate },\n * },\n * }\n */\n templates?: Record<string, Record<string, unknown>>;\n /**\n * Plugin page routes (#623). React-free shape — the framework\n * narrows `component` to `ComponentType<NpRouteRenderProps>`\n * at the dispatcher site. See\n * `docs/design/plugin-routes.md` for the contract +\n * precedence rules.\n */\n pageRoutes?: ReadonlyArray<{\n pattern: string;\n component: unknown;\n metadata?: unknown;\n surface?: \"site\" | \"member\";\n locale?: \"auto\" | \"none\";\n }>;\n}\n\nexport interface NpPluginContext {\n addCollection: (config: NpCollectionConfig) => void;\n addBlock: (config: NpBlockConfig) => void;\n addHook: (collection: string, event: string, hook: NpCollectionHook) => void;\n}\n\nexport interface NpNavItem {\n id: string;\n label: string;\n type: \"link\" | \"collection\" | \"page\";\n url?: string;\n collection?: string;\n /**\n * Set when `type === \"page\"` to record which collection the\n * referenced doc lives in. Defaults to `\"pages\"` when absent so\n * existing nav rows keep resolving against the reference page\n * collection unchanged. The URL resolver walks the doc through\n * the collection's `seo.urlPath` to produce the public path.\n *\n * The editor doesn't expose this as an editable field — the\n * panel that adds the item knows its source collection and\n * stamps it at write time.\n */\n collectionSlug?: string;\n pageId?: string;\n children?: NpNavItem[];\n}\n\n/**\n * Phase 11.1 — theme manifest. Pure metadata, kept React-free\n * so it can live in `@nexpress/core` (which is server-only and\n * intentionally has no React peer). The full theme — shell,\n * slots, templates with React component types — lives in\n * `@nexpress/theme` via `defineTheme()`. The registry stores\n * `NpRegisteredTheme` instances; `impl` is opaque to core but\n * typed for consumers downstream.\n */\nexport interface NpThemeManifest {\n id: string;\n name: string;\n version: string;\n description?: string;\n author?: { name: string; url?: string };\n /** Optional minimum NexPress version this theme requires. */\n nexpress?: { minVersion?: string };\n /**\n * Phase F.1 (theme v0.2) — declared data-shape requirements.\n *\n * Themes whose components assume specific collection fields\n * (e.g. magazine theme reads `posts.featured`) declare them\n * here. Two consumers read this:\n *\n * 1. `defineConfig` calls `mergeThemeRequirements` to UNION\n * declared fields into the operator's `collections` array\n * at config-resolution time. Operator-authored fields with\n * the same name always win, so the merge is non-\n * destructive. The framework's codegen then picks up the\n * union shape; the operator's next `pnpm db:generate &&\n * pnpm db:migrate` materialises the columns. Operators\n * add a theme via `pnpm nexpress theme add <pkg>` — there\n * is no longer a `theme:install` AST-patcher that touches\n * `src/collections/*.ts` (that flow was retired alongside\n * the auto-merge).\n * 2. Admin theme switcher: compares against the resolved\n * collections at activation time and surfaces residual\n * mismatches — chiefly TYPE conflicts where the operator\n * declared a field with the same name but a different\n * `type` and the merge respected the operator's choice.\n */\n requires?: {\n collections?: Record<string, NpThemeCollectionRequirement>;\n };\n /**\n * Phase F.3 (theme v0.2) — operator-tunable theme options.\n *\n * A Zod schema describing settings the admin should expose as\n * a form. The framework generates the form fields from the\n * schema (no per-theme admin UI code), persists submissions in\n * `np_settings` keyed by `theme.settings:<themeId>`, and\n * exposes the parsed value to theme components via\n * `getThemeSettings()`.\n *\n * Supported field types in v0.2:\n * - z.string() / z.string().url() / z.string().regex(...)\n * - z.number().int().min().max()\n * - z.boolean()\n * - z.enum([...])\n * - z.array(z.object({...}))\n * - z.object({...})\n *\n * Use `.default(value)` for initial form values and\n * `.describe(\"Help text\")` for the field label/description\n * the admin auto-form picks up.\n *\n * Typed as `unknown` here so `@nexpress/core` doesn't have to\n * re-export Zod type unions through every public surface;\n * theme authors writing `defineTheme({ manifest: { ... } })`\n * still get the proper Zod typing because they construct the\n * schema with `z.object(...)` themselves. The framework\n * narrows back to `ZodTypeAny` at the call site that runs\n * introspection / validation.\n */\n settingsSchema?: unknown;\n /**\n * v0.3 (D) — settings schema version, used by the migration\n * pipeline to detect when stored settings need upgrading.\n *\n * Theme authors bump this whenever `settingsSchema` changes\n * shape in a non-additive way (renaming a field, removing one,\n * tightening a default). Adding a NEW optional field is\n * compatible without bumping — Zod fills the missing key with\n * the field's default on parse.\n *\n * The framework treats absent / undefined as `1` (the v0.2\n * baseline). Themes that never bump stay forever at v1, no\n * migration ever runs.\n */\n settingsVersion?: number;\n /**\n * v0.3 (D) — migration function that brings a value persisted\n * under an older `settingsVersion` up to the current shape.\n *\n * Called on read when stored version < `settingsVersion`. The\n * function receives the OLD value (whatever shape v(N-1) had)\n * and the version it came from (so multi-step migrations can\n * branch). Returns a value that matches the CURRENT\n * `settingsSchema`. The framework re-parses the result and\n * falls back to schema defaults if the migration's output\n * still doesn't validate (defensive — a buggy migrate fn\n * shouldn't blow up the public site).\n *\n * The framework persists the migrated value back on the\n * operator's NEXT save through the admin form. Read paths\n * don't auto-write; the migration is recomputed on each read\n * until the operator triggers a save. That keeps read paths\n * pure (matches every other cached read in the framework).\n *\n * Example for a `accent` → `accentColor` rename at v2:\n *\n * ```ts\n * defineTheme({\n * manifest: {\n * settingsSchema: z.object({\n * accentColor: z.string().regex(...).optional(),\n * ...\n * }),\n * settingsVersion: 2,\n * settingsMigrate: (old, from) => {\n * if (from === 1) {\n * const o = old as { accent?: string };\n * return { ...o, accentColor: o.accent };\n * }\n * return old;\n * },\n * }\n * })\n * ```\n */\n settingsMigrate?: (old: unknown, fromVersion: number) => unknown;\n}\n\n/**\n * One collection's worth of theme requirements. The collection\n * may exist (the framework's auto-merge appends fields to the\n * existing array) or not (the merge skips it unless\n * `createIfAbsent` is set, in which case a minimal collection is\n * synthesised on the resolved config).\n */\nexport interface NpThemeCollectionRequirement {\n fields?: Record<string, NpThemeFieldRequirement>;\n /** True → the framework's `mergeThemeRequirements` step in\n * `defineConfig` synthesises a minimal collection (slug +\n * labels + the declared fields) when no collection with this\n * slug is registered. Operator-authored collections of the\n * same slug always take precedence. */\n createIfAbsent?: boolean;\n /**\n * Per-kind metadata for the `kind` discriminator field\n * (universal-content-model #748). Themes contribute one entry\n * per kind they author content for; the framework's auto-merge\n * unions entries across registered themes so the admin sidebar\n * and the public-site router both see one canonical map.\n *\n * Keyed by the option value declared on `fields.kind.options`\n * (e.g. `kinds.doc` matches the option whose `value: \"doc\"`).\n * The collection slug remains `posts`; kinds are a presentation\n * split, not a separate table.\n *\n * The `kind` field itself doesn't have to live on this\n * collection's `fields` for the metadata to apply — themes that\n * extend a single kind (`fields.kind.options: [{value:\"doc\"}]`\n * + `kinds.doc: {...}`) ship both together and the merge unions\n * them with whatever other themes declare. A `kinds` block on a\n * collection without a corresponding select field is a no-op\n * (the admin shows the regular collection list view).\n */\n kinds?: Record<string, NpThemeCollectionKind>;\n /**\n * Sidebar group metadata the theme contributes. Keyed by the\n * `admin.group` label the theme uses on its contributed fields\n * (e.g. theme-magazine contributes `Magazine: { icon: \"Newspaper\" }`).\n * Merged into the collection's `admin.groupMeta` via\n * last-write-wins union — two themes claiming the same group\n * label get the later theme's icon / description.\n *\n * Declaring a group key without contributing fields with the\n * same `admin.group` is allowed (the entry is unused but\n * harmless) — useful for overriding a framework default's\n * icon without adding any new fields.\n */\n groupMeta?: Record<string, NpAdminGroupMeta>;\n}\n\n/**\n * One kind entry — admin nav + public URL metadata for a single\n * discriminator value on a `select` field (typically `posts.kind`).\n *\n * Field merge: two themes declaring the same kind value get\n * last-wins on every property. Operators rarely need to redefine\n * a kind their theme already ships.\n */\n/**\n * Per-group sidebar metadata. Resolves at runtime in the admin\n * edit view; not codegen'd into the DB schema.\n */\nexport interface NpAdminGroupMeta {\n /**\n * Lucide icon name (no `Icon` suffix) shown next to the\n * group title in the editor sidebar. Examples: `\"Calendar\"`,\n * `\"BookOpen\"`, `\"Briefcase\"`. Resolved client-side; unknown\n * names render no icon (silent fallback, no warning).\n */\n icon?: string;\n /**\n * One-line description shown beneath the group title. Useful\n * for operator hints like \"Search-result preview + social\n * card.\" Truncated by the admin if it's long.\n */\n description?: string;\n}\n\nexport interface NpThemeCollectionKind {\n /** Singular human label — \"Doc\", \"Project\", \"Article\". */\n label: string;\n /** Plural label for the admin sidebar entry — \"Documentation\". */\n labelPlural: string;\n /** Lucide icon name (no `Icon` suffix) — \"BookOpen\", \"Briefcase\". */\n icon?: string;\n /**\n * Public-site URL pattern. `:slug` is the only supported param;\n * the catch-all router (`apps/web/src/app/(site)/[[...slug]]/page.tsx`)\n * matches the path, extracts the slug, and queries the host\n * collection with `where: { kind: \"<this-key>\", slug: \"<match>\" }`.\n *\n * Omit to fall back to the framework default (`/<collection-slug>/<slug>`,\n * shared with the kind=null catch-all path). Two kinds declaring\n * the same urlPattern collide and the first wins; the admin\n * surfaces this via the requirements diff.\n */\n urlPattern?: string;\n /**\n * True → admin's per-kind list view surfaces `parent` + `order`\n * controls and renders rows as a tree. Themes with hierarchical\n * content (docs, sections) opt in; flat kinds leave it false.\n */\n hierarchical?: boolean;\n /**\n * Framework-set. Stamped by `mergeThemeRequirements` with the\n * id of the theme whose `requires.collections.<slug>.kinds`\n * contributed this entry. The admin sidebar reads it to gate\n * per-kind nav entries on the active theme — the bundled-themes\n * prebake unions every built-in's kinds onto the schema, but\n * only the active theme's kinds deserve sidebar real estate.\n *\n * NEVER set this by hand from operator config. The underscore\n * is intentional — same convention as `admin._themeOrigin` at\n * the collection level.\n */\n _themeOrigin?: string;\n}\n\n/**\n * One field's requirement. The `type` matches an `NpFieldConfig`\n * variant's `type` string exactly so the activation check can\n * compare without translation.\n */\nexport interface NpThemeFieldRequirement {\n type:\n | \"text\"\n | \"textarea\"\n | \"richText\"\n | \"number\"\n | \"checkbox\"\n | \"date\"\n | \"select\"\n | \"upload\"\n | \"relationship\"\n | \"blocks\";\n /** For `relationship` — the collection slug it points to. */\n relationTo?: string | string[];\n /** For `relationship` / `select` — accepts list values. */\n hasMany?: boolean;\n required?: boolean;\n /**\n * Default `true`. Set `false` for \"nice to have, theme degrades\n * gracefully without it\" — admin warning shows but at lower\n * severity, and a future F.8 may treat it as opt-in patch.\n */\n hard?: boolean;\n /**\n * For `select` only — extra options to union into the existing\n * select field. Two themes can contribute disjoint option sets\n * (e.g. theme-docs adds `kind=\"doc\"`, theme-portfolio adds\n * `kind=\"project\"`); the merge dedupes on `value` and last-wins\n * on `label`. Universal-content-model Phase U.1 (#748).\n *\n * Ignored when the merge can't find an existing select with the\n * same `name`; theme authors that need a brand-new select can't\n * synthesise one through requirements (`NpThemeFieldRequirement`\n * doesn't carry enough to construct a valid `NpSelectField`).\n */\n options?: Array<{ label: string; value: string }>;\n /**\n * Optional admin hints forwarded onto the synthesised field's\n * `admin` slot. Themes use these to bucket their contributed\n * fields into the right sidebar group and hide fields when\n * irrelevant to the active kind.\n *\n * `group` — sidebar Card grouping label\n * (e.g. `\"Media\"`, `\"SEO\"`).\n * `condition` — runtime visibility gate. Either a function\n * (server-only — stripped at the RSC boundary)\n * or a serializable expression (works in both\n * environments). Prefer the expression form so\n * the admin's client renderer can re-evaluate\n * on live form values:\n * `{ when: \"kind\", equals: \"doc\" }`\n * `{ when: \"kind\", notEquals: \"doc\" }`\n * `{ when: \"kind\", in: [\"doc\", \"page\"] }`\n * `{ when: \"wpOriginalAuthor\", exists: true }`\n * `position` — main vs sidebar column. Defaults to the\n * framework's `isSidebarField` heuristic.\n */\n admin?: {\n group?: string;\n condition?: NpFieldCondition | NpFieldConditionExpr;\n position?: \"main\" | \"sidebar\";\n };\n}\n\nexport interface NpRegisteredTheme {\n manifest: NpThemeManifest;\n /**\n * The theme's runtime implementation — shell component,\n * slot components, page templates, default tokens.\n * `@nexpress/theme` types this; core treats it as opaque so\n * the React peer dependency stays out of this package.\n */\n impl: unknown;\n}\n\nexport interface NpI18nConfig {\n /**\n * Locales this site supports. Order matters only insofar as\n * the first locale becomes the default when `defaultLocale`\n * isn't explicitly set. Locale strings are passed through to\n * BCP-47 consumers (HTML `lang` attribute, hreflang) so\n * conventional codes are recommended (`en`, `en-US`, `ko`,\n * `pt-BR`).\n */\n locales: string[];\n /**\n * Locale used when the caller doesn't specify one — drives\n * default writes and fallback reads. Must appear in `locales`.\n */\n defaultLocale: string;\n}\n\nexport interface NpConfig {\n site: {\n name: string;\n url: string;\n };\n db: {\n connectionString: string;\n pool?: { max?: number };\n };\n storage?: {\n adapter: \"local\" | \"s3\";\n local?: { directory: string; baseUrl: string };\n s3?: { bucket: string; region: string; endpoint?: string };\n };\n collections: NpCollectionConfig[];\n blocks?: NpBlockConfig[];\n editor?: NpEditorConfig;\n /**\n * Phase 11.1 — multi-theme registry. Sites declare every\n * theme they want available; admins switch between them\n * via the settings UI without rebuilding. The first theme\n * in the array is the default-active until an admin sets\n * a different one (`np_settings.activeTheme`).\n */\n themes?: NpRegisteredTheme[];\n /**\n * Phase 12.1 — i18n config. Sites that want multi-language\n * content declare every locale they intend to support here.\n * Per-collection opt-in via `defineCollection({ i18n: true })`\n * is required: only collections that declare `i18n` get the\n * `locale` / `translation_group_id` columns codegen'd onto\n * their generated table. Sites with no i18n config (or that\n * opt no collections in) keep the existing single-locale\n * shape — i18n is purely additive.\n *\n * i18n: { locales: [\"en\", \"ko\", \"ja\"], defaultLocale: \"en\" }\n *\n * `defaultLocale` is what new docs land in when the caller\n * doesn't pass an explicit locale, and what the framework\n * falls back to when a translation is missing for a requested\n * locale (the public site renders a 404 only when the doc\n * doesn't exist in any locale).\n */\n i18n?: NpI18nConfig;\n images?: {\n sizes?: NpImageSize[];\n format?: \"webp\" | \"avif\" | \"jpeg\" | \"png\";\n quality?: number;\n };\n auth?: {\n secret: string;\n tokenExpiration?: number;\n refreshTokenExpiration?: number;\n maxLoginAttempts?: number;\n lockoutDuration?: number;\n };\n plugins?: Array<NpPluginConfig | NpResolvedPluginLike>;\n typescript?: {\n outputFile?: string;\n };\n /**\n * Phase 23.5 — operational thresholds and policies for the job\n * queue. Currently only carries the stuck-job thresholds the\n * admin Jobs widget compares against; future entries land\n * additively.\n */\n jobs?: {\n /**\n * Per-state count thresholds for the admin stuck-job widget.\n * When the live + archive UNION count for a state exceeds the\n * configured value the widget shows a warning indicator. Unset\n * values fall back to sensible defaults applied by the widget\n * itself (currently `failed: 10`, `expired: 50`).\n */\n stuckThreshold?: {\n failed?: number;\n expired?: number;\n };\n };\n}\n\nexport type NpJobType =\n | \"content:afterSave\"\n | \"content:afterDelete\"\n | \"content:publishScheduled\"\n | \"media:processImage\"\n | \"media:cleanup\"\n | \"plugin:scheduledTask\"\n | \"system:revisionPrune\"\n | \"system:sessionCleanup\"\n | \"system:jobLogPrune\"\n | \"auth:sendPasswordReset\"\n | \"members:sendVerifyEmail\"\n | \"members:sendPasswordReset\"\n | \"notifications:sendDigest\";\n\n/**\n * System-level filters that aren't part of any collection's\n * document shape but still belong on the `where` clause: tenant\n * scoping, visibility gating, locale narrowing. Kept separate\n * from the document type so `Partial<T>` can stay tight while\n * advanced callers (admin queries, bulk exports) can pass these\n * escape-hatch tokens.\n */\nexport interface NpFindWhereSystemTokens {\n /**\n * Multi-site scoping. Defaults to the resolved current site.\n * Pass `\"*\"` to query across every site (admin / migration\n * use only — leaks cross-site rows).\n */\n siteId?: string;\n /**\n * Visibility gate. Anonymous traffic is auto-restricted to\n * `\"public\"`. Pass `\"*\"` to bypass (the pipeline drops the\n * filter when a user is also passed).\n */\n visibility?: \"public\" | \"private\" | \"*\";\n /**\n * `where: { locale: \"ko\" }` is equivalent to the top-level\n * `locale` option. Listed here so a typed where clause can\n * still pass it without the document type having to declare\n * a `locale` field (only i18n-enabled collections do).\n */\n locale?: string;\n /**\n * Lifecycle status filter. Every collection in the framework\n * carries a `status` column (codegen-enforced), so exposing it\n * here lets a typed `where` clause filter to published rows\n * without the doc type having to redeclare it. Accepts a\n * single value or an array (IN match).\n */\n status?: NpDocumentStatus | NpDocumentStatus[];\n}\n\n/**\n * Strip `null` and unwrap arrays so a hasMany field like\n * `categories: string[] | null` reads as a `string` for the\n * single-target filter case.\n */\ntype NpFindWhereUnwrap<V> = V extends (infer U)[] | null\n ? U\n : V extends (infer U)[]\n ? U\n : V extends infer U | null\n ? U\n : V;\n\n/**\n * The accepted value shape for a single where field. Either the\n * unwrapped scalar (single match) or an array (IN match). Array\n * with zero elements short-circuits the query to no rows; the\n * pipeline guards against the SQL syntax error this would\n * otherwise produce.\n */\ntype NpFindWhereValue<V> = NpFindWhereUnwrap<V> | NpFindWhereUnwrap<V>[];\n\n/**\n * Per-row filter. With the default `T = Record<string, unknown>`,\n * any keys are allowed (back-compat). With a typed `T` (the\n * generated wrapper functions pass their `${Pascal}Document`\n * here), only document fields plus the system tokens above are\n * accepted — typos against field names become compile errors.\n *\n * Each field accepts a single value (matched with `=`) or an\n * array (matched with `IN (...)`). For hasMany relationships\n * (where the document's field type is `string[] | null`), the\n * single-value form is the common case — \"posts in this one\n * category\" — and the array form picks up the `OR` semantics\n * across multiple targets — \"posts in any of these categories\".\n */\nexport type NpFindWhere<T extends object = Record<string, unknown>> = {\n [K in keyof T]?: NpFindWhereValue<T[K]>;\n} & {\n [K in keyof NpFindWhereSystemTokens]?: NpFindWhereSystemTokens[K];\n};\n\nexport interface NpFindOptions<T extends object = Record<string, unknown>> {\n page?: number;\n limit?: number;\n sort?: string;\n search?: string;\n where?: NpFindWhere<T>;\n /**\n * Phase 12.1 — restrict the result set to one locale on\n * i18n-enabled collections. Equivalent to passing\n * `where: { locale }`, but kept top-level for ergonomics\n * (callers don't have to know it's a column). Ignored on\n * non-i18n collections (no `locale` column to match).\n */\n locale?: string;\n}\n\nexport interface NpFindResult<T = Record<string, unknown>> {\n docs: T[];\n totalDocs: number;\n totalPages: number;\n page: number;\n limit: number;\n hasNextPage: boolean;\n hasPrevPage: boolean;\n}\n\n/**\n * Document lifecycle status. `pending` (Phase 9.7c) is a moderation\n * holding pen for member-authored docs that haven't cleared review\n * — flagged by the spam adapter or sent there because the\n * collection set `community.memberWrite.defaultStatus = \"pending\"`.\n * Public listings filter to `published`, so pending rows are\n * invisible to anonymous and non-staff members until a mod\n * promotes them.\n */\nexport type NpDocumentStatus = \"draft\" | \"scheduled\" | \"published\" | \"archived\" | \"pending\";\n\nexport interface NpSaveOptions {\n status?: NpDocumentStatus;\n /**\n * Caller-owned Drizzle transaction handle. When provided, all\n * reads + writes in the save flow run against this transaction\n * instead of opening a private one — useful for batching many\n * saves (the seed loop) so a mid-batch failure rolls back the\n * pending writes as a unit.\n *\n * Typed as `unknown` here to avoid a circular import between\n * `config/types.ts` and `collections/pipeline.ts` (where the\n * structural type lives). Callers pass the `NpTransaction`\n * value exported from `@nexpress/core`; the pipeline narrows\n * with an internal cast at the boundary.\n *\n * Pre-write hooks still fire OUTSIDE the tx (they shouldn't see\n * pending state). Post-commit hooks (`content:afterSave` job,\n * plugin `content:afterCreate` / `content:afterUpdate`) fire\n * after the inner persist block but before the caller's outer\n * tx commits — their side effects can diverge from final DB\n * state on rollback. Same trade-off as `deleteDocument({ tx })`.\n */\n tx?: unknown;\n}\n\nexport interface NpSaveResult {\n doc: Record<string, unknown>;\n operation: \"create\" | \"update\";\n}\n\n/**\n * Numeric ranking of staff roles, retained for the few non-capability\n * call sites that still need to compare role rank — chiefly\n * `hasRoleOnSite()` in `sites/memberships.ts`, which evaluates a\n * per-site membership row's role against the user's. `moderator`\n * shares author-rank because the two are parallel tracks\n * (community-mod vs. content-author authority); the rank is meaningful\n * only on the content-authoring axis.\n *\n * For staff-user authorization, use `can(user, capability)` from\n * `auth/capabilities.ts` (#273) — this hierarchy is no longer the\n * primary check.\n */\nexport const ROLE_HIERARCHY: Record<NpUserRole, number> = {\n viewer: 0,\n author: 1,\n moderator: 1,\n editor: 2,\n admin: 3,\n};\n","import { ZodError } from \"zod\";\n\nimport { mergeThemeRequirements } from \"../themes/merge-requirements.js\";\nimport { type NpConfig } from \"./types.js\";\nimport { npConfigSchema } from \"./validation.js\";\n\n/**\n * Validates the project's NpConfig against the declarative schema and returns\n * it unchanged on success. Catches common mistakes (bad collection slug,\n * missing auth.secret, malformed storage adapter, etc.) at module-eval time\n * with a clear message instead of a cryptic runtime failure once the app\n * tries to boot.\n *\n * The most common boot trip-up by far is \"auth.secret\" / \"site.url\" /\n * \"db.connectionString\" missing on a fresh install. We translate Zod's raw\n * `String must contain at least 1 character` style messages into actionable\n * \"set NP_SECRET in .env, or run `pnpm run setup`\" hints so the new operator\n * isn't googling Zod path strings.\n *\n * Unknown plugin entries are accepted here — the plugin loader does the\n * deeper validation of manifests against @nexpress/plugin-sdk.\n *\n * After validation, theme requirements are auto-merged into the\n * `collections` array via `mergeThemeRequirements`. Operators no\n * longer need to AST-patch `src/collections/*.ts` when adopting a\n * theme — adding the theme to `themes: [...]` is enough; the next\n * `pnpm db:generate && pnpm db:migrate` picks up the new\n * theme-declared columns.\n */\nexport function defineConfig(config: NpConfig): NpConfig {\n try {\n npConfigSchema.parse(config);\n } catch (err) {\n if (err instanceof ZodError) {\n throw new Error(formatConfigError(err), { cause: err });\n }\n throw err;\n }\n\n // Phase 12.1 cross-field check — a collection can only opt\n // into i18n if the top-level i18n config is set. The schema\n // can't express this with `.refine()` cleanly because it\n // would force every collection to know the parent config.\n if (config.i18n === undefined) {\n const localized = config.collections.find((c) => c.i18n === true);\n if (localized) {\n throw new Error(\n `Collection \"${localized.slug}\" sets i18n: true but the top-level config has no \\`i18n\\` block. Add \\`i18n: { locales: [...], defaultLocale: \"...\" }\\` to nexpress.config.ts.`,\n );\n }\n }\n\n // Theme auto-merge. Non-destructive: operator-authored fields\n // are never overwritten, and a no-op when no themes (or no\n // themes with `requires`) are registered. Returns the input\n // array unchanged in that case, so the equality semantics\n // existing callers rely on hold.\n const mergedCollections = mergeThemeRequirements(config.collections, config.themes);\n if (mergedCollections === config.collections) {\n return config;\n }\n return { ...config, collections: mergedCollections };\n}\n\nconst FRIENDLY_HINTS: Record<string, string> = {\n \"auth.secret\":\n \"Set `NP_SECRET` in `.env` (≥32 random chars) — `pnpm run setup` will generate one for you.\",\n \"site.url\":\n \"Set `SITE_URL` in `.env` to your public origin — `pnpm run setup` collects it.\",\n \"db.connectionString\":\n \"Set `DATABASE_URL` in `.env` to your Postgres connection string — `pnpm run setup` will write it.\",\n \"storage.s3.bucket\": \"Set `NP_S3_BUCKET` in `.env` (or switch storage to local).\",\n \"storage.s3.region\": \"Set `NP_S3_REGION` in `.env`.\",\n};\n\nfunction formatConfigError(err: ZodError): string {\n const lines = err.issues.map((issue) => {\n const path = issue.path.join(\".\");\n const hint = FRIENDLY_HINTS[path];\n if (hint) return ` • ${path}: ${hint}`;\n return ` • ${path || \"<root>\"}: ${issue.message}`;\n });\n return [\n \"Invalid NexPress config — boot aborted before any service starts.\",\n \"\",\n ...lines,\n \"\",\n \"If this is your first run, `pnpm run setup` writes a working `.env`.\",\n ].join(\"\\n\");\n}\n","import type {\n NpCollectionConfig,\n NpFieldConfig,\n NpRegisteredTheme,\n NpThemeCollectionKind,\n NpThemeCollectionRequirement,\n NpThemeFieldRequirement,\n} from \"../config/types.js\";\nimport { getScopedLogger } from \"../observability/logger.js\";\n\n/**\n * Auto-merge themes' `manifest.requires.collections` into the\n * operator-authored `collections` array at config-resolution\n * time.\n *\n * Why this exists: the v0.2 theme contract lets themes declare\n * the collection fields their components depend on\n * (`requires.collections.<slug>.fields.<name>`). Pre-#F-track this\n * was wired up exclusively through `pnpm nexpress theme:install`,\n * which AST-patched the operator's `src/collections/*.ts`. That\n * \"code-write\" felt heavyweight for what is, conceptually, a\n * package add — and surfaced bugs (#604, #605, #606) at the\n * intersection of file naming, registration, and missing\n * directories.\n *\n * The framework-side merge replaces it: as soon as the operator\n * adds a theme to `themes: [...]`, `defineConfig` walks each\n * theme's `requires.collections` and:\n *\n * - For each existing collection slug, appends the theme's\n * fields to that collection's `fields` array (only when the\n * operator hasn't already declared a field with the same\n * name — operator wins).\n * - For each slug that doesn't yet exist AND the theme set\n * `createIfAbsent: true`, synthesises a minimal\n * `NpCollectionConfig` and pushes it onto `collections`.\n *\n * The merge is non-destructive: operator-authored fields are\n * never overwritten or reshaped. Theme-injected fields default\n * to `required: false` regardless of `req.required` because they\n * land on existing rows that don't have a value yet — a hard NOT\n * NULL constraint would block the very next migration. Theme\n * authors that NEED a value can validate in their template /\n * read path. Inheriting `hard: false` from the requirement just\n * means \"lower-severity admin warning\"; field-level `required`\n * is the runtime/codegen concern.\n *\n * The next `pnpm db:generate && pnpm db:migrate` picks up the\n * injected columns. From the operator's perspective: add a theme\n * to `themes:`, run the two-command migration, done.\n */\n\n/**\n * Resolved lazily so `setLogger()` calls made AFTER this module\n * is first imported (e.g. tests, host apps that swap to pino at\n * boot) still feed the merge's warnings. The scoped bindings\n * are constant, so re-creating per call is cheap.\n */\nfunction log(): ReturnType<typeof getScopedLogger> {\n return getScopedLogger({ component: \"config:theme-merge\" });\n}\n\n/**\n * Field requirement types we know how to synthesise into a\n * concrete `NpFieldConfig` at merge time. `select` is excluded:\n * `NpThemeFieldRequirement` has no `options` array, and the\n * validation schema (`fieldSchema`) requires `options.min(1)`,\n * so a synthesised select would crash boot. `upload` lands here\n * but requires `relationTo` on the requirement itself; we skip\n * with a warning when it's absent.\n */\nconst SUPPORTED_REQUIREMENT_TYPES = new Set<NpThemeFieldRequirement[\"type\"]>([\n \"text\",\n \"textarea\",\n \"richText\",\n \"number\",\n \"checkbox\",\n \"date\",\n \"upload\",\n \"relationship\",\n \"blocks\",\n]);\n\n/**\n * Translate a single field requirement into a concrete\n * `NpFieldConfig`. Returns null when the requirement can't be\n * sensibly materialised without further input (e.g. `select`\n * without options, `upload` without `relationTo`). The caller\n * logs a warning so the operator sees what was skipped.\n */\n/**\n * Build the `admin` slot for a synthesised field from the theme\n * requirement's `admin` hints. Only forwards keys that the\n * field-base type accepts so a theme-author typo doesn't\n * silently land in the runtime config.\n *\n * Typed as `Record<string, unknown>` because `NpFieldConfig` is\n * a discriminated union with at least one variant (row /\n * collapsible) that lacks the `admin` slot — the union type\n * `NpFieldConfig[\"admin\"]` doesn't resolve. The runtime shape is\n * the same `NpFieldBase.admin` object for every field that\n * accepts it; we're just side-stepping a TypeScript union\n * narrowing limitation.\n */\nfunction buildAdminSlot(\n req: NpThemeFieldRequirement,\n themeId: string,\n): Record<string, unknown> | undefined {\n const out: Record<string, unknown> = { _themeOrigin: themeId };\n if (req.admin?.group !== undefined) out.group = req.admin.group;\n if (req.admin?.condition !== undefined) out.condition = req.admin.condition;\n if (req.admin?.position !== undefined) out.position = req.admin.position;\n // `_themeOrigin` always present — the admin gates fields by it\n // so the bundled-themes prebake doesn't surface Portfolio's\n // sidebar groups when Magazine is the active theme. Tag every\n // theme-contributed field, even ones the operator's config\n // didn't ask for a separate admin slot.\n return out;\n}\n\nfunction requirementToField(\n name: string,\n req: NpThemeFieldRequirement,\n themeId: string,\n): NpFieldConfig | null {\n const admin = buildAdminSlot(req, themeId);\n switch (req.type) {\n case \"text\":\n case \"textarea\":\n case \"richText\":\n case \"number\":\n case \"checkbox\":\n case \"date\":\n case \"blocks\":\n // Plain scalar/blob fields — type + name is enough. We\n // deliberately do NOT forward `req.required: true` onto\n // the field. Theme-injected fields land on existing rows\n // that have no value; making them NOT NULL blocks the\n // very next migration. Theme authors that need a non-null\n // guarantee enforce it in their template/read path.\n return admin ? { type: req.type, name, admin } : { type: req.type, name };\n case \"upload\": {\n if (!req.relationTo || Array.isArray(req.relationTo)) {\n log().warn(\n \"Skipping theme-required upload field: requirement is missing a scalar `relationTo`.\",\n { field: name, relationTo: req.relationTo },\n );\n return null;\n }\n const base = {\n type: \"upload\" as const,\n name,\n relationTo: req.relationTo,\n };\n return admin ? { ...base, admin } : base;\n }\n case \"relationship\": {\n if (!req.relationTo) {\n log().warn(\n \"Skipping theme-required relationship field: requirement is missing `relationTo`.\",\n { field: name },\n );\n return null;\n }\n const baseField = {\n type: \"relationship\" as const,\n name,\n relationTo: req.relationTo,\n };\n const withMany = req.hasMany ? { ...baseField, hasMany: true } : baseField;\n return admin ? { ...withMany, admin } : withMany;\n }\n case \"select\": {\n // A theme can synthesise a select when it provides at least\n // one option on the requirement (universal-content-model\n // Phase U.1 #748). Without options the field schema would\n // reject the synthesised config (`options.min(1)`), so a\n // contribution with no options is still a no-op + warning.\n // Themes that contribute options to an EXISTING select don't\n // come through here — that path unions options inside\n // `mergeThemeRequirements`.\n if (!req.options || req.options.length === 0) {\n log().warn(\n \"Skipping theme-required select field: requirement is missing an `options` list.\",\n { field: name },\n );\n return null;\n }\n const base = {\n type: \"select\" as const,\n name,\n options: [...req.options],\n };\n return admin ? { ...base, admin } : base;\n }\n default: {\n // Exhaustiveness — adding a requirement type forces an\n // update here. Cast through `never` so a missed case is a\n // compile error, not a silent skip.\n const _exhaustive: never = req.type;\n void _exhaustive;\n log().warn(\"Unknown theme field requirement type; skipping.\", {\n field: name,\n type: req.type as unknown as string,\n });\n return null;\n }\n }\n}\n\n/**\n * Universal-content-model Phase U.1 (#748): union two select\n * option lists. Dedupe on `value` (so two themes contributing\n * the same kind don't double up); last-wins on `label` (theme B\n * loaded after theme A can re-label the shared option).\n *\n * Returns the original `base` array unchanged when the\n * contribution is empty or adds no new values — callers use\n * referential equality to decide whether to clone the field.\n */\nfunction mergeSelectOptions(\n base: ReadonlyArray<{ label: string; value: string }>,\n contribution: ReadonlyArray<{ label: string; value: string }>,\n): Array<{ label: string; value: string }> | ReadonlyArray<{ label: string; value: string }> {\n if (contribution.length === 0) return base;\n const byValue = new Map(base.map((o) => [o.value, o]));\n let changed = false;\n for (const opt of contribution) {\n const existing = byValue.get(opt.value);\n if (!existing) {\n byValue.set(opt.value, opt);\n changed = true;\n } else if (existing.label !== opt.label) {\n // Last-wins on label — keep value stable, refresh label.\n byValue.set(opt.value, opt);\n changed = true;\n }\n }\n if (!changed) return base;\n return Array.from(byValue.values());\n}\n\n/**\n * Set of names already declared on a collection. Walks\n * containers (`row`, `collapsible`) like the requirement\n * checker does so a field declared inside a row counts as\n * present. `array` / `group` are NOT walked — theme requirements\n * address top-level fields by name and shouldn't merge into\n * nested record scopes.\n */\nfunction collectExistingFieldNames(fields: NpFieldConfig[]): Set<string> {\n const names = new Set<string>();\n for (const f of fields) {\n if (f.type === \"row\" || f.type === \"collapsible\") {\n for (const name of collectExistingFieldNames(f.fields)) {\n names.add(name);\n }\n continue;\n }\n if (\"name\" in f && typeof f.name === \"string\") {\n names.add(f.name);\n }\n }\n return names;\n}\n\nfunction titleCase(s: string): string {\n return s\n .replace(/[-_]+/g, \" \")\n .replace(/\\b\\w/g, (ch) => ch.toUpperCase());\n}\n\nfunction synthesiseCollection(\n slug: string,\n requirement: NpThemeCollectionRequirement,\n injectedNames: Set<string>,\n themeId: string,\n): NpCollectionConfig | null {\n const fields: NpFieldConfig[] = [];\n for (const [fieldName, fieldReq] of Object.entries(requirement.fields ?? {})) {\n if (!SUPPORTED_REQUIREMENT_TYPES.has(fieldReq.type)) {\n // requirementToField will log the skip — call through so\n // the warning fires from a single place.\n const synth = requirementToField(fieldName, fieldReq, themeId);\n if (synth) fields.push(synth);\n continue;\n }\n const synth = requirementToField(fieldName, fieldReq, themeId);\n if (synth) {\n fields.push(synth);\n injectedNames.add(fieldName);\n }\n }\n if (fields.length === 0) {\n log().warn(\n \"Theme requested createIfAbsent for a collection with no synthesisable fields; skipping.\",\n { slug },\n );\n return null;\n }\n const titled = titleCase(slug);\n const singular = slug.endsWith(\"s\") ? titleCase(slug.slice(0, -1)) : titled;\n // `_themeOrigin` is the admin's signal that this collection\n // only exists because the named theme's `createIfAbsent`\n // synthesised it. The admin sidebar hides such entries when\n // the owning theme isn't the active one — the bundled-themes\n // prebake materialises every built-in's createIfAbsent slug,\n // but only the active theme's deserve sidebar real estate.\n return {\n slug,\n labels: { singular, plural: titled },\n fields,\n admin: { _themeOrigin: themeId },\n };\n}\n\ninterface MergeStats {\n /** Names injected per slug for telemetry / debugging. */\n injected: Map<string, Set<string>>;\n createdSlugs: Set<string>;\n}\n\n/**\n * Walks every theme on `themes` and unions its\n * `manifest.requires.collections` into `collections`. Returns a\n * NEW array (and new nested objects where modified) so the\n * function is safe to feed a frozen / re-used config object.\n *\n * Conflict resolution:\n * - Operator-declared field with the same name → keep the\n * operator's; do NOT overwrite.\n * - Two themes contribute the same field name on the same\n * slug → first theme wins, subsequent themes log a\n * `config:theme-merge` warning and skip.\n * - Theme declares a slug that doesn't exist and DOESN'T set\n * `createIfAbsent` → no-op (admin still surfaces the\n * mismatch via `checkThemeRequirements` so the operator can\n * decide whether to add the collection manually).\n */\nexport function mergeThemeRequirements(\n collections: NpCollectionConfig[],\n themes: NpRegisteredTheme[] | undefined,\n): NpCollectionConfig[] {\n if (!themes || themes.length === 0) return collections;\n\n const stats: MergeStats = {\n injected: new Map(),\n createdSlugs: new Set(),\n };\n // Work on a shallow-copied array; we clone individual\n // collections only when we need to mutate their `fields`.\n const merged: NpCollectionConfig[] = collections.slice();\n const indexBySlug = new Map<string, number>(\n merged.map((c, i) => [c.slug, i]),\n );\n // Track which fields already exist (operator-declared OR\n // earlier-theme-injected) per slug. Initialise with the\n // operator's view so we never overwrite a user-authored field.\n const existingFieldsBySlug = new Map<string, Set<string>>(\n merged.map((c) => [c.slug, collectExistingFieldNames(c.fields)]),\n );\n\n for (const theme of themes) {\n const requires = theme.manifest.requires?.collections;\n if (!requires) continue;\n for (const [slug, req] of Object.entries(requires)) {\n const existingIndex = indexBySlug.get(slug);\n if (existingIndex === undefined) {\n if (!req.createIfAbsent) continue;\n const injectedNames = new Set<string>();\n const synth = synthesiseCollection(slug, req, injectedNames, theme.manifest.id);\n if (!synth) continue;\n merged.push(synth);\n indexBySlug.set(slug, merged.length - 1);\n existingFieldsBySlug.set(slug, injectedNames);\n stats.createdSlugs.add(slug);\n stats.injected.set(slug, injectedNames);\n continue;\n }\n\n // Existing collection — append the theme's fields that\n // aren't already present (or, for select fields, union the\n // options into the existing select), and stamp the kinds\n // metadata. A theme that contributes only `kinds` (no\n // `fields`) still gets the merge — the early skip on\n // missing fields would have dropped it pre-#750.\n const reqFields = req.fields;\n const reqKinds = req.kinds;\n if (!reqFields && !reqKinds) continue;\n\n const alreadyDeclared = existingFieldsBySlug.get(slug) ?? new Set<string>();\n const target = merged[existingIndex];\n if (!target) continue; // defensive; the index was just looked up\n let nextFields: NpFieldConfig[] = target.fields;\n let fieldsCloned = false;\n const ensureCloned = (): NpFieldConfig[] => {\n if (!fieldsCloned) {\n nextFields = [...nextFields];\n fieldsCloned = true;\n }\n return nextFields;\n };\n\n for (const [fieldName, fieldReq] of Object.entries(reqFields ?? {})) {\n if (alreadyDeclared.has(fieldName)) {\n // Select-options union (universal-content-model Phase U.1).\n // When both sides are `select`, the requirement's\n // `options` add to the existing field's options instead\n // of being skipped. Two themes contributing disjoint\n // option sets (e.g. `kind=doc` + `kind=project`) is\n // exactly the case this enables.\n if (fieldReq.type === \"select\" && fieldReq.options && fieldReq.options.length > 0) {\n const idx = nextFields.findIndex(\n (f) => \"name\" in f && f.name === fieldName,\n );\n const existing = idx >= 0 ? nextFields[idx] : undefined;\n if (existing && existing.type === \"select\") {\n const merged = mergeSelectOptions(existing.options, fieldReq.options);\n if (merged !== existing.options) {\n const list = ensureCloned();\n // Spread copy ensures the result is a fresh mutable\n // array even when `mergeSelectOptions` returned the\n // input untouched in a code path the caller doesn't\n // see; cheap, removes the readonly union in the type.\n list[idx] = { ...existing, options: [...merged] };\n }\n continue;\n }\n // Falls through to the same-name warning path when the\n // existing field isn't a select — a select requirement\n // can't union into a text / number / etc.\n }\n\n // If this name was injected by an earlier theme on\n // this same merge pass, surface that explicitly —\n // operators reading the log can tell \"operator wins\"\n // (silent) apart from \"theme A wins over theme B\"\n // (warned). Operator-declared field collisions are\n // expected and unsurprising; theme-vs-theme collisions\n // are usually a misconfiguration.\n const injectedHere = stats.injected.get(slug);\n if (injectedHere?.has(fieldName)) {\n log().warn(\n \"Two themes contribute the same field on the same collection; keeping the first.\",\n { slug, field: fieldName, theme: theme.manifest.id },\n );\n }\n continue;\n }\n const synth = requirementToField(fieldName, fieldReq, theme.manifest.id);\n if (!synth) continue;\n ensureCloned().push(synth);\n alreadyDeclared.add(fieldName);\n let injectedHere = stats.injected.get(slug);\n if (!injectedHere) {\n injectedHere = new Set<string>();\n stats.injected.set(slug, injectedHere);\n }\n injectedHere.add(fieldName);\n }\n\n // Kinds metadata (universal-content-model #748). Themes\n // contribute one entry per kind they author; the merge\n // unions across themes and stamps the result onto\n // `target.admin.kinds`. Last-write-wins on per-kind props\n // (label, icon, urlPattern, …) — two themes claiming the\n // same kind value is unusual and the second theme's\n // description wins.\n //\n // Each merged kind carries a `_themeOrigin` tag so the\n // admin sidebar can hide kinds whose contributing theme\n // isn't active. Without this, the bundled-themes prebake\n // would surface every built-in theme's kinds on every\n // operator's sidebar — an operator on `default` would see\n // a \"Documentation\" entry contributed by `theme-docs`\n // (#754 follow-up).\n let nextAdmin = target.admin;\n let adminCloned = false;\n if (reqKinds && Object.keys(reqKinds).length > 0) {\n const existingKinds = target.admin?.kinds ?? {};\n const mergedKinds = { ...existingKinds };\n for (const [kindValue, kindMeta] of Object.entries(reqKinds)) {\n mergedKinds[kindValue] = {\n ...(mergedKinds[kindValue] ?? {}),\n ...kindMeta,\n _themeOrigin: theme.manifest.id,\n };\n }\n nextAdmin = { ...(target.admin ?? {}), kinds: mergedKinds };\n adminCloned = true;\n }\n\n // Group metadata (#8 of the editor track). Themes contribute\n // icon / description per sidebar group label. Merged\n // last-wins on per-key props. Unlike `kinds`, groupMeta has\n // no `_themeOrigin` gate — the metadata is purely\n // presentational, and showing an icon contributed by an\n // inactive theme is harmless (the group itself only renders\n // when an active field declares `admin.group: <name>`).\n const reqGroupMeta = req.groupMeta;\n if (reqGroupMeta && Object.keys(reqGroupMeta).length > 0) {\n const existingMeta = nextAdmin?.groupMeta ?? target.admin?.groupMeta ?? {};\n const mergedMeta = { ...existingMeta };\n for (const [groupName, meta] of Object.entries(reqGroupMeta)) {\n mergedMeta[groupName] = {\n ...(mergedMeta[groupName] ?? {}),\n ...meta,\n };\n }\n nextAdmin = { ...(nextAdmin ?? target.admin ?? {}), groupMeta: mergedMeta };\n adminCloned = true;\n }\n\n if (!fieldsCloned && !adminCloned) continue;\n\n // Clone the collection record + its fields / admin so we\n // don't mutate the operator's defineCollection() output.\n // Mutating the caller's array would surprise consumers that\n // re-use collection objects (tests, multi-site sandboxes).\n merged[existingIndex] = {\n ...target,\n ...(fieldsCloned ? { fields: nextFields } : {}),\n ...(adminCloned ? { admin: nextAdmin } : {}),\n };\n existingFieldsBySlug.set(slug, alreadyDeclared);\n }\n }\n\n return merged;\n}\n\n/**\n * Type re-export — `NpThemeCollectionKind` is imported as a value\n * here only so consumers of `mergeThemeRequirements` don't have to\n * pull the same type from `../config/types.js` separately.\n */\nexport type { NpThemeCollectionKind };\n","import { z } from \"zod\";\n\nconst functionSchema = z.custom<(...args: unknown[]) => unknown>(\n (value) => typeof value === \"function\",\n);\n\n/**\n * Serializable condition predicate (NpFieldConditionExpr) — the\n * Zod mirror of the type defined in `config/types.ts`. Permissive\n * (no exhaustive variant validation) because adding new operators\n * to the type union shouldn't force a schema bump. `z.unknown()`\n * on the value slots tolerates the wide payload shapes the editor\n * uses; the runtime evaluator (`evaluateFieldCondition`)\n * fail-opens on malformed expressions.\n */\nconst conditionExprSchema: z.ZodType = z.lazy(() =>\n z.union([\n z.object({ when: z.string().min(1), equals: z.unknown() }),\n z.object({ when: z.string().min(1), notEquals: z.unknown() }),\n z.object({ when: z.string().min(1), in: z.array(z.unknown()) }),\n z.object({ when: z.string().min(1), notIn: z.array(z.unknown()) }),\n z.object({ when: z.string().min(1), exists: z.boolean() }),\n z.object({ all: z.array(conditionExprSchema) }),\n z.object({ any: z.array(conditionExprSchema) }),\n ]),\n);\n\nconst fieldBaseSchema = z.object({\n name: z.string().min(1),\n label: z.string().min(1).optional(),\n required: z.boolean().optional(),\n defaultValue: z.unknown().optional(),\n hidden: z.boolean().optional(),\n admin: z\n .object({\n description: z.string().min(1).optional(),\n placeholder: z.string().optional(),\n readOnly: z.boolean().optional(),\n // Accepts either the legacy function form (server-only, stripped\n // at the RSC boundary) or the serializable expression form\n // (#764). The runtime evaluator handles both.\n condition: z.union([functionSchema, conditionExprSchema]).optional(),\n width: z.string().optional(),\n })\n .optional(),\n validate: functionSchema.optional(),\n});\n\nconst optionSchema = z.object({\n label: z.string().min(1),\n value: z.string().min(1),\n});\n\nconst fieldSchema: z.ZodType = z.lazy(() =>\n z.discriminatedUnion(\"type\", [\n fieldBaseSchema.extend({\n type: z.literal(\"text\"),\n minLength: z.number().int().nonnegative().optional(),\n maxLength: z.number().int().nonnegative().optional(),\n unique: z.boolean().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"textarea\"),\n minLength: z.number().int().nonnegative().optional(),\n maxLength: z.number().int().nonnegative().optional(),\n rows: z.number().int().positive().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"number\"),\n min: z.number().optional(),\n max: z.number().optional(),\n step: z.number().positive().optional(),\n integerOnly: z.boolean().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"richText\"),\n editor: z.object({ features: z.array(z.string()).optional() }).optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"blocks\"),\n allowedBlocks: z.array(z.string().min(1)).optional(),\n minRows: z.number().int().nonnegative().optional(),\n maxRows: z.number().int().nonnegative().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"checkbox\"),\n defaultValue: z.boolean().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"date\"),\n pickerOptions: z\n .object({\n format: z.string().optional(),\n includeTime: z.boolean().optional(),\n })\n .optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"upload\"),\n relationTo: z.string().min(1),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"relationship\"),\n relationTo: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]),\n hasMany: z.boolean().optional(),\n filterOptions: z.record(z.string(), z.unknown()).optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"select\"),\n options: z.array(optionSchema).min(1),\n hasMany: z.boolean().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"radio\"),\n options: z.array(optionSchema).min(1),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"email\"),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"json\"),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"array\"),\n fields: z.array(fieldSchema).min(1),\n minRows: z.number().int().nonnegative().optional(),\n maxRows: z.number().int().nonnegative().optional(),\n }),\n fieldBaseSchema.extend({\n type: z.literal(\"group\"),\n fields: z.array(fieldSchema).min(1),\n }),\n z.object({\n type: z.literal(\"row\"),\n fields: z.array(fieldSchema).min(1),\n }),\n z.object({\n type: z.literal(\"collapsible\"),\n label: z.string().min(1),\n fields: z.array(fieldSchema).min(1),\n }),\n ]),\n);\n\nconst imageSizeSchema = z.object({\n name: z.string().min(1),\n width: z.number().positive(),\n height: z.number().positive().optional(),\n crop: z.enum([\"center\", \"top\", \"bottom\", \"left\", \"right\"]).optional(),\n});\n\n// Discriminated union ties `adapter` to its required backend block —\n// previously both `local` and `s3` were optional regardless of the\n// adapter choice, so a config with `{ adapter: \"s3\" }` and no `s3`\n// block passed validation and only blew up at runtime when the storage\n// factory tried to read the missing block. (#64)\nconst storageSchema = z.discriminatedUnion(\"adapter\", [\n z.object({\n adapter: z.literal(\"local\"),\n local: z.object({\n directory: z.string().min(1),\n baseUrl: z.string().min(1),\n }),\n }),\n z.object({\n adapter: z.literal(\"s3\"),\n s3: z.object({\n bucket: z.string().min(1),\n region: z.string().min(1),\n endpoint: z.string().url().optional(),\n }),\n }),\n]);\n\n// Plugins are a mix of legacy NpPluginConfig (object with optional init fn)\n// and SDK-built NpResolvedPluginLike (object with manifest). Parse with\n// `z.unknown()` — deeper validation happens when loadPlugins() runs.\nconst pluginEntrySchema = z.unknown();\n\nexport const npConfigSchema = z.object({\n site: z.object({\n name: z.string().min(1),\n url: z.string().url(),\n }),\n db: z.object({\n connectionString: z.string(),\n pool: z\n .object({\n max: z.number().int().positive().optional(),\n })\n .optional(),\n }),\n storage: storageSchema.optional(),\n collections: z.array(z.lazy((): z.ZodType => collectionConfigSchema)),\n blocks: z.array(z.unknown()).optional(),\n editor: z\n .object({\n features: z.array(z.string().min(1)).optional(),\n })\n .optional(),\n images: z\n .object({\n sizes: z.array(imageSizeSchema).optional(),\n format: z.enum([\"webp\", \"avif\", \"jpeg\", \"png\"]).optional(),\n quality: z.number().int().min(1).max(100).optional(),\n })\n .optional(),\n auth: z\n .object({\n secret: z.string().min(1),\n tokenExpiration: z.number().int().positive().optional(),\n refreshTokenExpiration: z.number().int().positive().optional(),\n maxLoginAttempts: z.number().int().positive().optional(),\n lockoutDuration: z.number().int().positive().optional(),\n })\n .optional(),\n plugins: z.array(pluginEntrySchema).optional(),\n typescript: z.unknown().optional(),\n themes: z.array(z.unknown()).optional(),\n i18n: z\n .object({\n locales: z.array(z.string().min(1).max(35)).min(1),\n defaultLocale: z.string().min(1),\n })\n .refine((val) => val.locales.includes(val.defaultLocale), {\n message: \"defaultLocale must be one of the declared locales\",\n path: [\"defaultLocale\"],\n })\n .optional(),\n});\n\nexport const collectionConfigSchema = z.object({\n slug: z.string().min(1).max(63).regex(/^[a-z][a-z0-9-]*$/),\n labels: z.object({\n singular: z.string().min(1),\n plural: z.string().min(1),\n }),\n slugField: z\n .union([\n z.boolean(),\n z.object({\n useField: z.string().min(1).optional(),\n unique: z.boolean().optional(),\n }),\n ])\n .optional(),\n i18n: z.boolean().optional(),\n fields: z.array(fieldSchema).min(1),\n access: z\n .object({\n create: functionSchema.optional(),\n read: functionSchema.optional(),\n update: functionSchema.optional(),\n delete: functionSchema.optional(),\n })\n .optional(),\n hooks: z\n .object({\n beforeCreate: z.array(functionSchema).optional(),\n afterCreate: z.array(functionSchema).optional(),\n beforeUpdate: z.array(functionSchema).optional(),\n afterUpdate: z.array(functionSchema).optional(),\n beforeDelete: z.array(functionSchema).optional(),\n afterDelete: z.array(functionSchema).optional(),\n beforeRead: z.array(functionSchema).optional(),\n afterRead: z.array(functionSchema).optional(),\n })\n .optional(),\n versions: z\n .object({\n drafts: z\n .union([\n z.boolean(),\n z.object({\n autosave: z.boolean().optional(),\n autosaveInterval: z.number().int().positive().optional(),\n }),\n ])\n .optional(),\n max: z.number().int().positive().optional(),\n })\n .optional(),\n community: z\n .object({\n comments: z.boolean().optional(),\n memberWrite: z\n .object({\n create: z.boolean().optional(),\n update: z.boolean().optional(),\n delete: z.boolean().optional(),\n defaultStatus: z.enum([\"published\", \"pending\"]).optional(),\n })\n .optional(),\n })\n .optional(),\n timestamps: z.boolean().optional(),\n admin: z\n .object({\n listColumns: z.array(z.string().min(1)).optional(),\n defaultSort: z.string().min(1).optional(),\n group: z.string().min(1).optional(),\n hidden: z.boolean().optional(),\n description: z.string().optional(),\n components: z\n .object({\n listView: z.string().optional(),\n editView: z.string().optional(),\n createView: z.string().optional(),\n })\n .optional(),\n })\n .optional(),\n upload: z\n .object({\n maxFileSize: z.number().positive().optional(),\n allowedMimeTypes: z.array(z.string().min(1)).optional(),\n imageSizes: z\n .array(\n z.object({\n name: z.string().min(1),\n width: z.number().positive(),\n height: z.number().positive().optional(),\n crop: z.enum([\"center\", \"top\", \"bottom\", \"left\", \"right\"]).optional(),\n }),\n )\n .optional(),\n })\n .optional(),\n});\n","import { type NpCollectionConfig } from \"./types.js\";\n\nexport function defineCollection(config: NpCollectionConfig): NpCollectionConfig {\n return config;\n}\n","import { and, desc, eq } from \"drizzle-orm\";\n\nimport { npSettings } from \"../db/schema/system.js\";\nimport { npNavigation, npSlugHistory } from \"../db/schema/system.js\";\nimport type { NpThemeTokens, NpThemeTokensOverlay } from \"../theme/types.js\";\nimport type { NpNavItem, NpFindOptions, NpFindResult, NpAuthUser } from \"../config/types.js\";\nimport { DEFAULT_THEME } from \"../theme/defaults.js\";\nimport { findDocuments, getCollectionConfig, getDb } from \"../collections/index.js\";\nimport { getCurrentSiteId } from \"../sites/context.js\";\nimport { NP_DEFAULT_SITE_ID } from \"../sites/registry.js\";\nimport { getActiveTheme } from \"../themes/registry.js\";\n\n/**\n * Phase 15.4 — every settings/navigation read scopes by the\n * current site id so each tenant gets its own theme tokens,\n * navigation menus, and arbitrary settings. The resolver\n * falls back to the default site when no request context is\n * set (background workers, scripts, tests with no resolver\n * wired) so existing single-tenant code keeps working\n * unchanged.\n */\nasync function resolveSiteId(): Promise<string> {\n return (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n}\n\n/**\n * Resolves the effective theme tokens for the current site by\n * layering three sources, last-writer-wins:\n *\n * 1. `DEFAULT_THEME` — framework baseline (always full).\n * 2. The active theme's `impl.tokens` — author-shipped defaults\n * that distinguish a theme from the framework baseline (e.g.\n * magazine's warm cream palette, portfolio's dark surface).\n * 3. The DB row in `np_settings.theme` — admin overrides via the\n * theme settings tab.\n *\n * Each layer is `NpThemeTokensOverlay` (sub-tree-Partial) — themes\n * only declare the keys they care about, admins only save deltas\n * they edit. The merge ensures every emitted token has a value.\n *\n * This is the canonical token resolver — `apps/web`'s preview\n * route calls it directly so the page-builder iframe matches what\n * the public render produces for the same active theme.\n */\nexport async function getTheme(): Promise<NpThemeTokens> {\n const db = getDb();\n const siteId = await resolveSiteId();\n const rows = await db\n .select()\n .from(npSettings)\n .where(and(eq(npSettings.siteId, siteId), eq(npSettings.key, \"theme\")))\n .limit(1);\n\n // `impl` is opaque to core (declared as `unknown` so React types\n // don't leak into this package). Cast narrowly to read the only\n // field we need; everything else stays opaque.\n const active = await getActiveTheme();\n const themeOverlay = (\n active?.impl as { tokens?: NpThemeTokensOverlay } | null | undefined\n )?.tokens;\n // Admin's theme settings tab historically saved a full\n // `NpThemeTokens` shape, but accept partial overlays too — same\n // sub-tree-aware merge applies, so a future admin UI that only\n // edits a handful of fields doesn't have to round-trip the entire\n // token tree on every save.\n const dbOverlay = rows[0]?.value as NpThemeTokensOverlay | undefined;\n\n if (!themeOverlay && !dbOverlay) return DEFAULT_THEME;\n return mergeThemeTokens(DEFAULT_THEME, themeOverlay, dbOverlay);\n}\n\n/**\n * Layered token merge. Each subsequent overlay wins on key\n * collision. Sub-objects merge field-by-field so a theme that only\n * sets `colors.primary` doesn't blow away the rest of the colors\n * sub-tree.\n */\nfunction mergeThemeTokens(\n base: NpThemeTokens,\n ...overlays: Array<NpThemeTokensOverlay | undefined>\n): NpThemeTokens {\n const result: NpThemeTokens = {\n colors: { ...base.colors },\n typography: { ...base.typography },\n shape: { ...base.shape },\n };\n for (const overlay of overlays) {\n if (!overlay) continue;\n if (overlay.colors) Object.assign(result.colors, overlay.colors);\n if (overlay.typography) Object.assign(result.typography, overlay.typography);\n if (overlay.shape) Object.assign(result.shape, overlay.shape);\n }\n return result;\n}\n\nexport async function getNavigation(\n location: string = \"header\",\n): Promise<NpNavItem[]> {\n const db = getDb();\n const siteId = await resolveSiteId();\n const rows = await db\n .select()\n .from(npNavigation)\n .where(\n and(eq(npNavigation.siteId, siteId), eq(npNavigation.location, location)),\n )\n .limit(1);\n\n if (rows.length === 0 || !rows[0]) {\n return [];\n }\n\n return resolveNavItemUrls(rows[0].items);\n}\n\n/**\n * Replaces `url` on dynamic nav items with values derived from\n * the underlying record:\n *\n * - `type: \"page\"` + `pageId` (+ optional `collectionSlug`,\n * defaults to `\"pages\"`) → the URL the source collection's\n * `seo.urlPath` produces from the linked doc. Lets the page-\n * edit \"In navigation\" panel (#436) work for any page-shaped\n * collection, not just `pages`.\n * - `type: \"collection\"` + `collection` → the conventional\n * collection-list URL (`/{collection-slug}`). The convention\n * is editor-side only; themes that route a collection\n * elsewhere (e.g. /blog for posts) should keep using\n * `type: \"link\"` with an explicit URL until a per-collection\n * route helper lands.\n *\n * Themes still render `<a href={item.url}>` and need the resolved\n * URL handed to them. Items whose underlying record disappeared\n * (doc unpublished, collection unregistered, no `seo.urlPath` on\n * the collection) fall through to `#` so the rendered output stays\n * stable across status flips — dropping the item would invalidate\n * the cache shape every time.\n */\nasync function resolveNavItemUrls(items: NpNavItem[]): Promise<NpNavItem[]> {\n // Group page-typed refs by source collection so we issue one\n // batch of lookups per collection. Items missing `collectionSlug`\n // default to `\"pages\"` so existing nav rows keep resolving\n // unchanged — that's the v1 wire format.\n const refsByCollection = collectPageRefs(items);\n\n // Map keyed by `${collection}\\0${docId}` so doc ids don't collide\n // across collections (different collections can technically share\n // the same uuid namespace).\n const docByKey = new Map<string, Record<string, unknown>>();\n\n await Promise.all(\n [...refsByCollection.entries()].map(async ([collection, ids]) => {\n try {\n await Promise.all(\n ids.map(async (id) => {\n const result = await findDocuments(collection, {\n where: { id, status: \"published\" },\n limit: 1,\n });\n const doc = result.docs[0];\n if (doc) docByKey.set(`${collection}\\0${id}`, doc);\n }),\n );\n } catch {\n // Collection isn't registered (was renamed, removed, or\n // never existed). Items pointing at it just fall through\n // to \"#\" — same fate as items pointing at unpublished docs.\n }\n }),\n );\n\n return items.map((item) => mapNavItem(item, docByKey));\n}\n\nfunction collectPageRefs(items: NpNavItem[]): Map<string, string[]> {\n const out = new Map<string, string[]>();\n const walk = (arr: NpNavItem[]): void => {\n for (const item of arr) {\n if (item.type === \"page\" && item.pageId) {\n const slug = item.collectionSlug ?? \"pages\";\n const ids = out.get(slug) ?? [];\n ids.push(item.pageId);\n out.set(slug, ids);\n }\n if (item.children) walk(item.children);\n }\n };\n walk(items);\n return out;\n}\n\nfunction mapNavItem(\n item: NpNavItem,\n docByKey: Map<string, Record<string, unknown>>,\n): NpNavItem {\n const children = item.children\n ? item.children.map((child) => mapNavItem(child, docByKey))\n : undefined;\n const withChildren = children ? { ...item, children } : item;\n\n if (item.type === \"page\" && item.pageId) {\n const collection = item.collectionSlug ?? \"pages\";\n const doc = docByKey.get(`${collection}\\0${item.pageId}`);\n let url = \"#\";\n if (doc) {\n try {\n const config = getCollectionConfig(collection);\n const path = config.seo?.urlPath?.(doc);\n if (path) url = path;\n } catch {\n // Collection un-registered between fetch and lookup — keep\n // the \"#\" fallback so rendering doesn't blow up.\n }\n }\n return { ...withChildren, url };\n }\n\n if (item.type === \"collection\" && item.collection) {\n const slug = item.collection.replace(/^\\/+/, \"\");\n const url = slug ? `/${slug}` : \"#\";\n return { ...withChildren, url };\n }\n\n return withChildren;\n}\n\nexport async function getPageBySlug(\n slug: string,\n options?: { draft?: boolean; locale?: string },\n): Promise<Record<string, unknown> | null> {\n const where: Record<string, unknown> = { slug: slug || \"/\" };\n\n if (!options?.draft) {\n where.status = \"published\";\n }\n\n // Locale-scoped lookup — when the caller supplies a locale, the\n // findDocuments query restricts to rows in that locale (matches\n // the `(site_id, locale, slug)` unique index on i18n collections).\n // Single-locale collections ignore the option, so callers can\n // pass it unconditionally.\n const result = await findDocuments(\"pages\", {\n where,\n locale: options?.locale,\n limit: 1,\n });\n\n return result.docs[0] ?? null;\n}\n\nexport async function getPostBySlug(\n slug: string,\n options?: { draft?: boolean },\n): Promise<Record<string, unknown> | null> {\n const where: Record<string, unknown> = { slug };\n\n if (!options?.draft) {\n where.status = \"published\";\n }\n\n const result = await findDocuments(\"posts\", {\n where,\n limit: 1,\n });\n\n return result.docs[0] ?? null;\n}\n\nexport async function findPosts(\n options: NpFindOptions,\n user?: NpAuthUser,\n): Promise<NpFindResult> {\n return findDocuments(\"posts\", options, user);\n}\n\nexport async function getAllPageSlugs(): Promise<string[]> {\n const result = await findDocuments(\"pages\", {\n limit: 10000,\n });\n\n return result.docs\n .map((doc) => doc.slug as string)\n .filter(Boolean);\n}\n\n/**\n * When a slug-having collection's row gets renamed (`/old-page` →\n * `/new-page`), the public-site catch-all should 301 the old URL\n * to the new one instead of returning 404. This helper walks the\n * `np_slug_history` chain for the given collection + slug and\n * returns the most recent target.\n *\n * Chain example: A → B → C (renamed twice). Looking up A walks\n * `A → B → C` and returns C. Capped at 5 hops to bound work and\n * defend against pathological cycles (shouldn't happen but cheap\n * to enforce). Returns null when no redirect target exists or\n * the chain ends in the input slug itself.\n */\nconst SLUG_REDIRECT_MAX_HOPS = 5;\n\nexport async function findSlugRedirect(\n collection: string,\n oldSlug: string,\n): Promise<string | null> {\n if (!oldSlug || oldSlug.length === 0) return null;\n const db = getDb();\n const siteId = await resolveSiteId();\n\n const seen = new Set<string>([oldSlug]);\n let currentOld = oldSlug;\n let resolved: string | null = null;\n for (let hop = 0; hop < SLUG_REDIRECT_MAX_HOPS; hop++) {\n // Take the most recently written row for this `(site, collection,\n // oldSlug)` triple — a slug can be reused over time (a doc renamed\n // away from \"X\" later, another doc renamed *to* \"X\", then \"X\" gets\n // renamed again). The newest record is the operator's intent.\n const [latest] = await db\n .select()\n .from(npSlugHistory)\n .where(\n and(\n eq(npSlugHistory.siteId, siteId),\n eq(npSlugHistory.collection, collection),\n eq(npSlugHistory.oldSlug, currentOld),\n ),\n )\n .orderBy(desc(npSlugHistory.createdAt))\n .limit(1);\n if (!latest) break;\n const next = latest.newSlug;\n if (next === oldSlug || seen.has(next)) {\n // Cycle (A→B→A) — surface as \"no redirect\". Defensive; the\n // pipeline only writes new history rows on actual changes,\n // so this is unreachable in normal operation.\n break;\n }\n resolved = next;\n seen.add(next);\n currentOld = next;\n }\n return resolved;\n}\n\nexport async function getSetting<T = unknown>(key: string): Promise<T | null> {\n const db = getDb();\n const siteId = await resolveSiteId();\n const rows = await db\n .select()\n .from(npSettings)\n .where(and(eq(npSettings.siteId, siteId), eq(npSettings.key, key)))\n .limit(1);\n\n if (rows.length === 0 || !rows[0]) {\n return null;\n }\n\n return rows[0].value as T;\n}\n","import type { NpThemeTokens } from \"./types.js\";\n\nexport const DEFAULT_THEME: NpThemeTokens = {\n colors: {\n primary: \"oklch(0.546 0.245 262.881)\",\n primaryForeground: \"oklch(0.985 0.001 106.423)\",\n background: \"oklch(1 0 0)\",\n foreground: \"oklch(0.145 0.004 285.823)\",\n muted: \"oklch(0.967 0.001 286.375)\",\n mutedForeground: \"oklch(0.556 0.007 286.618)\",\n border: \"oklch(0.922 0.004 286.32)\",\n card: \"oklch(1 0 0)\",\n cardForeground: \"oklch(0.145 0.004 285.823)\",\n accent: \"oklch(0.967 0.001 286.375)\",\n accentForeground: \"oklch(0.145 0.004 285.823)\",\n destructive: \"oklch(0.577 0.245 27.325)\",\n destructiveForeground: \"oklch(0.985 0.001 106.423)\",\n },\n typography: {\n fontHeading: \"system-ui, -apple-system, sans-serif\",\n fontBody: \"system-ui, -apple-system, sans-serif\",\n fontMono: \"ui-monospace, 'Cascadia Code', monospace\",\n fontSizeBase: \"1rem\",\n lineHeight: \"1.625\",\n fontSizeSm: \"0.875rem\",\n fontSizeLg: \"1.125rem\",\n fontSizeXl: \"1.25rem\",\n fontSize2xl: \"1.5rem\",\n fontSize3xl: \"1.875rem\",\n fontSize4xl: \"2.25rem\",\n },\n shape: {\n radiusSm: \"0.25rem\",\n radiusMd: \"0.375rem\",\n radiusLg: \"0.5rem\",\n radiusFull: \"9999px\",\n shadowSm: \"0 1px 2px 0 rgb(0 0 0 / 0.05)\",\n shadowMd: \"0 4px 6px -1px rgb(0 0 0 / 0.1)\",\n shadowLg: \"0 10px 15px -3px rgb(0 0 0 / 0.1)\",\n },\n};\n","import { and, eq } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npSettings } from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\nimport { addStrings } from \"../i18n/strings.js\";\nimport { getCurrentSiteId } from \"../sites/context.js\";\nimport { NP_DEFAULT_SITE_ID as DEFAULT_SITE } from \"../sites/registry.js\";\nimport type { NpRegisteredTheme } from \"../config/types.js\";\n\n/**\n * Phase 11.1 — theme registry. Sites declare an array of themes\n * in `nexpress.config.ts`; the framework registers them once at\n * boot. The active theme id lives in `np_settings.activeTheme`,\n * so admins can switch between installed themes via the admin UI\n * without a redeploy. New theme INSTALLATION still requires a\n * rebuild (Next.js bundles the components) — same constraint\n * WordPress has with file uploads on the server.\n *\n * The registry is a process-level Map; same lifetime as the\n * collection / plugin registries. Re-registration overwrites\n * by `manifest.id` so a hot-reload during dev doesn't accumulate\n * stale entries.\n */\nconst registry = new Map<string, NpRegisteredTheme>();\n\n/**\n * Idempotent — call once at boot from the framework's\n * bootstrap, again from a hot-reload, etc. Themes are matched\n * by `manifest.id`; later registrations replace earlier ones.\n */\nexport function registerThemes(themes: NpRegisteredTheme[]): void {\n for (const theme of themes) {\n if (!theme?.manifest?.id) {\n throw new Error(\"Theme is missing manifest.id\");\n }\n registry.set(theme.manifest.id, theme);\n\n // Phase 12.5 — themes can ship UI-string bundles via\n // `impl.i18n: { locale: { key: value } }`. Merging happens\n // here (alongside theme registration) rather than in the\n // bootstrap so live theme swaps in dev pick up updated\n // strings without a restart.\n const impl = theme.impl as { i18n?: Record<string, Record<string, string>> };\n if (impl?.i18n && typeof impl.i18n === \"object\") {\n for (const [locale, bundle] of Object.entries(impl.i18n)) {\n if (bundle && typeof bundle === \"object\") {\n addStrings(locale, bundle);\n }\n }\n }\n }\n}\n\nexport function getRegisteredThemes(): NpRegisteredTheme[] {\n return Array.from(registry.values());\n}\n\nexport function getThemeById(id: string): NpRegisteredTheme | undefined {\n return registry.get(id);\n}\n\n/** Tests use this between cases; production callers should never need it. */\nexport function resetThemes(): void {\n registry.clear();\n}\n\n/**\n * Reads the persisted active-theme id from `np_settings` for\n * the current site. Returns `null` when no row exists —\n * caller's job to decide the fallback (typically the first\n * registered theme).\n *\n * Phase 15.4 — scoped by current site. Single-tenant\n * deployments leave every row at `site_id = 'default'`, so\n * the lookup behaves identically to the pre-15.4 global\n * version.\n */\nexport async function getActiveThemeId(): Promise<string | null> {\n const db = getDb();\n const siteId = (await getCurrentSiteId()) ?? DEFAULT_SITE;\n const rows = (await db\n .select()\n .from(npSettings)\n .where(and(eq(npSettings.siteId, siteId), eq(npSettings.key, \"activeTheme\")))\n .limit(1)) as Array<{ value: unknown }>;\n const row = rows[0];\n if (!row) return null;\n return typeof row.value === \"string\" ? row.value : null;\n}\n\n/**\n * Resolves the active theme to render. Looks up the persisted id\n * in the registry; falls back to the first registered theme when\n * the id is unset, missing, or points at a theme that's no\n * longer in the registry (e.g. it was removed from\n * `nexpress.config.ts` between deploys). Returns `null` only\n * when the registry is completely empty.\n */\nexport async function getActiveTheme(): Promise<NpRegisteredTheme | null> {\n const id = await getActiveThemeId();\n if (id) {\n const theme = registry.get(id);\n if (theme) return theme;\n }\n // Registry preserves insertion order; the first registered\n // theme is the implicit default.\n const first = registry.values().next();\n return first.done ? null : first.value;\n}\n\n/**\n * Persist the active theme. Validates the id is registered so\n * an admin can't pick a string that doesn't resolve to anything\n * (which would silently fall back to the default and confuse\n * the operator).\n *\n * Accepts an optional outer transaction so the active-theme flip\n * can sit inside the same atomic scope as a wipe + seed batch\n * (see `wipeSeededContent` / `seedAll`'s `tx` option). Without\n * an outer tx, the write runs against the pool handle and\n * commits standalone.\n */\nexport async function setActiveThemeId(\n id: string,\n updatedBy: string | null = null,\n options: { tx?: unknown } = {},\n): Promise<void> {\n if (!registry.has(id)) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"themeId\",\n message: `Unknown theme '${id}'. Register it in nexpress.config.ts first.`,\n },\n ]);\n }\n // `options.tx` is typed `unknown` here to avoid pulling Drizzle\n // internals onto the public registry surface. Callers thread\n // the NpTransaction value they received from\n // `db.transaction(async (tx) => …)`; structurally it has the\n // same `.insert(table)` chain we rely on.\n const dbHandle = (options.tx ?? getDb()) as ReturnType<typeof getDb>;\n const now = new Date();\n const siteId = (await getCurrentSiteId()) ?? DEFAULT_SITE;\n // Phase 15.4 — composite (site_id, key) PK.\n await dbHandle\n .insert(npSettings)\n .values({ siteId, key: \"activeTheme\", value: id, updatedAt: now, updatedBy })\n .onConflictDoUpdate({\n target: [npSettings.siteId, npSettings.key],\n set: { value: id, updatedAt: now, updatedBy },\n });\n}\n\n/**\n * Phase 11.3 — surface the active theme's templates for a\n * given collection so admin pickers and the catch-all renderer\n * can introspect what's available without reaching into the\n * opaque `impl` themselves. The result is sanitized for serial-\n * ization (no React component refs leak through this path) so\n * the same shape is safe to send over the API to the admin UI.\n */\nexport interface NpThemeTemplateSummary {\n id: string;\n label: string;\n description?: string;\n}\n\nexport async function getThemeTemplateSummaries(\n collectionSlug: string,\n): Promise<NpThemeTemplateSummary[]> {\n const summaries = new Map<string, NpThemeTemplateSummary>();\n\n // Phase 14.5 — start with plugin-contributed templates so\n // theme entries naturally overwrite them on id collision.\n // Lazy import keeps the registry → plugin coupling one-way\n // (plugins know about themes' template shape; themes don't\n // depend on plugins at type-import time).\n const { getPluginTemplatesForCollection } = await import(\n \"../plugins/templates.js\"\n );\n for (const [id, value] of getPluginTemplatesForCollection(collectionSlug)) {\n const def = value as { label?: unknown; description?: unknown };\n summaries.set(id, {\n id,\n label: typeof def.label === \"string\" ? def.label : id,\n description:\n typeof def.description === \"string\" ? def.description : undefined,\n });\n }\n\n const active = await getActiveTheme();\n if (active) {\n const impl = active.impl as {\n templates?: Record<\n string,\n Record<string, { label?: string; description?: string }>\n >;\n };\n const set = impl.templates?.[collectionSlug];\n if (set) {\n for (const [id, def] of Object.entries(set)) {\n summaries.set(id, {\n id,\n label: typeof def.label === \"string\" ? def.label : id,\n description:\n typeof def.description === \"string\" ? def.description : undefined,\n });\n }\n }\n }\n\n return [...summaries.values()];\n}\n\n/**\n * Phase 14.5 — resolve a template's render component for the\n * given collection + template id. Looks up theme first\n * (theme always wins), falls back to plugin-contributed\n * templates. Returns the opaque value (the catch-all casts to\n * `{ component }` at the render boundary).\n */\nexport async function resolveTemplateComponent(\n collectionSlug: string,\n templateId: string,\n): Promise<unknown> {\n const active = await getActiveTheme();\n if (active) {\n const impl = active.impl as {\n templates?: Record<string, Record<string, unknown>>;\n };\n const themeEntry = impl.templates?.[collectionSlug]?.[templateId];\n if (themeEntry) return themeEntry;\n }\n\n const { getPluginTemplatesForCollection } = await import(\n \"../plugins/templates.js\"\n );\n const pluginEntry =\n getPluginTemplatesForCollection(collectionSlug).get(templateId);\n return pluginEntry ?? null;\n}\n","import { createReadStream, createWriteStream } from \"node:fs\";\nimport { access, mkdir, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { ReadableStream } from \"node:stream/web\";\n\nimport type { NpFileMetadata, NpStorageAdapter } from \"./types.js\";\n\nexport interface LocalStorageAdapterConfig {\n directory: string;\n baseUrl: string;\n}\n\nexport class LocalStorageAdapter implements NpStorageAdapter {\n constructor(private readonly config: LocalStorageAdapterConfig) {}\n\n async upload(\n key: string,\n data: Buffer | ReadableStream,\n _: NpFileMetadata,\n ): Promise<void> {\n const filePath = this.resolvePath(key);\n\n await mkdir(dirname(filePath), { recursive: true });\n\n if (Buffer.isBuffer(data)) {\n await writeFile(filePath, data);\n return;\n }\n\n await pipeline(Readable.fromWeb(data), createWriteStream(filePath));\n }\n\n getStream(key: string): Promise<ReadableStream> {\n return Promise.resolve(\n Readable.toWeb(createReadStream(this.resolvePath(key))),\n );\n }\n\n getUrl(key: string): Promise<string> {\n // `baseUrl` is commonly a relative path (the default is\n // `\"/uploads\"`) — `new URL(key, \"/uploads/\")` throws because\n // the URL constructor requires an absolute base. Concatenate\n // for relative baseUrls and let the URL constructor handle\n // absolute ones (full origin, S3-style, etc.).\n const base = this.normalizeBaseUrl(this.config.baseUrl);\n if (base.startsWith(\"/\")) {\n return Promise.resolve(`${base}/${key}`);\n }\n return Promise.resolve(new URL(key, `${base}/`).toString());\n }\n\n async delete(key: string): Promise<void> {\n await unlink(this.resolvePath(key));\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await access(this.resolvePath(key));\n return true;\n } catch {\n return false;\n }\n }\n\n private resolvePath(key: string): string {\n return join(this.config.directory, key);\n }\n\n private normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.endsWith(\"/\") ? baseUrl.slice(0, -1) : baseUrl;\n }\n}\n","import { Readable } from \"node:stream\";\nimport type { ReadableStream } from \"node:stream/web\";\n\nimport type { S3Client } from \"@aws-sdk/client-s3\";\n// `import type *` keeps this a compile-time alias only —\n// the actual `import(\"@aws-sdk/client-s3\")` happens lazily\n// in `s3ModulePromise` below, so apps that don't use S3\n// don't pay the import cost.\nimport type * as awsS3 from \"@aws-sdk/client-s3\";\n\nimport type { NpFileMetadata, NpStorageAdapter } from \"./types.js\";\n\nexport interface S3StorageAdapterConfig {\n bucket: string;\n region: string;\n endpoint?: string;\n credentials?: {\n accessKeyId: string;\n secretAccessKey: string;\n };\n}\n\ntype S3Module = typeof awsS3;\n\nlet s3ModulePromise: Promise<S3Module> | null = null;\n\nexport class S3StorageAdapter implements NpStorageAdapter {\n private clientPromise: Promise<S3Client> | null = null;\n\n constructor(private readonly config: S3StorageAdapterConfig) {}\n\n async upload(\n key: string,\n data: Buffer | ReadableStream,\n metadata: NpFileMetadata,\n ): Promise<void> {\n const [{ PutObjectCommand }, client] = await Promise.all([\n loadS3Module(),\n this.getClient(),\n ]);\n\n await client.send(\n new PutObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n Body: Buffer.isBuffer(data) ? data : Readable.fromWeb(data),\n ContentType: metadata.contentType,\n ContentLength: metadata.contentLength,\n Metadata: {\n originalFilename: metadata.originalFilename,\n },\n }),\n );\n }\n\n async getStream(key: string): Promise<ReadableStream> {\n const [{ GetObjectCommand }, client] = await Promise.all([\n loadS3Module(),\n this.getClient(),\n ]);\n const response = await client.send(\n new GetObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n }),\n );\n\n return toReadableStream(response.Body);\n }\n\n getUrl(key: string): Promise<string> {\n if (this.config.endpoint) {\n return Promise.resolve(\n new URL(\n key,\n `${normalizeUrl(this.config.endpoint)}/${this.config.bucket}/`,\n ).toString(),\n );\n }\n\n return Promise.resolve(\n new URL(\n key,\n `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/`,\n ).toString(),\n );\n }\n\n async delete(key: string): Promise<void> {\n const [{ DeleteObjectCommand }, client] = await Promise.all([\n loadS3Module(),\n this.getClient(),\n ]);\n\n await client.send(\n new DeleteObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n }),\n );\n }\n\n async exists(key: string): Promise<boolean> {\n const [{ HeadObjectCommand }, client] = await Promise.all([\n loadS3Module(),\n this.getClient(),\n ]);\n\n try {\n await client.send(\n new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n }),\n );\n return true;\n } catch (error) {\n if (isNotFoundError(error)) {\n return false;\n }\n\n throw error;\n }\n }\n\n private getClient(): Promise<S3Client> {\n if (!this.clientPromise) {\n this.clientPromise = this.createClient();\n }\n\n return this.clientPromise;\n }\n\n private async createClient(): Promise<S3Client> {\n const { S3Client } = await loadS3Module();\n\n return new S3Client({\n region: this.config.region,\n endpoint: this.config.endpoint,\n credentials: this.config.credentials,\n forcePathStyle: Boolean(this.config.endpoint),\n });\n }\n}\n\nasync function loadS3Module(): Promise<S3Module> {\n s3ModulePromise ??= import(\"@aws-sdk/client-s3\");\n return s3ModulePromise;\n}\n\nfunction toReadableStream(body: unknown): ReadableStream {\n if (hasTransformToWebStream(body)) {\n return body.transformToWebStream();\n }\n\n if (body instanceof Readable) {\n return Readable.toWeb(body);\n }\n\n throw new Error(\"S3 object body is not a readable stream.\");\n}\n\nfunction hasTransformToWebStream(\n value: unknown,\n): value is { transformToWebStream(): ReadableStream } {\n return typeof value === \"object\"\n && value !== null\n && \"transformToWebStream\" in value\n && typeof value.transformToWebStream === \"function\";\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n return typeof error === \"object\"\n && error !== null\n && ((\"name\" in error && error.name === \"NotFound\")\n || (\"$metadata\" in error\n && typeof error.$metadata === \"object\"\n && error.$metadata !== null\n && \"httpStatusCode\" in error.$metadata\n && error.$metadata.httpStatusCode === 404));\n}\n\nfunction normalizeUrl(value: string): string {\n return value.endsWith(\"/\") ? value.slice(0, -1) : value;\n}\n","import type { NpConfig } from \"../config/types.js\";\nimport { LocalStorageAdapter } from \"./local.js\";\nimport { S3StorageAdapter } from \"./s3.js\";\nimport type { NpStorageAdapter } from \"./types.js\";\n\nexport type { NpFileMetadata, NpStorageAdapter } from \"./types.js\";\nexport { LocalStorageAdapter } from \"./local.js\";\nexport { S3StorageAdapter } from \"./s3.js\";\n\ntype NpStorageConfig = NonNullable<NpConfig[\"storage\"]>;\n\nexport function createStorageAdapter(config: NpConfig[\"storage\"]): NpStorageAdapter {\n if (!config) {\n throw new Error(\"Storage configuration is required.\");\n }\n\n if (config.adapter === \"local\") {\n if (!config.local) {\n throw new Error(\"Local storage configuration is required.\");\n }\n\n return new LocalStorageAdapter(config.local);\n }\n\n if (!config.s3) {\n throw new Error(\"S3 storage configuration is required.\");\n }\n\n const s3Config = config.s3 as NpStorageConfig[\"s3\"] & {\n credentials?: {\n accessKeyId: string;\n secretAccessKey: string;\n };\n };\n\n return new S3StorageAdapter(s3Config);\n}\n","import { NpError } from \"../errors.js\";\nimport type { NpEmailAdapter, NpEmailMessage } from \"./types.js\";\n\nexport interface SmtpEmailAdapterOptions {\n host: string;\n port: number;\n user?: string;\n pass?: string;\n /** Default `From` header when a message doesn't override. */\n from: string;\n /**\n * Use implicit TLS (port 465) when `true`. When `false`, STARTTLS is\n * negotiated (port 587 / 25). Defaults to `port === 465`.\n */\n secure?: boolean;\n}\n\n// Narrow structural type for the nodemailer transporter we use. Declared\n// locally so core doesn't import @types/nodemailer just for one method.\ninterface NodemailerTransporterLike {\n sendMail(opts: {\n from: string;\n to: string;\n subject: string;\n text: string;\n html?: string;\n }): Promise<unknown>;\n verify?: () => Promise<unknown>;\n}\n\n/**\n * Nodemailer-backed SMTP adapter. Works with any SMTP-speaking provider\n * (Resend, SES, Mailgun, Postmark, Gmail, Zoho, custom relays).\n *\n * `nodemailer` is loaded dynamically on first send so that apps that don't\n * use the SMTP adapter (noop or custom adapter) never pay its import cost.\n */\nexport class SmtpEmailAdapter implements NpEmailAdapter {\n readonly kind = \"smtp\";\n private readonly options: SmtpEmailAdapterOptions;\n private transporter: NodemailerTransporterLike | null = null;\n\n constructor(options: SmtpEmailAdapterOptions) {\n this.options = options;\n }\n\n private async ensureTransporter(): Promise<NodemailerTransporterLike> {\n if (this.transporter) return this.transporter;\n\n let nodemailer: {\n createTransport: (cfg: unknown) => NodemailerTransporterLike;\n };\n try {\n // Indirect specifier so TypeScript doesn't try to\n // resolve `nodemailer` at compile time —\n // `@nexpress/core` doesn't depend on it. Apps using the\n // noop or a custom adapter never pay the import cost.\n const moduleId: string = \"nodemailer\";\n nodemailer = (await import(moduleId)) as typeof nodemailer;\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n throw new NpError(\n `Could not load \\`nodemailer\\` — add it to the app's dependencies to use the SMTP adapter. Cause: ${cause}`,\n \"EMAIL_ADAPTER_MISSING_DEPENDENCY\",\n 500,\n );\n }\n\n const { host, port, user, pass } = this.options;\n const secure = this.options.secure ?? port === 465;\n\n this.transporter = nodemailer.createTransport({\n host,\n port,\n secure,\n auth: user && pass ? { user, pass } : undefined,\n });\n return this.transporter;\n }\n\n async send(message: NpEmailMessage): Promise<void> {\n const transporter = await this.ensureTransporter();\n try {\n await transporter.sendMail({\n from: message.from ?? this.options.from,\n to: message.to,\n subject: message.subject,\n text: message.text,\n html: message.html,\n });\n } catch (error) {\n const cause = error instanceof Error ? error.message : String(error);\n throw new NpError(\n `Failed to deliver email via SMTP: ${cause}`,\n \"EMAIL_DELIVERY_FAILED\",\n 502,\n );\n }\n }\n}\n","export function sanitizeTokenValue(value: string): string {\n return value\n .replace(/[;{}]/g, \"\")\n .replace(/url\\s*\\(/gi, \"\")\n .replace(/expression\\s*\\(/gi, \"\")\n .replace(/@import/gi, \"\")\n .slice(0, 200);\n}\n","import type {\n NpCollectionConfig,\n NpFieldConfig,\n NpRelationshipField,\n NpThemeFieldRequirement,\n NpThemeManifest,\n} from \"../config/types.js\";\n\n/**\n * Phase F.1 — theme requirement check.\n *\n * Compares a theme's `manifest.requires` against the site's\n * registered collections and reports mismatches. The result\n * drives admin warnings on theme activation; F.8 will reuse\n * the same shape to drive the CLI patcher.\n *\n * \"Hard\" requirements (default) are normal mismatches the\n * operator should resolve. \"Soft\" requirements (`hard: false`)\n * are recorded separately so admin can show them at lower\n * severity — the theme renders without them but with degraded\n * behavior.\n */\n\nexport interface NpThemeRequirementMissingField {\n collection: string;\n field: string;\n expected: NpThemeFieldRequirement;\n hard: boolean;\n}\n\nexport interface NpThemeRequirementTypeConflict {\n collection: string;\n field: string;\n expected: NpThemeFieldRequirement[\"type\"];\n actual: string;\n hard: boolean;\n}\n\nexport interface NpThemeRequirementRelationConflict {\n collection: string;\n field: string;\n expected: NonNullable<NpThemeFieldRequirement[\"relationTo\"]>;\n actual: string | string[];\n hard: boolean;\n}\n\nexport interface NpThemeRequirementResult {\n themeId: string;\n hasMismatches: boolean;\n /** Has at least one HARD mismatch — operator should resolve before activation. */\n hasHardMismatches: boolean;\n missingCollections: Array<{\n collection: string;\n createIfAbsent: boolean;\n }>;\n missingFields: NpThemeRequirementMissingField[];\n typeConflicts: NpThemeRequirementTypeConflict[];\n relationConflicts: NpThemeRequirementRelationConflict[];\n}\n\n/**\n * Walk a collection config's field tree and produce a flat\n * `name → field` map. Theme requirements address top-level\n * fields by name; the walker handles `row` / `collapsible`\n * containers (which inline their children) but not `array` /\n * `group` (which scope their children inside a sub-record —\n * theme requirements don't reach into those, by design).\n */\nfunction flattenTopLevelFields(\n fields: NpFieldConfig[],\n): Map<string, NpFieldConfig> {\n const out = new Map<string, NpFieldConfig>();\n for (const f of fields) {\n if (f.type === \"row\" || f.type === \"collapsible\") {\n for (const [name, child] of flattenTopLevelFields(f.fields)) {\n out.set(name, child);\n }\n continue;\n }\n if (\"name\" in f && typeof f.name === \"string\") {\n out.set(f.name, f);\n }\n }\n return out;\n}\n\nfunction relationToMatches(\n expected: NonNullable<NpThemeFieldRequirement[\"relationTo\"]>,\n actual: NpRelationshipField[\"relationTo\"],\n): boolean {\n const expectedList = Array.isArray(expected) ? expected : [expected];\n const actualList = Array.isArray(actual) ? actual : [actual];\n // Theme expects every declared target to exist on the actual\n // field. Actual may include extras (theme isn't picky about\n // those) but must cover the expected set.\n return expectedList.every((e) => actualList.includes(e));\n}\n\nexport function checkThemeRequirements(\n manifest: NpThemeManifest,\n collections: NpCollectionConfig[],\n): NpThemeRequirementResult {\n const result: NpThemeRequirementResult = {\n themeId: manifest.id,\n hasMismatches: false,\n hasHardMismatches: false,\n missingCollections: [],\n missingFields: [],\n typeConflicts: [],\n relationConflicts: [],\n };\n\n const requires = manifest.requires?.collections;\n if (!requires) return result;\n\n const bySlug = new Map(collections.map((c) => [c.slug, c]));\n\n for (const [slug, req] of Object.entries(requires)) {\n const collection = bySlug.get(slug);\n if (!collection) {\n result.missingCollections.push({\n collection: slug,\n createIfAbsent: req.createIfAbsent ?? false,\n });\n continue;\n }\n if (!req.fields) continue;\n\n const fieldMap = flattenTopLevelFields(collection.fields);\n for (const [fieldName, fieldReq] of Object.entries(req.fields)) {\n const hard = fieldReq.hard ?? true;\n const actual = fieldMap.get(fieldName);\n if (!actual) {\n result.missingFields.push({\n collection: slug,\n field: fieldName,\n expected: fieldReq,\n hard,\n });\n continue;\n }\n if (actual.type !== fieldReq.type) {\n result.typeConflicts.push({\n collection: slug,\n field: fieldName,\n expected: fieldReq.type,\n actual: actual.type,\n hard,\n });\n continue;\n }\n if (\n fieldReq.type === \"relationship\" &&\n fieldReq.relationTo &&\n actual.type === \"relationship\"\n ) {\n if (\n !relationToMatches(\n fieldReq.relationTo,\n actual.relationTo,\n )\n ) {\n result.relationConflicts.push({\n collection: slug,\n field: fieldName,\n expected: fieldReq.relationTo,\n actual: actual.relationTo,\n hard,\n });\n }\n }\n }\n }\n\n const hardMismatches =\n result.missingCollections.length > 0 ||\n result.missingFields.some((m) => m.hard) ||\n result.typeConflicts.some((c) => c.hard) ||\n result.relationConflicts.some((c) => c.hard);\n const anyMismatches =\n result.missingCollections.length > 0 ||\n result.missingFields.length > 0 ||\n result.typeConflicts.length > 0 ||\n result.relationConflicts.length > 0;\n\n result.hasHardMismatches = hardMismatches;\n result.hasMismatches = anyMismatches;\n return result;\n}\n","import { and, eq } from \"drizzle-orm\";\nimport type { ZodTypeAny } from \"zod\";\n\nimport type { NpThemeManifest } from \"../config/types.js\";\nimport { getDb } from \"../db/index.js\";\nimport { npSettings } from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\nimport { getCurrentSiteId } from \"../sites/context.js\";\nimport { getActiveTheme, getThemeById } from \"./registry.js\";\nimport {\n introspectThemeSettingsSchema,\n type NpThemeSettingsField,\n} from \"./settings-schema.js\";\n\nconst DEFAULT_SITE = \"default\";\n\n/**\n * Phase F.3 — per-theme operator settings.\n *\n * Stored at `np_settings.(site_id, key=\"theme.settings:<themeId>\")`\n * with the value being the parsed `z.infer<typeof\n * settingsSchema>`. Reuses the existing `nx:theme:<siteId>`\n * cache tag (see design doc §5.3) — settings live on the same\n * read paths as tokens / active id, so a shared bust avoids\n * fragmenting the tag namespace.\n */\n\nfunction settingsKey(themeId: string): string {\n return `theme.settings:${themeId}`;\n}\n\n/**\n * v0.3 (D) — versioned envelope for persisted theme settings.\n *\n * Sentinel keys (`__npVersion`, `__npSettings`) avoid collision\n * with theme-owned setting fields (themes rarely choose names\n * starting with `__np`; a `version` / `value` heuristic was\n * considered but rejected because both names are plausible\n * theme-author choices for actual settings).\n *\n * Legacy unwrapped values (written by v0.2 before this PR) are\n * detected by the absence of the sentinel keys and migrated\n * in-place to v1 → current.\n */\n/** Internal — exported for unit tests only. */\nexport interface NpVersionedSettings {\n __npVersion: number;\n __npSettings: unknown;\n}\n\n/** Internal — exported for unit tests only. */\nexport function isVersionedSettings(value: unknown): value is NpVersionedSettings {\n if (!value || typeof value !== \"object\") return false;\n const candidate = value as Partial<NpVersionedSettings>;\n // Number.isFinite rejects NaN / Infinity / non-numbers — a\n // hand-crafted or corrupted DB value with `__npVersion: NaN`\n // would otherwise pass typeof and trip the migration path's\n // `>=` comparisons (NaN >= N always false).\n return (\n typeof candidate.__npVersion === \"number\" &&\n Number.isFinite(candidate.__npVersion) &&\n \"__npSettings\" in candidate\n );\n}\n\n/** Run the theme's `settingsMigrate` from `from` to current\n * schema version. No-op when versions match (or when the theme\n * doesn't declare a migrator). Defensive try/catch — a buggy\n * migrate fn shouldn't blow up the read path; we fall back to\n * the original value and let `safeParse` decide. */\n/** Internal — exported for unit tests only. */\nexport function applyMigration(\n manifest: NpThemeManifest,\n rawValue: unknown,\n fromVersion: number,\n): unknown {\n const target = manifest.settingsVersion ?? 1;\n if (fromVersion >= target) return rawValue;\n const migrate = manifest.settingsMigrate;\n if (typeof migrate !== \"function\") return rawValue;\n try {\n return migrate(rawValue, fromVersion);\n } catch {\n return rawValue;\n }\n}\n\nfunction defaultsFrom(fields: NpThemeSettingsField[]): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const f of fields) {\n if (f.default !== undefined) {\n out[f.name] = f.default;\n continue;\n }\n if (f.type === \"object\") {\n out[f.name] = defaultsFrom(f.fields);\n }\n if (f.type === \"array\") {\n out[f.name] = [];\n }\n }\n return out;\n}\n\n/**\n * Read the persisted settings row for a theme and parse it via\n * the theme's schema. Returns the parsed value when valid;\n * falls back to schema defaults on parse failure (with the\n * failure recorded for the admin to surface, see\n * `getThemeSettingsWithStatus`).\n *\n * `themeId` defaults to the active theme. Pass an explicit id\n * to read another installed theme's settings (used by the\n * admin settings page).\n *\n * Return type is `unknown` because core can't type-narrow to\n * a specific theme's `z.infer<typeof schema>` — the schema\n * lives in the theme package, not in core. Theme components\n * should cast at the call site, ideally against an exported\n * type alias from the theme package itself:\n *\n * // packages/themes/magazine/src/index.ts\n * export const settingsSchema = z.object({ ... });\n * export type MagazineSettings = z.infer<typeof settingsSchema>;\n *\n * // a theme component\n * const settings = (await getThemeSettings()) as MagazineSettings;\n */\nexport async function getThemeSettings(\n themeId?: string,\n): Promise<unknown> {\n const result = await getThemeSettingsWithStatus(themeId);\n return result.value;\n}\n\nexport interface NpThemeSettingsResult {\n themeId: string | null;\n /** Parsed settings or schema defaults (never null when a theme\n * has a schema; empty object when the theme has no schema). */\n value: unknown;\n /** True when there's a stored row, regardless of whether it\n * passed validation. */\n hasPersisted: boolean;\n /** Set when the persisted value failed `schema.parse()`. The\n * admin surface uses this to show a \"values reset to\n * defaults\" banner. */\n parseError?: string;\n}\n\nexport async function getThemeSettingsWithStatus(\n themeId?: string,\n): Promise<NpThemeSettingsResult> {\n const theme = themeId ? getThemeById(themeId) : await getActiveTheme();\n if (!theme) {\n return { themeId: null, value: {}, hasPersisted: false };\n }\n const schema = theme.manifest.settingsSchema as ZodTypeAny | undefined;\n if (!schema) {\n return { themeId: theme.manifest.id, value: {}, hasPersisted: false };\n }\n\n const db = getDb();\n const siteId = (await getCurrentSiteId()) ?? DEFAULT_SITE;\n const rows = (await db\n .select()\n .from(npSettings)\n .where(\n and(\n eq(npSettings.siteId, siteId),\n eq(npSettings.key, settingsKey(theme.manifest.id)),\n ),\n )\n .limit(1)) as Array<{ value: unknown }>;\n\n const fields = introspectThemeSettingsSchema(schema);\n const defaults = defaultsFrom(fields);\n\n const row = rows[0];\n if (!row) {\n // No row stored yet — first access returns schema defaults.\n // We don't write a row eagerly; the operator's first save\n // creates one.\n const parsed = schema.safeParse(defaults);\n return {\n themeId: theme.manifest.id,\n value: parsed.success ? parsed.data : defaults,\n hasPersisted: false,\n };\n }\n\n // v0.3 (D) — version detection + migration.\n //\n // Storage shape options:\n // - `{ __npVersion: N, __npSettings: ... }` (v0.3+ wrapped)\n // - bare value (legacy v0.2 unwrapped — treated as v1)\n //\n // When stored version < manifest.settingsVersion, run the\n // migrator. Re-parse the result; on parse failure (buggy\n // migrate, or schema drift the migrator doesn't cover), fall\n // back to defaults with parseError so the admin surfaces it.\n const versioned = isVersionedSettings(row.value) ? row.value : null;\n const storedVersion = versioned ? versioned.__npVersion : 1;\n const rawValue = versioned ? versioned.__npSettings : row.value;\n const valueToParse = applyMigration(theme.manifest, rawValue, storedVersion);\n\n const parsed = schema.safeParse(valueToParse);\n if (parsed.success) {\n return {\n themeId: theme.manifest.id,\n value: parsed.data,\n hasPersisted: true,\n };\n }\n\n // Schema mismatch (theme upgrade changed the shape and either\n // declared no migrator, or the migrator didn't cover this path).\n // Fall back to defaults and surface the error; the admin\n // renders a \"settings were reset\" banner.\n return {\n themeId: theme.manifest.id,\n value: defaults,\n hasPersisted: true,\n parseError: parsed.error.message,\n };\n}\n\n/**\n * Validate and persist a theme's settings. Throws\n * `NpValidationError` when `value` doesn't pass the schema —\n * the admin form must surface field-level errors before\n * calling this.\n *\n * **Cache invalidation is the caller's responsibility.** This\n * function writes to `np_settings` only; it doesn't import\n * `next/cache`. The admin API route (`PUT\n * /api/admin/themes/[id]/settings`) busts `nx:theme:<siteId>`\n * (and `nx:sitemap:*` / `nx:feed:*` when `impl.seo` is\n * declared) after a successful write. Other callers — jobs,\n * scripts, server actions — must do the same to avoid stale\n * cached reads.\n */\nexport async function setThemeSettings(\n themeId: string,\n value: unknown,\n updatedBy: string | null = null,\n): Promise<unknown> {\n const theme = getThemeById(themeId);\n if (!theme) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"themeId\",\n message: `Unknown theme '${themeId}'. Register it in nexpress.config.ts first.`,\n },\n ]);\n }\n const schema = theme.manifest.settingsSchema as ZodTypeAny | undefined;\n if (!schema) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"themeId\",\n message: `Theme '${themeId}' does not declare a settingsSchema.`,\n },\n ]);\n }\n\n const parsed = schema.safeParse(value);\n if (!parsed.success) {\n throw new NpValidationError(\n \"Settings failed validation\",\n parsed.error.issues.map((i) => ({\n field: i.path.join(\".\"),\n message: i.message,\n })),\n );\n }\n\n // v0.3 (D) — wrap in the versioned envelope so future schema\n // changes can detect what version produced this row. Themes\n // that haven't declared `settingsVersion` get `1` (the v0.2\n // baseline) for forward-compat with the migration pipeline.\n const wrapped: NpVersionedSettings = {\n __npVersion: theme.manifest.settingsVersion ?? 1,\n __npSettings: parsed.data,\n };\n\n const db = getDb();\n const now = new Date();\n const siteId = (await getCurrentSiteId()) ?? DEFAULT_SITE;\n await db\n .insert(npSettings)\n .values({\n siteId,\n key: settingsKey(themeId),\n value: wrapped,\n updatedAt: now,\n updatedBy,\n })\n .onConflictDoUpdate({\n target: [npSettings.siteId, npSettings.key],\n set: { value: wrapped, updatedAt: now, updatedBy },\n });\n\n return parsed.data;\n}\n\n/**\n * Whether the active theme contributes SEO hooks. The settings\n * save path consults this to decide whether to additionally\n * invalidate `nx:sitemap:*` / `nx:feed:*` tags alongside the\n * always-busted `nx:theme:<siteId>`. Implemented here (in core)\n * so the API layer in `apps/web` doesn't need to duck-type the\n * theme `impl`.\n */\nexport async function activeThemeContributesSeo(): Promise<boolean> {\n const theme = await getActiveTheme();\n if (!theme) return false;\n // `impl` is opaque to core; we do a structural check.\n const impl = theme.impl as\n | { seo?: { sitemapEntries?: unknown; feedEntries?: unknown } }\n | undefined;\n if (!impl?.seo) return false;\n return Boolean(impl.seo.sitemapEntries || impl.seo.feedEntries);\n}\n","import { getActiveTheme } from \"./registry.js\";\n\n/**\n * Phase F.6 — extract the active theme's declared nav\n * locations as plain JSON metadata, narrowed structurally\n * because core treats `theme.impl` as opaque (`unknown`).\n *\n * Used by:\n * - The admin nav editor's location-dropdown endpoint\n * (`/api/navigation/locations`) so the dropdown surfaces\n * theme-named slots with friendly labels.\n * - Future cookbook recipes that surface \"this theme\n * expects you to fill in these menus\" guidance.\n *\n * Returns `[]` when no theme is active or the active theme\n * doesn't declare any locations.\n */\n\nexport interface NpThemeNavLocationDescriptor {\n /** Location key, e.g. `\"primary\"`. Used as the `location`\n * argument to `getNavigation(...)` and the database row's\n * `(siteId, location)` lookup. */\n key: string;\n label: string;\n description?: string;\n maxItems?: number;\n}\n\ninterface ImplShape {\n navLocations?: Record<\n string,\n {\n label?: unknown;\n description?: unknown;\n maxItems?: unknown;\n }\n >;\n}\n\n/**\n * Pure extractor — narrows a theme `impl` (opaque from core's\n * perspective) to a flat list of validated location\n * descriptors. Exported for unit testability without the DB\n * roundtrip that `getActiveThemeNavLocations` does.\n */\nexport function extractNavLocationsFromImpl(\n impl: unknown,\n): NpThemeNavLocationDescriptor[] {\n const shape = impl as ImplShape | undefined;\n const declared = shape?.navLocations;\n if (!declared || typeof declared !== \"object\") return [];\n\n const out: NpThemeNavLocationDescriptor[] = [];\n for (const [key, raw] of Object.entries(declared)) {\n if (!raw || typeof raw !== \"object\") continue;\n if (typeof raw.label !== \"string\") continue;\n out.push({\n key,\n label: raw.label,\n description:\n typeof raw.description === \"string\" ? raw.description : undefined,\n maxItems:\n typeof raw.maxItems === \"number\" ? raw.maxItems : undefined,\n });\n }\n return out;\n}\n\nexport async function getActiveThemeNavLocations(): Promise<\n NpThemeNavLocationDescriptor[]\n> {\n const theme = await getActiveTheme();\n if (!theme) return [];\n return extractNavLocationsFromImpl(theme.impl);\n}\n","import type { NpFeedEntry, NpSitemapEntry } from \"../seo/index.js\";\nimport { getActiveTheme } from \"./registry.js\";\n\n/**\n * Phase F.7 — pure structural narrowers for theme `notFound`,\n * `error`, and `seo` contributions. Core treats `theme.impl`\n * as opaque (`unknown`); these helpers do the duck-typing in\n * one place so the consuming routes stay readable.\n *\n * `getActiveThemeNotFoundComponent` / `…ErrorComponent` return\n * the React component refs unchanged — typing as `unknown`\n * here, the consumers (in `apps/web`) cast to ComponentType\n * at the JSX site. Core can't import `react` directly without\n * dragging the peer into a server-only package, so the cast\n * is delegated.\n */\n\nexport interface NpThemeSeoHooksExtracted {\n sitemapEntries?: () => Promise<NpSitemapEntry[]> | NpSitemapEntry[];\n feedEntries?: () => Promise<NpFeedEntry[]> | NpFeedEntry[];\n robotsTxt?: () => string | Promise<string>;\n}\n\ninterface ImplShape {\n notFound?: unknown;\n error?: unknown;\n members?: {\n notFound?: unknown;\n error?: unknown;\n };\n seo?: NpThemeSeoHooksExtracted;\n}\n\nexport function extractNotFoundComponent(impl: unknown): unknown {\n const shape = impl as ImplShape | undefined;\n return typeof shape?.notFound === \"function\" ? shape.notFound : null;\n}\n\nexport function extractErrorComponent(impl: unknown): unknown {\n const shape = impl as ImplShape | undefined;\n return typeof shape?.error === \"function\" ? shape.error : null;\n}\n\n/**\n * Phase M.3 — member-tree 404 component, with fallback to the\n * top-level `impl.notFound`. Returns `null` when neither the\n * member-specific nor the top-level component is declared (the\n * caller renders the framework default). Same opacity contract\n * as `extractNotFoundComponent` — typed as `unknown` here, the\n * consumer in `apps/web/src/app/(member)/not-found.tsx` casts\n * to `ComponentType` at the JSX site.\n */\nexport function extractMembersNotFoundComponent(impl: unknown): unknown {\n const shape = impl as ImplShape | undefined;\n const memberLevel = shape?.members?.notFound;\n if (typeof memberLevel === \"function\") return memberLevel;\n return typeof shape?.notFound === \"function\" ? shape.notFound : null;\n}\n\nexport function extractSeoHooks(impl: unknown): NpThemeSeoHooksExtracted {\n const shape = impl as ImplShape | undefined;\n const seo = shape?.seo;\n if (!seo || typeof seo !== \"object\") return {};\n const out: NpThemeSeoHooksExtracted = {};\n if (typeof seo.sitemapEntries === \"function\") {\n out.sitemapEntries = seo.sitemapEntries;\n }\n if (typeof seo.feedEntries === \"function\") {\n out.feedEntries = seo.feedEntries;\n }\n if (typeof seo.robotsTxt === \"function\") {\n out.robotsTxt = seo.robotsTxt;\n }\n return out;\n}\n\n/**\n * Async sugar over the active theme. Each helper returns a\n * fresh resolution per call; multi-site safety comes from\n * `getActiveTheme()` reading per-request site context.\n */\n\nexport async function getActiveThemeNotFound(): Promise<unknown> {\n const theme = await getActiveTheme();\n if (!theme) return null;\n return extractNotFoundComponent(theme.impl);\n}\n\nexport async function getActiveThemeError(): Promise<unknown> {\n const theme = await getActiveTheme();\n if (!theme) return null;\n return extractErrorComponent(theme.impl);\n}\n\n/**\n * Phase M.3 — async sugar for the member-tree 404. Returns the\n * theme's `impl.members.notFound` when declared, falling back\n * to its `impl.notFound` (top-level), then `null`.\n */\nexport async function getActiveThemeMembersNotFound(): Promise<unknown> {\n const theme = await getActiveTheme();\n if (!theme) return null;\n return extractMembersNotFoundComponent(theme.impl);\n}\n\nexport async function getActiveThemeSeoHooks(): Promise<NpThemeSeoHooksExtracted> {\n const theme = await getActiveTheme();\n if (!theme) return {};\n return extractSeoHooks(theme.impl);\n}\n","import { and, eq } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npSiteMemberships, npUsers } from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\nimport type { NpAuthUser, NpUserRole } from \"../config/types.js\";\n\nimport { getCurrentSiteId } from \"./context.js\";\nimport { NP_DEFAULT_SITE_ID } from \"./registry.js\";\n\n/**\n * Phase 15.5 — per-site role memberships.\n *\n * `npUsers.role` stays the \"global default role\" (used by\n * existing single-tenant code and as a fallback when no\n * explicit membership exists for a given site). New\n * multi-tenant deployments grant explicit memberships via\n * the helpers here; the `isSuperAdmin` flag (also new in\n * 15.5) bypasses membership checks entirely so a super-admin\n * can administer every site without having to be enrolled\n * on each one individually.\n *\n * The framework's existing `hasRole(user, minRole)` keeps\n * working as a global check. New site-scoped checks should\n * use `hasRoleOnSite(user, siteId, minRole)`.\n */\n\nexport interface SiteMembership {\n siteId: string;\n userId: string;\n role: NpUserRole;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport async function listSiteMemberships(\n siteId: string,\n): Promise<SiteMembership[]> {\n const db = getDb();\n const rows = await db\n .select()\n .from(npSiteMemberships)\n .where(eq(npSiteMemberships.siteId, siteId));\n return rows.map((row) => ({\n siteId: row.siteId,\n userId: row.userId,\n role: row.role,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n}\n\nexport async function listMembershipsForUser(\n userId: string,\n): Promise<SiteMembership[]> {\n const db = getDb();\n const rows = await db\n .select()\n .from(npSiteMemberships)\n .where(eq(npSiteMemberships.userId, userId));\n return rows.map((row) => ({\n siteId: row.siteId,\n userId: row.userId,\n role: row.role,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n}\n\nexport async function getMembership(\n siteId: string,\n userId: string,\n): Promise<SiteMembership | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSiteMemberships)\n .where(\n and(\n eq(npSiteMemberships.siteId, siteId),\n eq(npSiteMemberships.userId, userId),\n ),\n )\n .limit(1);\n if (!row) return null;\n return {\n siteId: row.siteId,\n userId: row.userId,\n role: row.role,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport async function grantSiteMembership(\n siteId: string,\n userId: string,\n role: NpUserRole,\n): Promise<SiteMembership> {\n const db = getDb();\n const now = new Date();\n const [row] = await db\n .insert(npSiteMemberships)\n .values({ siteId, userId, role, createdAt: now, updatedAt: now })\n .onConflictDoUpdate({\n target: [npSiteMemberships.siteId, npSiteMemberships.userId],\n set: { role, updatedAt: now },\n })\n .returning();\n if (!row) throw new Error(\"Failed to grant membership\");\n return {\n siteId: row.siteId,\n userId: row.userId,\n role: row.role,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport async function revokeSiteMembership(\n siteId: string,\n userId: string,\n): Promise<void> {\n const db = getDb();\n await db\n .delete(npSiteMemberships)\n .where(\n and(\n eq(npSiteMemberships.siteId, siteId),\n eq(npSiteMemberships.userId, userId),\n ),\n );\n}\n\n/**\n * Promote / demote a user's super-admin status. Super-admins\n * bypass per-site membership checks; this is the framework's\n * \"I can do anything\" gate so it should be granted sparingly\n * (one or two operators per deployment, typically).\n */\nexport async function setSuperAdmin(\n userId: string,\n isSuperAdmin: boolean,\n): Promise<void> {\n const db = getDb();\n const result = await db\n .update(npUsers)\n .set({ isSuperAdmin, updatedAt: new Date() })\n .where(eq(npUsers.id, userId))\n .returning({ id: npUsers.id });\n if (result.length === 0) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"userId\", message: `User \"${userId}\" not found` },\n ]);\n }\n}\n\nconst ROLE_RANK: Record<NpUserRole, number> = {\n viewer: 0,\n author: 1,\n moderator: 2,\n editor: 3,\n admin: 4,\n};\n\n/**\n * Resolve a user's effective role on a specific site:\n * 1. Super-admins always get `admin`.\n * 2. Explicit site membership wins over the global role.\n * 3. Fallback: the user's global `npUsers.role` (preserves\n * single-tenant behavior).\n *\n * Use this rather than `user.role` for any check that should\n * respect tenant boundaries.\n */\nexport async function resolveUserRoleOnSite(\n user: NpAuthUser,\n siteId: string,\n): Promise<NpUserRole> {\n // Super-admin shortcut — read from npUsers (the JWT may\n // not carry the flag).\n const db = getDb();\n const [row] = await db\n .select({ isSuperAdmin: npUsers.isSuperAdmin, role: npUsers.role })\n .from(npUsers)\n .where(eq(npUsers.id, user.id))\n .limit(1);\n if (!row) return user.role;\n if (row.isSuperAdmin) return \"admin\";\n\n // Explicit membership for this site?\n const membership = await getMembership(siteId, user.id);\n if (membership) return membership.role;\n\n // Fallback to global default role.\n return row.role;\n}\n\n/**\n * Site-scoped variant of `hasRole`. Resolves the user's\n * effective role on the site (super-admin → admin, explicit\n * membership → that role, otherwise global default) and\n * compares against `minRole` using the same rank order\n * `hasRole` uses.\n *\n * Defaults to the current request site (or the framework's\n * `default` site when no resolver is wired) so callers don't\n * have to thread siteId everywhere.\n */\nexport async function hasRoleOnSite(\n user: NpAuthUser,\n minRole: NpUserRole,\n siteId?: string,\n): Promise<boolean> {\n const targetSite =\n siteId ?? (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const role = await resolveUserRoleOnSite(user, targetSite);\n return ROLE_RANK[role] >= ROLE_RANK[minRole];\n}\n\n/**\n * Quick boolean check for super-admin status. Cheaper than\n * `resolveUserRoleOnSite` when the caller only needs to know\n * \"can this user manage the framework as a whole?\".\n */\nexport async function isSuperAdmin(user: NpAuthUser): Promise<boolean> {\n const db = getDb();\n const [row] = await db\n .select({ isSuperAdmin: npUsers.isSuperAdmin })\n .from(npUsers)\n .where(eq(npUsers.id, user.id))\n .limit(1);\n return Boolean(row?.isSuperAdmin);\n}\n","import { eq } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { npPlugins } from \"../db/schema/system.js\";\nimport { invalidatePluginEnabled } from \"./enabled-gate.js\";\n\n/**\n * G.1 — `np_plugins` is now a lean meta row: `(id, enabled,\n * installed_at, updated_at)`. The legacy `config` jsonb column was\n * dropped in favor of `np_settings` rows keyed by `plugin.config:<id>`\n * (see `packages/core/src/plugins/config.ts` and decision E in\n * `docs/design/plugin-config-auto-form.md`). Read / write plugin\n * config through `getPluginConfig` / `setPluginConfig` from the\n * config module — `getPluginState` only knows about the enable flag.\n */\nexport interface NpPluginState {\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n}\n\nexport interface NpPluginStateUpdate {\n enabled?: boolean;\n}\n\ninterface DrizzleDb {\n select: NodePgDatabase<Record<string, unknown>>[\"select\"];\n insert: NodePgDatabase<Record<string, unknown>>[\"insert\"];\n update: NodePgDatabase<Record<string, unknown>>[\"update\"];\n}\n\nfunction toState(row: {\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n}): NpPluginState {\n return {\n id: row.id,\n enabled: row.enabled,\n installedAt: row.installedAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport async function listPluginStates(\n db: NodePgDatabase<Record<string, unknown>>,\n): Promise<NpPluginState[]> {\n const rows = (await (db as unknown as DrizzleDb).select().from(npPlugins)) as Array<{\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n }>;\n\n return rows.map(toState);\n}\n\nexport async function getPluginState(\n db: NodePgDatabase<Record<string, unknown>>,\n id: string,\n): Promise<NpPluginState | null> {\n const rows = (await (db as unknown as DrizzleDb)\n .select()\n .from(npPlugins)\n .where(eq(npPlugins.id, id))\n .limit(1)) as Array<{\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n }>;\n\n return rows[0] ? toState(rows[0]) : null;\n}\n\n/**\n * Ensures every known plugin id has a row in `np_plugins`. Missing rows are\n * inserted with `enabled=true`. Existing rows are never touched — this is\n * called on boot and must not clobber operator edits.\n *\n * Uses a single INSERT … ON CONFLICT DO NOTHING so concurrent boots (multi-\n * process deployments) can all race safely without unique-key violations.\n */\nexport async function syncPluginRegistrations(\n db: NodePgDatabase<Record<string, unknown>>,\n pluginIds: readonly string[],\n): Promise<void> {\n if (pluginIds.length === 0) return;\n\n const now = new Date();\n await db\n .insert(npPlugins)\n .values(\n pluginIds.map((id) => ({\n id,\n enabled: true,\n installedAt: now,\n updatedAt: now,\n })),\n )\n .onConflictDoNothing({ target: npPlugins.id });\n}\n\nexport async function updatePluginState(\n db: NodePgDatabase<Record<string, unknown>>,\n id: string,\n patch: NpPluginStateUpdate,\n): Promise<NpPluginState | null> {\n const values: Record<string, unknown> = {\n updatedAt: new Date(),\n };\n\n if (patch.enabled !== undefined) {\n values.enabled = patch.enabled;\n }\n\n const rows = (await (db as unknown as DrizzleDb)\n .update(npPlugins)\n .set(values)\n .where(eq(npPlugins.id, id))\n .returning()) as Array<{\n id: string;\n enabled: boolean;\n installedAt: Date;\n updatedAt: Date;\n }>;\n\n // Drop the cached enabled flag so the very next dispatch re-reads the row\n // instead of waiting out the TTL. Without this, a toggle from the admin UI\n // would feel laggy for up to 5s.\n if (patch.enabled !== undefined) {\n invalidatePluginEnabled(id);\n }\n\n return rows[0] ? toState(rows[0]) : null;\n}\n"],"mappings":"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","names":["synth","merged","injectedHere","and","eq","getPluginTemplatesForCollection","and","eq","Readable","and","eq","and","eq","parsed","and","eq","eq","and","isSuperAdmin","eq","eq"]}