@nexpress/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/audit-54XLVCWD.js +14 -0
  4. package/dist/audit-54XLVCWD.js.map +1 -0
  5. package/dist/auth.d.ts +640 -0
  6. package/dist/auth.js +94 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/can-YLUHRJAB.js +19 -0
  9. package/dist/can-YLUHRJAB.js.map +1 -0
  10. package/dist/chunk-2G264RCD.js +68 -0
  11. package/dist/chunk-2G264RCD.js.map +1 -0
  12. package/dist/chunk-2YDGE7YX.js +92 -0
  13. package/dist/chunk-2YDGE7YX.js.map +1 -0
  14. package/dist/chunk-473S4TER.js +538 -0
  15. package/dist/chunk-473S4TER.js.map +1 -0
  16. package/dist/chunk-4ZLMEKFX.js +18 -0
  17. package/dist/chunk-4ZLMEKFX.js.map +1 -0
  18. package/dist/chunk-55FU6WED.js +179 -0
  19. package/dist/chunk-55FU6WED.js.map +1 -0
  20. package/dist/chunk-6YI5K2TI.js +1959 -0
  21. package/dist/chunk-6YI5K2TI.js.map +1 -0
  22. package/dist/chunk-BHK3AD3Q.js +41 -0
  23. package/dist/chunk-BHK3AD3Q.js.map +1 -0
  24. package/dist/chunk-CRUQBZUF.js +39 -0
  25. package/dist/chunk-CRUQBZUF.js.map +1 -0
  26. package/dist/chunk-CTSQ7BRI.js +175 -0
  27. package/dist/chunk-CTSQ7BRI.js.map +1 -0
  28. package/dist/chunk-DK2JBJH7.js +81 -0
  29. package/dist/chunk-DK2JBJH7.js.map +1 -0
  30. package/dist/chunk-DP2PREDU.js +597 -0
  31. package/dist/chunk-DP2PREDU.js.map +1 -0
  32. package/dist/chunk-EQ2Z3KMD.js +24 -0
  33. package/dist/chunk-EQ2Z3KMD.js.map +1 -0
  34. package/dist/chunk-FZ7O6DWI.js +305 -0
  35. package/dist/chunk-FZ7O6DWI.js.map +1 -0
  36. package/dist/chunk-ISLYFQWL.js +1270 -0
  37. package/dist/chunk-ISLYFQWL.js.map +1 -0
  38. package/dist/chunk-JJL74ZPK.js +68 -0
  39. package/dist/chunk-JJL74ZPK.js.map +1 -0
  40. package/dist/chunk-JKXAPSU4.js +24 -0
  41. package/dist/chunk-JKXAPSU4.js.map +1 -0
  42. package/dist/chunk-KU5M27ZC.js +24 -0
  43. package/dist/chunk-KU5M27ZC.js.map +1 -0
  44. package/dist/chunk-LSHHRDVR.js +34 -0
  45. package/dist/chunk-LSHHRDVR.js.map +1 -0
  46. package/dist/chunk-M43PGOQY.js +715 -0
  47. package/dist/chunk-M43PGOQY.js.map +1 -0
  48. package/dist/chunk-MEJAHXIO.js +150 -0
  49. package/dist/chunk-MEJAHXIO.js.map +1 -0
  50. package/dist/chunk-NUCGHWCF.js +101 -0
  51. package/dist/chunk-NUCGHWCF.js.map +1 -0
  52. package/dist/chunk-OK5HOCQI.js +845 -0
  53. package/dist/chunk-OK5HOCQI.js.map +1 -0
  54. package/dist/chunk-OROPGO65.js +13 -0
  55. package/dist/chunk-OROPGO65.js.map +1 -0
  56. package/dist/chunk-PPAS4SZR.js +176 -0
  57. package/dist/chunk-PPAS4SZR.js.map +1 -0
  58. package/dist/chunk-PPBWRKO2.js +171 -0
  59. package/dist/chunk-PPBWRKO2.js.map +1 -0
  60. package/dist/chunk-PZ5AY32C.js +10 -0
  61. package/dist/chunk-PZ5AY32C.js.map +1 -0
  62. package/dist/chunk-QO7LAQZH.js +321 -0
  63. package/dist/chunk-QO7LAQZH.js.map +1 -0
  64. package/dist/chunk-QVJ2HCAX.js +225 -0
  65. package/dist/chunk-QVJ2HCAX.js.map +1 -0
  66. package/dist/chunk-RIPHIRPP.js +68 -0
  67. package/dist/chunk-RIPHIRPP.js.map +1 -0
  68. package/dist/chunk-S27S42QY.js +134 -0
  69. package/dist/chunk-S27S42QY.js.map +1 -0
  70. package/dist/chunk-SBCVAC2Z.js +40 -0
  71. package/dist/chunk-SBCVAC2Z.js.map +1 -0
  72. package/dist/chunk-TFJ4MKPH.js +694 -0
  73. package/dist/chunk-TFJ4MKPH.js.map +1 -0
  74. package/dist/chunk-THX3SHYA.js +75 -0
  75. package/dist/chunk-THX3SHYA.js.map +1 -0
  76. package/dist/chunk-UGQSQO5B.js +222 -0
  77. package/dist/chunk-UGQSQO5B.js.map +1 -0
  78. package/dist/chunk-V2UNHGAP.js +26 -0
  79. package/dist/chunk-V2UNHGAP.js.map +1 -0
  80. package/dist/chunk-VGTPQXNQ.js +2790 -0
  81. package/dist/chunk-VGTPQXNQ.js.map +1 -0
  82. package/dist/chunk-VNIHXQ7W.js +194 -0
  83. package/dist/chunk-VNIHXQ7W.js.map +1 -0
  84. package/dist/chunk-WV272MPW.js +31 -0
  85. package/dist/chunk-WV272MPW.js.map +1 -0
  86. package/dist/chunk-X5KKBOUS.js +26 -0
  87. package/dist/chunk-X5KKBOUS.js.map +1 -0
  88. package/dist/chunk-XANPEOJC.js +17 -0
  89. package/dist/chunk-XANPEOJC.js.map +1 -0
  90. package/dist/chunk-XPVQIHAQ.js +83 -0
  91. package/dist/chunk-XPVQIHAQ.js.map +1 -0
  92. package/dist/chunk-ZCINJSS4.js +75 -0
  93. package/dist/chunk-ZCINJSS4.js.map +1 -0
  94. package/dist/community.d.ts +1425 -0
  95. package/dist/community.js +206 -0
  96. package/dist/community.js.map +1 -0
  97. package/dist/config-2GDU7PCK.js +32 -0
  98. package/dist/config-2GDU7PCK.js.map +1 -0
  99. package/dist/context-MNZ4QXPC.js +16 -0
  100. package/dist/context-MNZ4QXPC.js.map +1 -0
  101. package/dist/db-schema.d.ts +4 -0
  102. package/dist/db-schema.js +102 -0
  103. package/dist/db-schema.js.map +1 -0
  104. package/dist/db.d.ts +7 -0
  105. package/dist/db.js +117 -0
  106. package/dist/db.js.map +1 -0
  107. package/dist/digest-SY42GQSU.js +17 -0
  108. package/dist/digest-SY42GQSU.js.map +1 -0
  109. package/dist/errors-5OS3S2J3.js +22 -0
  110. package/dist/errors-5OS3S2J3.js.map +1 -0
  111. package/dist/host-OBOI4MJK.js +51 -0
  112. package/dist/host-OBOI4MJK.js.map +1 -0
  113. package/dist/i18n.d.ts +301 -0
  114. package/dist/i18n.js +68 -0
  115. package/dist/i18n.js.map +1 -0
  116. package/dist/index-B6-_vr_m.d.ts +590 -0
  117. package/dist/index-CY55LC0u.d.ts +4722 -0
  118. package/dist/index-CeiTvwbp.d.ts +168 -0
  119. package/dist/index-XwP1ET8b.d.ts +61 -0
  120. package/dist/index.d.ts +2037 -0
  121. package/dist/index.js +2205 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/job-log-VZXWQUDK.js +24 -0
  124. package/dist/job-log-VZXWQUDK.js.map +1 -0
  125. package/dist/jobs.d.ts +4 -0
  126. package/dist/jobs.js +76 -0
  127. package/dist/jobs.js.map +1 -0
  128. package/dist/logger-DqGaOU_j.d.ts +29 -0
  129. package/dist/logger-S7REWDNE.js +16 -0
  130. package/dist/logger-S7REWDNE.js.map +1 -0
  131. package/dist/media.d.ts +5 -0
  132. package/dist/media.js +41 -0
  133. package/dist/media.js.map +1 -0
  134. package/dist/mentions-2IHFVSHW.js +23 -0
  135. package/dist/mentions-2IHFVSHW.js.map +1 -0
  136. package/dist/mutes-EWAE5FZR.js +21 -0
  137. package/dist/mutes-EWAE5FZR.js.map +1 -0
  138. package/dist/notification-prefs-VPJDU7I6.js +21 -0
  139. package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
  140. package/dist/observability.d.ts +156 -0
  141. package/dist/observability.js +32 -0
  142. package/dist/observability.js.map +1 -0
  143. package/dist/profanity-adapter-NU2JQSLX.js +12 -0
  144. package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
  145. package/dist/queue-XE5BC75T.js +14 -0
  146. package/dist/queue-XE5BC75T.js.map +1 -0
  147. package/dist/rate-limit.d.ts +99 -0
  148. package/dist/rate-limit.js +14 -0
  149. package/dist/rate-limit.js.map +1 -0
  150. package/dist/registry-XIXDEPVI.js +31 -0
  151. package/dist/registry-XIXDEPVI.js.map +1 -0
  152. package/dist/reputation-JRL2YQHM.js +11 -0
  153. package/dist/reputation-JRL2YQHM.js.map +1 -0
  154. package/dist/routes.d.ts +43 -0
  155. package/dist/routes.js +12 -0
  156. package/dist/routes.js.map +1 -0
  157. package/dist/scheduled-CIQM57HT.js +20 -0
  158. package/dist/scheduled-CIQM57HT.js.map +1 -0
  159. package/dist/seo.d.ts +410 -0
  160. package/dist/seo.js +44 -0
  161. package/dist/seo.js.map +1 -0
  162. package/dist/settings-FOBIESPB.js +17 -0
  163. package/dist/settings-FOBIESPB.js.map +1 -0
  164. package/dist/spam-adapter-XX3G737Z.js +12 -0
  165. package/dist/spam-adapter-XX3G737Z.js.map +1 -0
  166. package/dist/strings-VAE47B2C.js +29 -0
  167. package/dist/strings-VAE47B2C.js.map +1 -0
  168. package/dist/templates-IFVJMCJ6.js +12 -0
  169. package/dist/templates-IFVJMCJ6.js.map +1 -0
  170. package/dist/types-TlsbXS0T.d.ts +871 -0
  171. package/package.json +129 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/schema/system.ts","../src/db/schema/media.ts","../src/db/schema/community.ts"],"sourcesContent":["import {\n type AnyPgColumn,\n boolean,\n index,\n integer,\n jsonb,\n pgEnum,\n pgTable,\n primaryKey,\n text,\n timestamp,\n unique,\n uuid,\n} from \"drizzle-orm/pg-core\";\n\nimport { npMedia } from \"./media.js\";\nimport {\n type NpBlockInstance,\n type NpNavItem,\n type NpRichTextContent,\n} from \"../../config/types.js\";\n\nexport const npUserRoleEnum = pgEnum(\"np_user_role\", [\n \"admin\",\n \"editor\",\n // 9.5: community moderator. Sits OUTSIDE the linear content-edit\n // hierarchy — a moderator handles community moderation (hide\n // comments, resolve reports, issue bans) but cannot author or edit\n // collection content. ROLE_HIERARCHY in config/types.ts intentionally\n // does not list this role; community-moderation paths check the role\n // explicitly via `principalCan()`.\n \"moderator\",\n \"author\",\n \"viewer\",\n]);\n\nexport const npRevisionStatusEnum = pgEnum(\"np_revision_status\", [\n \"draft\",\n \"published\",\n \"autosave\",\n]);\n\ntype NpRevisionSnapshot = Record<string, unknown> & {\n blocks?: NpBlockInstance[];\n content?: NpRichTextContent;\n};\n\nexport const npPasswordResetPurposeEnum = pgEnum(\"np_password_reset_purpose\", [\"invite\", \"reset\"]);\n\nexport const npUsers = pgTable(\"np_users\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n email: text(\"email\").notNull().unique(),\n password: text(\"password\").notNull(),\n name: text(\"name\").notNull(),\n role: npUserRoleEnum(\"role\").notNull(),\n /**\n * Phase 15.5 — super-admin flag. Bypasses per-site membership\n * checks; the super-admin can manage every site including\n * creating / deleting tenants. The flag is independent of\n * the per-site `role` enum (a super-admin still needs a\n * `role` field for non-multi-site contexts; multi-site\n * permissions check `is_super_admin OR site_membership`).\n */\n isSuperAdmin: boolean(\"is_super_admin\").default(false).notNull(),\n avatar: uuid(\"avatar\").references((): AnyPgColumn => npMedia.id),\n loginAttempts: integer(\"login_attempts\").default(0).notNull(),\n lockUntil: timestamp(\"lock_until\", { withTimezone: true, mode: \"date\" }),\n tokenVersion: integer(\"token_version\").default(0).notNull(),\n passwordResetTokenHash: text(\"password_reset_token_hash\"),\n passwordResetExpiresAt: timestamp(\"password_reset_expires_at\", {\n withTimezone: true,\n mode: \"date\",\n }),\n passwordResetPurpose: npPasswordResetPurposeEnum(\"password_reset_purpose\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n});\n\n/**\n * Phase 15.5 — per-site role grants. A user can hold a\n * different role on each site they're a member of (admin on\n * `acme`, editor on `partner-blog`, no role on `internal`).\n * Composite PK on (site_id, user_id) so each pair is unique;\n * the role enum reuses the existing `np_user_role` so the\n * concept stays consistent across the framework.\n *\n * `npUsers.role` becomes the \"global default role\" — used in\n * single-tenant contexts and as the fallback when a user has\n * no explicit membership on the current site. Most operators\n * will give cross-site users an explicit membership per\n * site they should access; the `is_super_admin` flag\n * separately bypasses the membership check entirely.\n */\nexport const npSiteMemberships = pgTable(\n \"np_site_memberships\",\n {\n siteId: text(\"site_id\").notNull(),\n userId: uuid(\"user_id\")\n .notNull()\n .references((): AnyPgColumn => npUsers.id, { onDelete: \"cascade\" }),\n role: npUserRoleEnum(\"role\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [primaryKey({ columns: [table.siteId, table.userId] })],\n);\n\n/**\n * Per-user OAuth identity links. A user can have one identity per provider\n * (composite unique on `(provider, providerUserId)` AND on `(userId,\n * provider)`). The first identity is created either when the OAuth\n * callback finds an existing user with the same email, or when a brand-\n * new user is auto-created from the OAuth profile (default role\n * `viewer`).\n */\nexport const npUserOAuthIdentities = pgTable(\n \"np_user_oauth_identities\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n userId: uuid(\"user_id\")\n .notNull()\n .references(() => npUsers.id, { onDelete: \"cascade\" }),\n provider: text(\"provider\").notNull(),\n providerUserId: text(\"provider_user_id\").notNull(),\n /** Free-form per-provider metadata (avatar URL, scopes granted, etc.). */\n metadata: jsonb(\"metadata\").$type<Record<string, unknown>>().default({}).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => ({\n providerSubjectUnique: unique(\"np_user_oauth_identities_provider_subject_unique\").on(\n table.provider,\n table.providerUserId,\n ),\n userProviderUnique: unique(\"np_user_oauth_identities_user_provider_unique\").on(\n table.userId,\n table.provider,\n ),\n userIdx: index(\"np_user_oauth_identities_user_idx\").on(table.userId),\n }),\n);\n\nexport const npSessions = pgTable(\"np_sessions\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n userId: uuid(\"user_id\")\n .notNull()\n .references(() => npUsers.id, { onDelete: \"cascade\" }),\n tokenHash: text(\"token_hash\").notNull(),\n userAgent: text(\"user_agent\"),\n ip: text(\"ip\"),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"date\" }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n});\n\nexport const npRevisions = pgTable(\n \"np_revisions\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n collection: text(\"collection\").notNull(),\n documentId: text(\"document_id\").notNull(),\n version: integer(\"version\").notNull(),\n status: npRevisionStatusEnum(\"status\").notNull(),\n snapshot: jsonb(\"snapshot\").$type<NpRevisionSnapshot>().notNull(),\n changedFields: text(\"changed_fields\").array().notNull(),\n authorId: uuid(\"author_id\").references(() => npUsers.id),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => ({\n documentVersionUnique: unique(\"np_revisions_document_id_version_unique\").on(\n table.documentId,\n table.version,\n ),\n collectionIdx: index(\"np_revisions_collection_idx\").on(table.collection),\n documentIdIdx: index(\"np_revisions_document_id_idx\").on(table.documentId),\n }),\n);\n\n/**\n * Phase 15.4 — settings are scoped per site so each tenant\n * has its own active theme, theme tokens, SEO config, etc.\n * Single-tenant deployments leave every row at\n * `site_id = 'default'`, matching the framework's\n * default-site invariant. Composite PK on (site_id, key) so\n * the same key (e.g. `activeTheme`) can take different\n * values per tenant.\n */\nexport const npSettings = pgTable(\n \"np_settings\",\n {\n siteId: text(\"site_id\").default(\"default\").notNull(),\n key: text(\"key\").notNull(),\n value: jsonb(\"value\").$type<unknown>().notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedBy: uuid(\"updated_by\").references(() => npUsers.id),\n },\n (table) => [primaryKey({ columns: [table.siteId, table.key] })],\n);\n\n/**\n * Slug history for collections that declare `slugField`. Every\n * slug change writes a row mapping the previous slug to the\n * current one; the public-site catch-all reads it on 404 and\n * 301-redirects so old URLs (search-engine indices, external\n * links, bookmarks) keep working after a rename.\n *\n * Indexed by `(site_id, collection, old_slug)` because the read\n * path is \"I just got a 404 for this slug, where did it go?\" —\n * point lookups on that triple. Multiple rows can share the same\n * `(site_id, collection, document_id)` over time as a doc gets\n * renamed repeatedly; the catch-all walks the chain `oldSlug →\n * newSlug` to resolve to the current target.\n */\nexport const npSlugHistory = pgTable(\n \"np_slug_history\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n siteId: text(\"site_id\").default(\"default\").notNull(),\n collection: text(\"collection\").notNull(),\n documentId: text(\"document_id\").notNull(),\n oldSlug: text(\"old_slug\").notNull(),\n newSlug: text(\"new_slug\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" })\n .defaultNow()\n .notNull(),\n },\n (table) => [\n index(\"np_slug_history_lookup_idx\").on(table.siteId, table.collection, table.oldSlug),\n index(\"np_slug_history_doc_idx\").on(table.siteId, table.collection, table.documentId),\n ],\n);\n\n/**\n * Phase 15.4 — navigation is scoped per site too. Same model\n * as settings: composite uniqueness on (site_id, location)\n * lets each tenant own its own header / footer menus.\n */\nexport const npNavigation = pgTable(\n \"np_navigation\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n siteId: text(\"site_id\").default(\"default\").notNull(),\n location: text(\"location\").notNull(),\n items: jsonb(\"items\").$type<NpNavItem[]>().notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedBy: uuid(\"updated_by\").references(() => npUsers.id),\n },\n (table) => [unique(\"np_navigation_site_location_idx\").on(table.siteId, table.location)],\n);\n\n/**\n * Phase D — UI string admin overrides. Plugins and themes\n * register translation bundles via `addStrings()` (Phase 12.5);\n * admins layer overrides on top via this table without\n * touching plugin/theme code. Composite PK on\n * (site_id, locale, key) makes per-tenant overrides natural —\n * \"acme\" and \"default\" can each override the same plugin's\n * \"Read more\" string differently.\n *\n * `value` is nullable so an admin can explicitly mark a key\n * as \"fall back to bundle\" without deleting the row (useful\n * for audit-trail UIs that want to show \"this WAS overridden\n * but the operator reverted it\"). The runtime treats null\n * the same as no row for resolution purposes.\n */\nexport const npStringOverrides = pgTable(\n \"np_string_overrides\",\n {\n siteId: text(\"site_id\").default(\"default\").notNull(),\n locale: text(\"locale\").notNull(),\n key: text(\"key\").notNull(),\n value: text(\"value\"),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedBy: uuid(\"updated_by\").references(() => npUsers.id),\n },\n (table) => [primaryKey({ columns: [table.siteId, table.locale, table.key] })],\n);\n\n/**\n * Phase 15.1 — multi-site model. One row per tenant. The\n * framework auto-creates a `default` site at boot when the\n * table is empty so single-tenant installs keep working\n * without operator intervention. Subsequent sites are added\n * via the super-admin UI (15.3) — the framework treats\n * additional sites as additive: they share users, plugins,\n * and theme code at install time, but each site has its own\n * collection content, navigation, and settings.\n *\n * `hostname` is nullable so the default site can match\n * \"anything that doesn't have an explicit host route\". When\n * a request's `Host` header matches a non-default site's\n * hostname, that site wins; otherwise the default site is\n * used. Multi-domain sites (apex + www) need separate rows\n * pointing at the same `id` — that's a 15.x follow-up;\n * v15.1 is one-hostname-per-site.\n */\nexport const npSites = pgTable(\n \"np_sites\",\n {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n hostname: text(\"hostname\"),\n description: text(\"description\"),\n settings: jsonb(\"settings\").$type<Record<string, unknown>>().default({}).notNull(),\n isDefault: boolean(\"is_default\").default(false).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [unique(\"np_sites_hostname_idx\").on(table.hostname)],\n);\n\n/**\n * G.1 — lean meta row. Plugin config moved to `np_settings` rows\n * keyed by `plugin.config:<id>` (see decision E in\n * `docs/design/plugin-config-auto-form.md`); the legacy `config`\n * jsonb column was dropped. Reads / writes go through\n * `getPluginConfig` / `setPluginConfig` in the config module.\n */\nexport const npPlugins = pgTable(\"np_plugins\", {\n id: text(\"id\").primaryKey(),\n enabled: boolean(\"enabled\").default(true).notNull(),\n installedAt: timestamp(\"installed_at\", { withTimezone: true, mode: \"date\" })\n .defaultNow()\n .notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n});\n\n/**\n * Phase 17 — plugin K/V storage with multi-tenant scope.\n *\n * The PK is `(plugin_id, site_id, key)`; `site_id` defaults to\n * `_global_` so single-site deploys (and pre-Phase-17 callers\n * that don't pass a site) keep their non-tenant behavior.\n * Plugin context auto-scopes reads/writes to the current site,\n * so plugin authors don't have to think about it — every plugin\n * operating inside a request automatically gets a per-site\n * keyspace, while background workers / scripts (no resolved\n * site) share the `_global_` space.\n */\nexport const NP_GLOBAL_PLUGIN_SITE_ID = \"_global_\";\n\nexport const npPluginStorage = pgTable(\n \"np_plugin_storage\",\n {\n pluginId: text(\"plugin_id\").notNull(),\n siteId: text(\"site_id\").default(NP_GLOBAL_PLUGIN_SITE_ID).notNull(),\n key: text(\"key\").notNull(),\n value: jsonb(\"value\").$type<unknown>().notNull(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"date\" }),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => ({\n pk: primaryKey({ columns: [table.pluginId, table.siteId, table.key] }),\n pluginIdx: index(\"np_plugin_storage_plugin_id_idx\").on(table.pluginId),\n siteIdx: index(\"np_plugin_storage_site_idx\").on(table.siteId),\n }),\n);\n\n/**\n * Phase 19 — worker liveness heartbeat. Each worker process\n * upserts a row keyed on its self-generated id (hostname + pid)\n * every `WORKER_HEARTBEAT_INTERVAL_MS` (30s). Admin reads this\n * to tell whether the queue actually has a process draining\n * jobs — without it the only signal was \"Pending stays high\n * while Completed doesn't grow,\" which a stuck DB or a stopped\n * worker look identical from outside.\n *\n * Stale rows (no heartbeat for > 90s) are reported as\n * `unhealthy`; they survive in the table for forensic review\n * until an operator GCs them or a fresh worker reuses the id.\n */\nexport const npWorkerHeartbeats = pgTable(\"np_worker_heartbeats\", {\n id: text(\"id\").primaryKey(),\n status: text(\"status\").default(\"running\").notNull(),\n startedAt: timestamp(\"started_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n lastSeenAt: timestamp(\"last_seen_at\", { withTimezone: true, mode: \"date\" })\n .defaultNow()\n .notNull(),\n /** Free-form metadata (worker version, hostname, env). */\n meta: jsonb(\"meta\").$type<Record<string, unknown>>().default({}).notNull(),\n});\n\n/**\n * Phase 20.3 — per-job log capture. Each row is one structured\n * log entry recorded during a handler invocation. The framework\n * wraps every `boss.work()` callback in an AsyncLocalStorage\n * context so handlers calling `recordJobLog()` (or going through\n * the framework `getLogger()`) get their entries automatically\n * stamped with the running job's id.\n *\n * The `job_id` column is `text` (not `uuid`) because pg-boss job\n * ids are returned as strings and we want the relationship to\n * mirror what's surfaced to the admin without translation.\n *\n * Indexes target the two queries the admin will run:\n * - \"logs for this job\" → (job_id, created_at)\n * - \"prune logs older than X\" → (created_at)\n */\nexport const npJobLogs = pgTable(\n \"np_job_logs\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n jobId: text(\"job_id\").notNull(),\n level: text(\"level\").notNull(),\n message: text(\"message\").notNull(),\n context: jsonb(\"context\").$type<Record<string, unknown> | null>(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n index(\"np_job_logs_job_idx\").on(table.jobId, table.createdAt),\n index(\"np_job_logs_created_idx\").on(table.createdAt),\n ],\n);\n","import {\n type AnyPgColumn,\n bigint,\n index,\n integer,\n jsonb,\n pgEnum,\n pgTable,\n text,\n timestamp,\n uuid,\n} from \"drizzle-orm/pg-core\";\n\nimport { npMembers } from \"./community.js\";\nimport { npUsers } from \"./system.js\";\nimport { type NpRichTextContent } from \"../../config/types.js\";\n\nexport const npMediaStatusEnum = pgEnum(\"np_media_status\", [\n \"processing\",\n \"ready\",\n \"error\",\n]);\n\ntype NpMediaFocalPoint = {\n x: number;\n y: number;\n};\n\ntype NpMediaSizes = Record<string, Record<string, unknown>>;\n\nexport const npMediaFolders = pgTable(\"np_media_folders\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n name: text(\"name\").notNull(),\n parentId: uuid(\"parent_id\").references((): AnyPgColumn => npMediaFolders.id),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" })\n .defaultNow()\n .notNull(),\n});\n\nexport const npMedia = pgTable(\n \"np_media\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n filename: text(\"filename\").notNull(),\n originalFilename: text(\"original_filename\").notNull(),\n mimeType: text(\"mime_type\").notNull(),\n filesize: bigint(\"filesize\", { mode: \"number\" }).notNull(),\n width: integer(\"width\"),\n height: integer(\"height\"),\n alt: text(\"alt\"),\n caption: jsonb(\"caption\").$type<NpRichTextContent>(),\n focalPoint: jsonb(\"focal_point\").$type<NpMediaFocalPoint>(),\n sizes: jsonb(\"sizes\").$type<NpMediaSizes>(),\n storageKey: text(\"storage_key\").notNull(),\n hash: text(\"hash\").notNull(),\n status: npMediaStatusEnum(\"status\").notNull(),\n folderId: uuid(\"folder_id\").references(() => npMediaFolders.id),\n uploadedBy: uuid(\"uploaded_by\").references((): AnyPgColumn => npUsers.id),\n /**\n * Set when a member uploaded the row instead of a staff user\n * (Phase 9.7j). Mutually exclusive with `uploadedBy`: a row\n * has exactly one uploader. Member-side moderation tools key\n * off this column to filter \"uploads I should review.\"\n * `ON DELETE SET NULL` so a member account deletion doesn't\n * cascade-delete their uploads — staff still need them for\n * the audit trail (just like `member_author_id` on\n * collection tables).\n */\n uploadedByMemberId: uuid(\"uploaded_by_member_id\").references(\n (): AnyPgColumn => npMembers.id,\n { onDelete: \"set null\" },\n ),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" })\n .defaultNow()\n .notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" })\n .defaultNow()\n .notNull(),\n deletedAt: timestamp(\"deleted_at\", { withTimezone: true, mode: \"date\" }),\n },\n (table) => ({\n hashIdx: index(\"np_media_hash_idx\").on(table.hash),\n statusIdx: index(\"np_media_status_idx\").on(table.status),\n uploadedByMemberIdx: index(\"np_media_uploaded_by_member_idx\").on(\n table.uploadedByMemberId,\n ),\n }),\n);\n\nexport const npMediaRefs = pgTable(\n \"np_media_refs\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n mediaId: uuid(\"media_id\")\n .notNull()\n .references(() => npMedia.id, { onDelete: \"cascade\" }),\n collection: text(\"collection\").notNull(),\n documentId: text(\"document_id\").notNull(),\n field: text(\"field\").notNull(),\n },\n (table) => ({\n mediaIdIdx: index(\"np_media_refs_media_id_idx\").on(table.mediaId),\n documentIdIdx: index(\"np_media_refs_document_id_idx\").on(table.documentId),\n }),\n);\n","import {\n type AnyPgColumn,\n boolean,\n index,\n integer,\n jsonb,\n pgEnum,\n pgTable,\n primaryKey,\n text,\n timestamp,\n unique,\n uuid,\n} from \"drizzle-orm/pg-core\";\n\nimport { npMedia } from \"./media.js\";\nimport { npUsers } from \"./system.js\";\n\n/**\n * Member-side schema: public site visitors who can register, log in,\n * comment, react, follow, etc. Deliberately separate from `np_users`\n * (CMS staff) — separate cookie family, separate JWT audience, no\n * `role` column on the member table itself. Scoped moderator authority\n * is granted via `np_member_roles` instead. See `docs/design/community-design.md` (frozen design rationale) or `docs/community.md` (live behavior).\n */\n\n/**\n * Phase 21.7 — `imported` is a member created by the WordPress\n * importer to attribute archived guest comments. Imported members\n * cannot log in (no usable password set) and don't fire community\n * notifications when content tags them. Default themes render the\n * member's handle with an `(imported)` suffix so visitors can tell\n * archived discussion apart from live activity.\n */\nexport const npMemberStatusEnum = pgEnum(\"np_member_status\", [\n \"active\",\n \"pending\",\n \"suspended\",\n \"deleted\",\n \"imported\",\n]);\n\nexport const npBanScopeEnum = pgEnum(\"np_ban_scope\", [\"site\", \"category\", \"collection\"]);\nexport const npBanKindEnum = pgEnum(\"np_ban_kind\", [\"temporary\", \"permanent\"]);\n\n/**\n * Comment lifecycle status.\n * - `visible` — public.\n * - `pending` — awaiting moderation. Used by the spam / profanity\n * adapters when a verdict comes back as `flag` (9.7n).\n * - `hidden` — taken down by a mod; row stays for restore + audit.\n * - `deleted` — soft-delete by the author or post-cascade.\n */\nexport const npCommentStatusEnum = pgEnum(\"np_comment_status\", [\n \"visible\",\n \"pending\",\n \"hidden\",\n \"deleted\",\n]);\n\n/**\n * Type column for `np_member_roles.scope_type`. Polymorphic across the\n * community surface so the same grants table covers site-wide,\n * per-category, per-collection, and per-thread roles.\n */\nexport const npMemberRoleScopeEnum = pgEnum(\"np_member_role_scope\", [\n \"site\",\n \"category\",\n \"collection\",\n \"thread\",\n]);\n\nexport const npMembers = pgTable(\n \"np_members\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n handle: text(\"handle\").notNull().unique(),\n email: text(\"email\").notNull().unique(),\n emailVerified: boolean(\"email_verified\").default(false).notNull(),\n /** Argon2 hash. Nullable so SSO-only members can exist without a password. */\n password: text(\"password\"),\n displayName: text(\"display_name\").notNull(),\n avatar: uuid(\"avatar\").references((): AnyPgColumn => npMedia.id),\n bio: text(\"bio\"),\n status: npMemberStatusEnum(\"status\").default(\"pending\").notNull(),\n reputation: integer(\"reputation\").default(0).notNull(),\n loginAttempts: integer(\"login_attempts\").default(0).notNull(),\n lockUntil: timestamp(\"lock_until\", { withTimezone: true, mode: \"date\" }),\n /** Bumped to invalidate every issued JWT (logout-everywhere, password reset). */\n tokenVersion: integer(\"token_version\").default(0).notNull(),\n passwordResetTokenHash: text(\"password_reset_token_hash\"),\n passwordResetExpiresAt: timestamp(\"password_reset_expires_at\", {\n withTimezone: true,\n mode: \"date\",\n }),\n emailVerifyTokenHash: text(\"email_verify_token_hash\"),\n emailVerifyExpiresAt: timestamp(\"email_verify_expires_at\", {\n withTimezone: true,\n mode: \"date\",\n }),\n /** Plugin-extensible bag — preferences, custom profile fields, etc. */\n meta: jsonb(\"meta\").$type<Record<string, unknown>>().default({}).notNull(),\n /**\n * Phase 16.3 — per-member notification preferences. Shape:\n * { disabled?: string[] } — kinds the member opted out of\n * { digest?: \"off\"|\"daily\"|\"weekly\" } — email digest cadence (16.4)\n * Empty default = every kind enabled, no email digest.\n */\n notificationPrefs: jsonb(\"notification_prefs\")\n .$type<Record<string, unknown>>()\n .default({})\n .notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [index(\"np_members_status_idx\").on(table.status)],\n);\n\nexport const npMemberSessions = pgTable(\"np_member_sessions\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n memberId: uuid(\"member_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n tokenHash: text(\"token_hash\").notNull(),\n userAgent: text(\"user_agent\"),\n ip: text(\"ip\"),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"date\" }).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n});\n\n/**\n * Per-member OAuth identity links. Mirrors `np_user_oauth_identities`\n * for the staff side (Phase 9.6a) but resolves to `np_members`\n * instead of `np_users`. The first row is created either when an\n * OAuth callback finds an existing member with the same email, or\n * when a brand-new member is auto-provisioned from the profile\n * (status=`active`, no password).\n *\n * `subject` is the provider's stable user id (GitHub `id`, Google\n * `sub`, etc.) — naming kept from the 9.1 placeholder schema for\n * backward compat. The staff equivalent calls the same column\n * `provider_user_id`; both serve the same role.\n */\nexport const npMemberIdentities = pgTable(\n \"np_member_identities\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n memberId: uuid(\"member_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n provider: text(\"provider\").notNull(),\n subject: text(\"subject\").notNull(),\n email: text(\"email\"),\n /** Free-form per-provider metadata (avatar URL, scopes granted, etc.). */\n metadata: jsonb(\"metadata\").$type<Record<string, unknown>>().default({}).notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n unique(\"np_member_identities_provider_subject_uq\").on(table.provider, table.subject),\n unique(\"np_member_identities_member_provider_uq\").on(table.memberId, table.provider),\n index(\"np_member_identities_member_idx\").on(table.memberId),\n ],\n);\n\n/**\n * Polymorphic role grants. A member with a row here can act as that\n * role within the indicated scope. `scope_id` is null when\n * `scope_type='site'`. The `(member, role, scope_type, scope_id)`\n * uniqueness keeps grants idempotent; `expires_at` is honored by\n * `memberCan()` so time-boxed promotions are possible.\n */\nexport const npMemberRoles = pgTable(\n \"np_member_roles\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n memberId: uuid(\"member_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n role: text(\"role\").notNull(),\n scopeType: npMemberRoleScopeEnum(\"scope_type\").notNull(),\n /** Nullable for `scope_type='site'`. Otherwise an opaque string id. */\n scopeId: text(\"scope_id\"),\n /**\n * Phase 18 — the tenant the grant applies on. For\n * `scope_type='site'` this column IS the site identifier\n * (`scope_id` stays null because site is the root scope).\n * For category / collection / thread grants, `site_id` says\n * which tenant's category/collection/thread this row\n * targets — the same slug exists on every site.\n */\n siteId: text(\"site_id\").default(\"default\").notNull(),\n grantedBy: uuid(\"granted_by\").references(() => npUsers.id),\n grantedAt: timestamp(\"granted_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"date\" }),\n },\n (table) => [\n // Two indexes mirror the two access patterns: \"what can this member\n // do?\" (memberId scan) and \"who mods this scope?\" (scope scan).\n index(\"np_member_roles_member_idx\").on(table.memberId),\n index(\"np_member_roles_scope_idx\").on(table.scopeType, table.scopeId),\n index(\"np_member_roles_site_idx\").on(table.siteId, table.memberId),\n // `scope_id` is null for site-wide grants. NULLS NOT\n // DISTINCT makes two null `scope_id`s collide so the\n // unique constraint enforces \"one grant per (member, role,\n // scope, site).\" `site_id` widens the key so the same\n // member can hold the same role on different tenants.\n unique(\"np_member_roles_grant_uq\")\n .on(table.memberId, table.role, table.scopeType, table.scopeId, table.siteId)\n .nullsNotDistinct(),\n ],\n);\n\n/**\n * Member bans. Scoped: a category-mod can ban a member from their\n * category only; a `community-mod` or staff `moderator` can issue\n * site-wide bans. `memberCan()` short-circuits to deny when an active\n * (unexpired) ban matches the action's target scope chain.\n *\n * `byUserId` records the staff issuer; `byMemberId` records when a\n * member-mod (e.g. category-mod) issued the ban. Exactly one is set.\n */\n/**\n * Polymorphic comment table — `target_type` is the collection slug\n * (e.g. `\"posts\"`), `target_id` the document id within that\n * collection. One row per comment regardless of which collection it\n * lives under, indexed for the typical \"list comments under doc X\"\n * read.\n *\n * Bodies are stored twice: `body_md` is the canonical user input,\n * `body_html` is the rendered + sanitised HTML the renderer ships\n * to browsers. We re-render on edit; we never trust the html column\n * to be HTML-safe based on incoming requests — see\n * `community/markdown.ts` for the renderer.\n */\nexport const npComments = pgTable(\n \"np_comments\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n targetType: text(\"target_type\").notNull(),\n targetId: uuid(\"target_id\").notNull(),\n parentId: uuid(\"parent_id\").references((): AnyPgColumn => npComments.id, {\n onDelete: \"cascade\",\n }),\n memberId: uuid(\"member_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n bodyMd: text(\"body_md\").notNull(),\n bodyHtml: text(\"body_html\").notNull(),\n status: npCommentStatusEnum(\"status\").default(\"visible\").notNull(),\n hiddenByUserId: uuid(\"hidden_by_user_id\").references(() => npUsers.id),\n hiddenByMemberId: uuid(\"hidden_by_member_id\").references((): AnyPgColumn => npMembers.id),\n hiddenReason: text(\"hidden_reason\"),\n editedAt: timestamp(\"edited_at\", { withTimezone: true, mode: \"date\" }),\n /**\n * Phase 18 — site this comment belongs to. Filled at insert\n * time from the target document's site (canonical) so a\n * forged request resolver can't smuggle a comment into the\n * wrong site. Defaults to `'default'` for legacy single-\n * tenant rows so the migration backfill is a no-op.\n */\n siteId: text(\"site_id\").default(\"default\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n index(\"np_comments_target_idx\").on(table.targetType, table.targetId, table.createdAt),\n index(\"np_comments_member_idx\").on(table.memberId, table.createdAt),\n index(\"np_comments_site_idx\").on(table.siteId, table.createdAt),\n ],\n);\n\n/**\n * Polymorphic reactions. `target_type` is the surface — only\n * `'comment'` is wired today; `'thread'` / `'reply'` are reserved\n * for a future threads schema (the forum plugin shipped without\n * one, reusing `np_comments` under the `discussions` collection).\n * `kind` is configurable per site — default vocabulary in v1 is\n * just `'like'`. The unique constraint enforces \"one reaction-of-\n * kind per member per target,\" so toggling a like is an upsert /\n * delete.\n */\nexport const npReactions = pgTable(\n \"np_reactions\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n targetType: text(\"target_type\").notNull(),\n targetId: uuid(\"target_id\").notNull(),\n memberId: uuid(\"member_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n kind: text(\"kind\").notNull(),\n /** Phase 18 — site this reaction belongs to (derived from target). */\n siteId: text(\"site_id\").default(\"default\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n index(\"np_reactions_target_idx\").on(table.targetType, table.targetId),\n index(\"np_reactions_site_idx\").on(table.siteId),\n unique(\"np_reactions_unique\").on(table.targetType, table.targetId, table.memberId, table.kind),\n ],\n);\n\n/**\n * Follow graph. Polymorphic over what's being followed:\n * - `member` — target_id is `np_members.id` as a string\n * - `thread` — reserved; no thread schema today (forum plugin\n * reuses `np_comments` so there's nothing to follow per-thread)\n * - `tag` — target_id is the tag slug (no FK; tags are strings)\n *\n * `target_id` is `text` rather than `uuid` so all three kinds share\n * one column. Cascading on a polymorphic id isn't possible in plain\n * SQL; the soft-delete pattern on `np_members` keeps follows pointing\n * at a still-valid (if anonymised) row.\n */\nexport const npFollows = pgTable(\n \"np_follows\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n followerId: uuid(\"follower_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n targetType: text(\"target_type\").notNull(),\n targetId: text(\"target_id\").notNull(),\n /**\n * Phase 18 — site the follow happened on. The same global\n * member can follow on multiple sites and each row scopes\n * to where the click happened (so site-scoped notifications\n * + activity feeds don't leak cross-tenant). The unique\n * key is widened to include site_id so the same follower\n * can have parallel follow rows under different tenants.\n */\n siteId: text(\"site_id\").default(\"default\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n index(\"np_follows_target_idx\").on(table.targetType, table.targetId),\n index(\"np_follows_site_idx\").on(table.siteId),\n unique(\"np_follows_unique\").on(\n table.followerId,\n table.targetType,\n table.targetId,\n table.siteId,\n ),\n ],\n);\n\n/**\n * Phase 16.1 — member-to-member mute. One-directional: A muting\n * B means A doesn't see B's comments and doesn't get\n * notifications about B's actions (replies, reactions, follows\n * targeted at A's content). B isn't told and can keep posting\n * normally — Twitter-style soft-block.\n *\n * Self-mute is rejected at the API layer. The composite PK on\n * `(memberId, targetId)` enforces idempotence: muting the same\n * person twice is a no-op rather than two rows.\n *\n * Distinct from `np_bans` — bans are staff-issued and global\n * (block writes). Mutes are member-issued and personal (hide\n * reads).\n */\nexport const npMemberMutes = pgTable(\n \"np_member_mutes\",\n {\n memberId: uuid(\"member_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n targetId: uuid(\"target_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n /**\n * Phase 18 — site the mute applies to. A muter can choose\n * to silence someone on one tenant without affecting their\n * other tenants. PK is widened to include site_id so the\n * same `(member, target)` pair can hold parallel rows per\n * site.\n */\n siteId: text(\"site_id\").default(\"default\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n primaryKey({ columns: [table.memberId, table.targetId, table.siteId] }),\n index(\"np_member_mutes_target_idx\").on(table.targetId),\n ],\n);\n\n/**\n * Per-member notification inbox. `kind` is a free-form discriminator\n * (e.g. `'comment.reply'`, `'reaction.received'`, `'follow.received'`)\n * paired with a `payload` whose shape depends on the kind — the\n * recipient's UI renders based on those.\n *\n * Indexed on `(member_id, read_at, created_at)` to cover both the\n * unread-count probe and the recent-list paging that an inbox UI uses.\n */\nexport const npNotifications = pgTable(\n \"np_notifications\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n memberId: uuid(\"member_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n kind: text(\"kind\").notNull(),\n payload: jsonb(\"payload\").$type<Record<string, unknown>>().default({}).notNull(),\n readAt: timestamp(\"read_at\", { withTimezone: true, mode: \"date\" }),\n /**\n * Phase 18 — site this notification belongs to. A member\n * who's active on multiple tenants gets one inbox per site\n * (the inbox API filters by current site) so cross-tenant\n * activity doesn't bleed into the wrong site's UI.\n */\n siteId: text(\"site_id\").default(\"default\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n index(\"np_notifications_inbox_idx\").on(table.memberId, table.readAt, table.createdAt),\n index(\"np_notifications_site_inbox_idx\").on(table.siteId, table.memberId, table.readAt),\n ],\n);\n\n/**\n * Member-filed reports against community content. `target_type` is\n * `'comment' | 'thread' | 'reply' | 'member'` — anything a member can\n * report. `resolved_at` flags closed cases; the unresolved index\n * powers the moderation queue's \"unread first\" view.\n *\n * `resolved_by_user_id` and `resolved_by_member_id` are mutually\n * exclusive — staff resolutions populate the user, member-mod\n * resolutions populate the member.\n */\nexport const npReports = pgTable(\n \"np_reports\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n reporterId: uuid(\"reporter_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n targetType: text(\"target_type\").notNull(),\n targetId: text(\"target_id\").notNull(),\n reason: text(\"reason\").notNull(),\n resolvedAt: timestamp(\"resolved_at\", { withTimezone: true, mode: \"date\" }),\n resolvedByUserId: uuid(\"resolved_by_user_id\").references(() => npUsers.id),\n resolvedByMemberId: uuid(\"resolved_by_member_id\").references((): AnyPgColumn => npMembers.id),\n resolution: text(\"resolution\"),\n /**\n * Phase 18 — site this report belongs to. The mod queue\n * is per-site so a category-mod on tenant A doesn't see\n * tenant B's reports.\n */\n siteId: text(\"site_id\").default(\"default\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n index(\"np_reports_queue_idx\").on(table.resolvedAt, table.createdAt),\n index(\"np_reports_target_idx\").on(table.targetType, table.targetId),\n index(\"np_reports_site_queue_idx\").on(table.siteId, table.resolvedAt),\n ],\n);\n\n/**\n * Append-only moderation audit log. Every hide / restore / ban / role\n * grant write should append a row so an admin can answer \"who took\n * this action and when?\" without diffing logs.\n *\n * `actor_kind` distinguishes staff / member-mod / system writes\n * (e.g. an automated revocation when a member soft-deletes their\n * account). `target_id` is `text` because some actions target string\n * ids — like `\"posts\"` for a `collection-mod` grant scope.\n */\nexport const npAuditEvents = pgTable(\n \"np_audit_events\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n actorKind: text(\"actor_kind\").notNull(),\n actorUserId: uuid(\"actor_user_id\").references(() => npUsers.id),\n actorMemberId: uuid(\"actor_member_id\").references((): AnyPgColumn => npMembers.id),\n action: text(\"action\").notNull(),\n targetType: text(\"target_type\"),\n targetId: text(\"target_id\"),\n payload: jsonb(\"payload\").$type<Record<string, unknown>>().default({}).notNull(),\n /**\n * Phase 17 — site-scoped audit. Filled by `recordAuditEvent`\n * from the current request's site (the multi-site resolver).\n * Nullable for events that don't belong to a single site\n * (super-admin actions, background jobs, scripts).\n */\n siteId: text(\"site_id\"),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n index(\"np_audit_target_idx\").on(table.targetType, table.targetId, table.createdAt),\n index(\"np_audit_actor_user_idx\").on(table.actorUserId, table.createdAt),\n index(\"np_audit_actor_member_idx\").on(table.actorMemberId, table.createdAt),\n index(\"np_audit_site_idx\").on(table.siteId, table.createdAt),\n ],\n);\n\nexport const npBans = pgTable(\n \"np_bans\",\n {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n memberId: uuid(\"member_id\")\n .notNull()\n .references(() => npMembers.id, { onDelete: \"cascade\" }),\n scopeType: npBanScopeEnum(\"scope_type\").notNull(),\n scopeId: text(\"scope_id\"),\n kind: npBanKindEnum(\"kind\").notNull(),\n expiresAt: timestamp(\"expires_at\", { withTimezone: true, mode: \"date\" }),\n reason: text(\"reason\"),\n byUserId: uuid(\"by_user_id\").references(() => npUsers.id),\n byMemberId: uuid(\"by_member_id\").references((): AnyPgColumn => npMembers.id),\n /**\n * Phase 18 — the tenant this ban applies to. Pre-Phase 18\n * `scope_type='site'` rows had `scope_id=null` because\n * \"site\" was the singular root scope; with multi-tenancy\n * the column tells `assertNotBanned` WHICH site the ban\n * blocks writes on. Category / collection scopes resolve\n * per-site too — the same `posts` collection slug exists\n * on every tenant.\n */\n siteId: text(\"site_id\").default(\"default\").notNull(),\n createdAt: timestamp(\"created_at\", { withTimezone: true, mode: \"date\" }).defaultNow().notNull(),\n },\n (table) => [\n index(\"np_bans_member_scope_idx\").on(table.memberId, table.scopeType, table.scopeId),\n index(\"np_bans_active_idx\").on(table.memberId, table.expiresAt),\n index(\"np_bans_site_idx\").on(table.siteId, table.memberId),\n ],\n);\n"],"mappings":"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","names":["boolean","index","integer","jsonb","pgEnum","pgTable","primaryKey","text","timestamp","unique","uuid","index","integer","jsonb","pgEnum","pgTable","text","timestamp","uuid","pgEnum","pgTable","uuid","text","timestamp","integer","jsonb","index","pgEnum","pgTable","uuid","text","boolean","integer","timestamp","primaryKey","jsonb","unique","index"]}
@@ -0,0 +1,150 @@
1
+ import {
2
+ getI18nConfig
3
+ } from "./chunk-4ZLMEKFX.js";
4
+
5
+ // src/i18n/locale-resolver.ts
6
+ function resolveLocale(input = {}) {
7
+ const config = getI18nConfig();
8
+ if (!config) return null;
9
+ const configured = new Set(config.locales);
10
+ if (input.pathname) {
11
+ const segments = input.pathname.split("/").filter(Boolean);
12
+ const first = segments[0];
13
+ if (first && configured.has(first)) {
14
+ const remaining = segments.slice(1).join("/");
15
+ const without = remaining.length > 0 ? `/${remaining}` : "/";
16
+ return { locale: first, source: "path", pathnameWithoutLocale: without };
17
+ }
18
+ }
19
+ const fromHeader = matchAcceptLanguage(input.acceptLanguage, configured);
20
+ if (fromHeader) {
21
+ return { locale: fromHeader, source: "header", pathnameWithoutLocale: input.pathname };
22
+ }
23
+ return {
24
+ locale: config.defaultLocale,
25
+ source: "default",
26
+ pathnameWithoutLocale: input.pathname
27
+ };
28
+ }
29
+ function getCurrentLocale(input = {}) {
30
+ const resolved = resolveLocale(input);
31
+ if (resolved) return resolved.locale;
32
+ const config = getI18nConfig();
33
+ return config?.defaultLocale ?? "en";
34
+ }
35
+ function parseAcceptLanguage(header) {
36
+ return header.split(",").map((entry) => {
37
+ const [tagRaw, ...params] = entry.trim().split(";");
38
+ if (!tagRaw) return null;
39
+ const tag = tagRaw.trim().toLowerCase();
40
+ let quality = 1;
41
+ for (const param of params) {
42
+ const match = /^\s*q\s*=\s*([0-9.]+)\s*$/i.exec(param);
43
+ if (match) {
44
+ const parsed = Number(match[1]);
45
+ if (Number.isFinite(parsed)) quality = parsed;
46
+ }
47
+ }
48
+ return { tag, quality };
49
+ }).filter((entry) => entry !== null && entry.quality > 0).sort((a, b) => b.quality - a.quality);
50
+ }
51
+ function matchAcceptLanguage(header, configured) {
52
+ if (!header) return null;
53
+ const parsed = parseAcceptLanguage(header);
54
+ const lowerToActual = /* @__PURE__ */ new Map();
55
+ for (const loc of configured) lowerToActual.set(loc.toLowerCase(), loc);
56
+ for (const { tag } of parsed) {
57
+ if (lowerToActual.has(tag)) return lowerToActual.get(tag);
58
+ if (tag === "*") continue;
59
+ const primary = tag.split("-")[0];
60
+ if (primary && lowerToActual.has(primary)) return lowerToActual.get(primary);
61
+ }
62
+ return null;
63
+ }
64
+
65
+ // src/i18n/direction.ts
66
+ function getLocaleDirection(locale) {
67
+ if (typeof locale !== "string" || locale.length === 0) return "ltr";
68
+ try {
69
+ const parsed = new Intl.Locale(locale);
70
+ const dir = parsed.textInfo?.direction;
71
+ return dir === "rtl" ? "rtl" : "ltr";
72
+ } catch {
73
+ return "ltr";
74
+ }
75
+ }
76
+
77
+ // src/i18n/format.ts
78
+ function resolveLocale2(explicit) {
79
+ if (explicit && explicit.length > 0) return explicit;
80
+ return getI18nConfig()?.defaultLocale ?? "en";
81
+ }
82
+ var numberFormatCache = /* @__PURE__ */ new Map();
83
+ var dateFormatCache = /* @__PURE__ */ new Map();
84
+ var relativeTimeFormatCache = /* @__PURE__ */ new Map();
85
+ function getNumberFormatter(locale, options) {
86
+ const key = `${locale}|${options ? JSON.stringify(options) : ""}`;
87
+ let cached = numberFormatCache.get(key);
88
+ if (!cached) {
89
+ cached = new Intl.NumberFormat(locale, options);
90
+ numberFormatCache.set(key, cached);
91
+ }
92
+ return cached;
93
+ }
94
+ function getDateFormatter(locale, options) {
95
+ const key = `${locale}|${options ? JSON.stringify(options) : ""}`;
96
+ let cached = dateFormatCache.get(key);
97
+ if (!cached) {
98
+ cached = new Intl.DateTimeFormat(locale, options);
99
+ dateFormatCache.set(key, cached);
100
+ }
101
+ return cached;
102
+ }
103
+ function getRelativeTimeFormatter(locale, options) {
104
+ const key = `${locale}|${options ? JSON.stringify(options) : ""}`;
105
+ let cached = relativeTimeFormatCache.get(key);
106
+ if (!cached) {
107
+ cached = new Intl.RelativeTimeFormat(locale, options);
108
+ relativeTimeFormatCache.set(key, cached);
109
+ }
110
+ return cached;
111
+ }
112
+ function formatNumber(value, locale, options) {
113
+ if (!Number.isFinite(value)) return String(value);
114
+ return getNumberFormatter(resolveLocale2(locale), options).format(value);
115
+ }
116
+ function formatDate(value, locale, options) {
117
+ const date = toDate(value);
118
+ if (!date) return "";
119
+ return getDateFormatter(resolveLocale2(locale), options).format(date);
120
+ }
121
+ function formatRelativeTime(value, unit, locale, options) {
122
+ if (!Number.isFinite(value)) return String(value);
123
+ return getRelativeTimeFormatter(resolveLocale2(locale), options).format(
124
+ value,
125
+ unit
126
+ );
127
+ }
128
+ function toDate(value) {
129
+ if (value instanceof Date) {
130
+ return Number.isNaN(value.getTime()) ? null : value;
131
+ }
132
+ const parsed = new Date(value);
133
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
134
+ }
135
+ function resetIntlFormatterCache() {
136
+ numberFormatCache.clear();
137
+ dateFormatCache.clear();
138
+ relativeTimeFormatCache.clear();
139
+ }
140
+
141
+ export {
142
+ resolveLocale,
143
+ getCurrentLocale,
144
+ getLocaleDirection,
145
+ formatNumber,
146
+ formatDate,
147
+ formatRelativeTime,
148
+ resetIntlFormatterCache
149
+ };
150
+ //# sourceMappingURL=chunk-MEJAHXIO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/i18n/locale-resolver.ts","../src/i18n/direction.ts","../src/i18n/format.ts"],"sourcesContent":["import { getI18nConfig } from \"./registry.js\";\n\nexport interface NpResolveLocaleInput {\n /**\n * Request pathname (e.g. `/en/blog/post-1` or `/blog/post-1`).\n * The first segment is checked against the configured locale\n * list — if it matches, that locale wins. This is the\n * primary signal: themes / page authors building under\n * `app/(site)/*` always have a pathname.\n */\n pathname?: string;\n /**\n * `Accept-Language` header value. Used as a fallback when the\n * pathname doesn't carry a locale prefix. The first\n * comma-separated tag whose primary subtag matches a configured\n * locale wins. Quality factors are honored.\n */\n acceptLanguage?: string;\n}\n\nexport interface NpResolveLocaleResult {\n /**\n * The resolved locale code (e.g. `\"en\"`, `\"ko\"`). Always one\n * of the configured locales; never an arbitrary user-supplied\n * string.\n */\n locale: string;\n /**\n * Where the locale came from. Useful when the page wants to\n * decide whether to issue a 301 (redirect bare `/blog` to\n * `/{defaultLocale}/blog` for SEO) or render in place.\n */\n source: \"path\" | \"header\" | \"default\";\n /**\n * The pathname with the locale prefix stripped, when the\n * locale came from the path. Same as the input pathname\n * otherwise. Useful for downstream slug lookups that store\n * paths without the locale segment.\n */\n pathnameWithoutLocale: string | undefined;\n}\n\n/**\n * Resolve the current request's locale using the same conventions\n * the reference app's `[[...slug]]` route uses, so theme / page\n * authors don't have to reimplement the logic.\n *\n * 1. Pathname prefix (`/en/...`) — wins if present and matches\n * a configured locale.\n * 2. `Accept-Language` header — first tag whose primary subtag\n * (or full tag) matches a configured locale.\n * 3. The site's default locale.\n *\n * Returns `null` only when i18n hasn't been configured for the\n * site (no `nexpressConfig.i18n` set). Page authors should treat\n * that as \"monolingual site\" and ignore locale entirely.\n */\nexport function resolveLocale(input: NpResolveLocaleInput = {}): NpResolveLocaleResult | null {\n const config = getI18nConfig();\n if (!config) return null;\n const configured = new Set(config.locales);\n\n if (input.pathname) {\n const segments = input.pathname.split(\"/\").filter(Boolean);\n const first = segments[0];\n if (first && configured.has(first)) {\n const remaining = segments.slice(1).join(\"/\");\n const without = remaining.length > 0 ? `/${remaining}` : \"/\";\n return { locale: first, source: \"path\", pathnameWithoutLocale: without };\n }\n }\n\n const fromHeader = matchAcceptLanguage(input.acceptLanguage, configured);\n if (fromHeader) {\n return { locale: fromHeader, source: \"header\", pathnameWithoutLocale: input.pathname };\n }\n\n return {\n locale: config.defaultLocale,\n source: \"default\",\n pathnameWithoutLocale: input.pathname,\n };\n}\n\n/**\n * Convenience wrapper that returns just the locale string.\n * Returns the default locale when i18n isn't configured (instead\n * of null) so call sites can blindly chain `.toLowerCase()` etc.\n * without a null check. Use `resolveLocale` directly when you\n * need the source / stripped path.\n */\nexport function getCurrentLocale(input: NpResolveLocaleInput = {}): string {\n const resolved = resolveLocale(input);\n if (resolved) return resolved.locale;\n // i18n not configured — return whatever the runtime knows, or\n // a hard-coded \"en\" fallback so this never throws.\n const config = getI18nConfig();\n return config?.defaultLocale ?? \"en\";\n}\n\ninterface ParsedLanguageTag {\n tag: string;\n quality: number;\n}\n\nfunction parseAcceptLanguage(header: string): ParsedLanguageTag[] {\n return header\n .split(\",\")\n .map((entry) => {\n const [tagRaw, ...params] = entry.trim().split(\";\");\n if (!tagRaw) return null;\n const tag = tagRaw.trim().toLowerCase();\n let quality = 1;\n for (const param of params) {\n const match = /^\\s*q\\s*=\\s*([0-9.]+)\\s*$/i.exec(param);\n if (match) {\n const parsed = Number(match[1]);\n if (Number.isFinite(parsed)) quality = parsed;\n }\n }\n return { tag, quality };\n })\n .filter((entry): entry is ParsedLanguageTag => entry !== null && entry.quality > 0)\n .sort((a, b) => b.quality - a.quality);\n}\n\nfunction matchAcceptLanguage(\n header: string | undefined,\n configured: Set<string>,\n): string | null {\n if (!header) return null;\n const parsed = parseAcceptLanguage(header);\n // Pre-compute lowercase configured locales so case-insensitive\n // matching works without lossy mutation of the configured set.\n const lowerToActual = new Map<string, string>();\n for (const loc of configured) lowerToActual.set(loc.toLowerCase(), loc);\n\n for (const { tag } of parsed) {\n if (lowerToActual.has(tag)) return lowerToActual.get(tag)!;\n // Try the primary subtag (`en-US` → `en`). Skip the wildcard.\n if (tag === \"*\") continue;\n const primary = tag.split(\"-\")[0];\n if (primary && lowerToActual.has(primary)) return lowerToActual.get(primary)!;\n }\n return null;\n}\n","/**\n * Phase 12.8 — locale-to-text-direction lookup.\n *\n * Uses `Intl.Locale.prototype.textInfo` (ECMA-402 stage 3,\n * supported in Node 18+ and every evergreen browser) to\n * resolve a BCP-47 tag to its CLDR-canonical script direction.\n * `ar`, `he`, `fa`, `ur` etc. → `rtl`; everything else → `ltr`.\n *\n * Returning a static `\"ltr\"` on lookup failure (unknown tag,\n * older runtimes that haven't shipped textInfo) keeps the\n * call-side tolerant — a misconfigured locale shouldn't take\n * the page render down. Operators see the wrong direction\n * (which is fixable in config) instead of a 500.\n */\nexport type NpLocaleDirection = \"ltr\" | \"rtl\";\n\ninterface LocaleWithTextInfo extends Intl.Locale {\n /**\n * Stage-3 ECMA-402 addition. Marked optional so the helper\n * still type-checks if a future @types/node downgrade\n * removes it.\n */\n readonly textInfo?: { direction?: string };\n}\n\nexport function getLocaleDirection(locale: string): NpLocaleDirection {\n if (typeof locale !== \"string\" || locale.length === 0) return \"ltr\";\n try {\n const parsed = new Intl.Locale(locale) as LocaleWithTextInfo;\n const dir = parsed.textInfo?.direction;\n return dir === \"rtl\" ? \"rtl\" : \"ltr\";\n } catch {\n // `new Intl.Locale(\"xx-not-a-tag-\")` throws RangeError; we\n // swallow because rendering the page in `ltr` is\n // strictly better than 500ing on a typo'd config entry.\n return \"ltr\";\n }\n}\n","import { getI18nConfig } from \"./registry.js\";\n\n/**\n * Phase 12.10 — locale-aware formatting helpers for sites that\n * render standalone numbers / dates outside an ICU template\n * wrapper. Thin layer over `Intl.NumberFormat`,\n * `Intl.DateTimeFormat`, and `Intl.RelativeTimeFormat` that\n *\n * 1. Resolves the locale once: explicit arg → i18n config's\n * default → runtime default (\"en\").\n * 2. Caches formatter instances per (locale, options) so the\n * relatively-expensive `new Intl.*Format(...)` only runs\n * the first time a given shape is requested. Cache keys\n * include the `JSON.stringify` of the options for stable\n * ordering.\n *\n * Sites that need behavior outside this surface (e.g. `formatToParts`,\n * locale-cascading fallbacks) should keep calling `Intl.*` directly —\n * these helpers are intentionally narrow.\n */\n\nfunction resolveLocale(explicit: string | undefined): string {\n if (explicit && explicit.length > 0) return explicit;\n return getI18nConfig()?.defaultLocale ?? \"en\";\n}\n\nconst numberFormatCache = new Map<string, Intl.NumberFormat>();\nconst dateFormatCache = new Map<string, Intl.DateTimeFormat>();\nconst relativeTimeFormatCache = new Map<string, Intl.RelativeTimeFormat>();\n\nfunction getNumberFormatter(\n locale: string,\n options: Intl.NumberFormatOptions | undefined,\n): Intl.NumberFormat {\n const key = `${locale}|${options ? JSON.stringify(options) : \"\"}`;\n let cached = numberFormatCache.get(key);\n if (!cached) {\n cached = new Intl.NumberFormat(locale, options);\n numberFormatCache.set(key, cached);\n }\n return cached;\n}\n\nfunction getDateFormatter(\n locale: string,\n options: Intl.DateTimeFormatOptions | undefined,\n): Intl.DateTimeFormat {\n const key = `${locale}|${options ? JSON.stringify(options) : \"\"}`;\n let cached = dateFormatCache.get(key);\n if (!cached) {\n cached = new Intl.DateTimeFormat(locale, options);\n dateFormatCache.set(key, cached);\n }\n return cached;\n}\n\nfunction getRelativeTimeFormatter(\n locale: string,\n options: Intl.RelativeTimeFormatOptions | undefined,\n): Intl.RelativeTimeFormat {\n const key = `${locale}|${options ? JSON.stringify(options) : \"\"}`;\n let cached = relativeTimeFormatCache.get(key);\n if (!cached) {\n cached = new Intl.RelativeTimeFormat(locale, options);\n relativeTimeFormatCache.set(key, cached);\n }\n return cached;\n}\n\n/**\n * Format a number for display. Returns the input as-is when\n * `value` isn't a finite number — defending the caller against\n * `NaN` / `Infinity` from upstream parsing failures so the page\n * renders something readable instead of \"NaN\".\n */\nexport function formatNumber(\n value: number,\n locale?: string,\n options?: Intl.NumberFormatOptions,\n): string {\n if (!Number.isFinite(value)) return String(value);\n return getNumberFormatter(resolveLocale(locale), options).format(value);\n}\n\n/**\n * Format a date for display. Accepts the three shapes a CMS\n * caller typically has on hand:\n * - `Date` (already parsed)\n * - ISO string (`updatedAt` from the API)\n * - epoch milliseconds\n * Returns an empty string for unparseable inputs so a stale\n * \"Invalid Date\" never lands in the page.\n */\nexport function formatDate(\n value: Date | string | number,\n locale?: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const date = toDate(value);\n if (!date) return \"\";\n return getDateFormatter(resolveLocale(locale), options).format(date);\n}\n\n/**\n * Format a relative time difference (`-2 days`, `in 3 hours`).\n * Wraps `Intl.RelativeTimeFormat`; the unit is constrained to\n * the standard set the platform supports.\n */\nexport function formatRelativeTime(\n value: number,\n unit: Intl.RelativeTimeFormatUnit,\n locale?: string,\n options?: Intl.RelativeTimeFormatOptions,\n): string {\n if (!Number.isFinite(value)) return String(value);\n return getRelativeTimeFormatter(resolveLocale(locale), options).format(\n value,\n unit,\n );\n}\n\nfunction toDate(value: Date | string | number): Date | null {\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value;\n }\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? null : parsed;\n}\n\n/**\n * Test hook — clear the formatter caches between tests so a\n * stale `Intl.*Format` instance from a previous test doesn't\n * survive into a new locale registration. Production code never\n * needs to call this.\n */\nexport function resetIntlFormatterCache(): void {\n numberFormatCache.clear();\n dateFormatCache.clear();\n relativeTimeFormatCache.clear();\n}\n"],"mappings":"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","names":["resolveLocale"]}
@@ -0,0 +1,101 @@
1
+ import {
2
+ NP_DEFAULT_SITE_ID
3
+ } from "./chunk-FZ7O6DWI.js";
4
+ import {
5
+ getCurrentSiteId,
6
+ requireSiteId
7
+ } from "./chunk-SBCVAC2Z.js";
8
+ import {
9
+ NpNotFoundError,
10
+ NpValidationError
11
+ } from "./chunk-ZCINJSS4.js";
12
+ import {
13
+ getDb
14
+ } from "./chunk-XANPEOJC.js";
15
+ import {
16
+ npMemberMutes,
17
+ npMembers
18
+ } from "./chunk-M43PGOQY.js";
19
+
20
+ // src/community/mutes.ts
21
+ import { and, desc, eq } from "drizzle-orm";
22
+ async function muteMember(input) {
23
+ if (input.memberId === input.targetId) {
24
+ throw new NpValidationError("Invalid input", [
25
+ { field: "targetId", message: "Cannot mute yourself." }
26
+ ]);
27
+ }
28
+ const db = getDb();
29
+ const [muter] = await db.select({ id: npMembers.id }).from(npMembers).where(eq(npMembers.id, input.memberId)).limit(1);
30
+ if (!muter) throw new NpNotFoundError("member", input.memberId);
31
+ const [target] = await db.select({ id: npMembers.id, status: npMembers.status }).from(npMembers).where(eq(npMembers.id, input.targetId)).limit(1);
32
+ if (!target) throw new NpNotFoundError("member", input.targetId);
33
+ const siteId = await requireSiteId();
34
+ await db.insert(npMemberMutes).values({
35
+ memberId: input.memberId,
36
+ targetId: input.targetId,
37
+ siteId
38
+ }).onConflictDoNothing();
39
+ }
40
+ async function unmuteMember(input) {
41
+ if (input.memberId === input.targetId) {
42
+ throw new NpValidationError("Invalid input", [
43
+ { field: "targetId", message: "Cannot unmute yourself." }
44
+ ]);
45
+ }
46
+ const db = getDb();
47
+ const siteId = await requireSiteId();
48
+ const result = await db.delete(npMemberMutes).where(
49
+ and(
50
+ eq(npMemberMutes.memberId, input.memberId),
51
+ eq(npMemberMutes.targetId, input.targetId),
52
+ eq(npMemberMutes.siteId, siteId)
53
+ )
54
+ ).returning({ memberId: npMemberMutes.memberId });
55
+ return result.length > 0;
56
+ }
57
+ async function isMuted(input) {
58
+ if (input.memberId === input.targetId) return false;
59
+ const db = getDb();
60
+ const siteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
61
+ const [row] = await db.select({ memberId: npMemberMutes.memberId }).from(npMemberMutes).where(
62
+ and(
63
+ eq(npMemberMutes.memberId, input.memberId),
64
+ eq(npMemberMutes.targetId, input.targetId),
65
+ eq(npMemberMutes.siteId, siteId)
66
+ )
67
+ ).limit(1);
68
+ return !!row;
69
+ }
70
+ async function getMutedTargetIds(memberId) {
71
+ const db = getDb();
72
+ const siteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
73
+ const rows = await db.select({ targetId: npMemberMutes.targetId }).from(npMemberMutes).where(and(eq(npMemberMutes.memberId, memberId), eq(npMemberMutes.siteId, siteId)));
74
+ return new Set(rows.map((r) => r.targetId));
75
+ }
76
+ async function listMutes(memberId, options = {}) {
77
+ const db = getDb();
78
+ const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);
79
+ const siteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
80
+ const rows = await db.select({
81
+ targetId: npMemberMutes.targetId,
82
+ handle: npMembers.handle,
83
+ displayName: npMembers.displayName,
84
+ createdAt: npMemberMutes.createdAt
85
+ }).from(npMemberMutes).innerJoin(npMembers, eq(npMemberMutes.targetId, npMembers.id)).where(and(eq(npMemberMutes.memberId, memberId), eq(npMemberMutes.siteId, siteId))).orderBy(desc(npMemberMutes.createdAt)).limit(limit);
86
+ return rows.map((r) => ({
87
+ targetId: r.targetId,
88
+ handle: r.handle,
89
+ displayName: r.displayName,
90
+ createdAt: r.createdAt.toISOString()
91
+ }));
92
+ }
93
+
94
+ export {
95
+ muteMember,
96
+ unmuteMember,
97
+ isMuted,
98
+ getMutedTargetIds,
99
+ listMutes
100
+ };
101
+ //# sourceMappingURL=chunk-NUCGHWCF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/community/mutes.ts"],"sourcesContent":["import { and, desc, eq } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npMemberMutes, npMembers } from \"../db/schema/community.js\";\nimport { NpNotFoundError, NpValidationError } from \"../errors.js\";\nimport { getCurrentSiteId, requireSiteId } from \"../sites/context.js\";\nimport { NP_DEFAULT_SITE_ID } from \"../sites/registry.js\";\n\n/**\n * Phase 16.1 — member-to-member mute. One-directional: A\n * muting B hides B from A's surfaces (comments, notification\n * fan-out). B keeps posting normally.\n *\n * Distinct from `np_bans` (staff-issued, global write block).\n * Mutes are always self-service: a member calls these helpers\n * for their own mute list, never for someone else's.\n */\n\nexport interface NpMemberMuteRow {\n memberId: string;\n targetId: string;\n createdAt: Date;\n}\n\nexport interface MuteMemberInput {\n /** The muter — the current member taking the action. */\n memberId: string;\n /** The muted — whose content should disappear. */\n targetId: string;\n}\n\nexport async function muteMember(input: MuteMemberInput): Promise<void> {\n if (input.memberId === input.targetId) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"targetId\", message: \"Cannot mute yourself.\" },\n ]);\n }\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n\n // Confirm both rows exist — otherwise the FK violation\n // surfaces as an opaque 500. NotFound is the right shape:\n // a deleted member shouldn't be muteable.\n const [muter] = (await db\n .select({ id: npMembers.id })\n .from(npMembers)\n .where(eq(npMembers.id, input.memberId))\n .limit(1)) as Array<{ id: string }>;\n if (!muter) throw new NpNotFoundError(\"member\", input.memberId);\n const [target] = (await db\n .select({ id: npMembers.id, status: npMembers.status })\n .from(npMembers)\n .where(eq(npMembers.id, input.targetId))\n .limit(1)) as Array<{ id: string; status: string }>;\n if (!target) throw new NpNotFoundError(\"member\", input.targetId);\n\n // Phase 18 — site_id is part of the PK so the same muter can\n // hold a separate \"muted-on-site-A\" / \"muted-on-site-B\" set.\n // Idempotent: muting twice on the same site doesn't error.\n // #272 — write: must NOT silently fall through to default site.\n const siteId = await requireSiteId();\n await db\n .insert(npMemberMutes)\n .values({\n memberId: input.memberId,\n targetId: input.targetId,\n siteId,\n })\n .onConflictDoNothing();\n}\n\nexport async function unmuteMember(input: MuteMemberInput): Promise<boolean> {\n if (input.memberId === input.targetId) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"targetId\", message: \"Cannot unmute yourself.\" },\n ]);\n }\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n // #272 — write: must NOT silently fall through to default site.\n const siteId = await requireSiteId();\n const result = (await db\n .delete(npMemberMutes)\n .where(\n and(\n eq(npMemberMutes.memberId, input.memberId),\n eq(npMemberMutes.targetId, input.targetId),\n eq(npMemberMutes.siteId, siteId),\n ),\n )\n .returning({ memberId: npMemberMutes.memberId })) as Array<{\n memberId: string;\n }>;\n return result.length > 0;\n}\n\n/**\n * `true` when `memberId` has muted `targetId` on the current\n * site. Used by comment listing + notification fan-out to\n * filter views and skip alerts.\n */\nexport async function isMuted(input: MuteMemberInput): Promise<boolean> {\n if (input.memberId === input.targetId) return false;\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n const siteId = (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const [row] = (await db\n .select({ memberId: npMemberMutes.memberId })\n .from(npMemberMutes)\n .where(\n and(\n eq(npMemberMutes.memberId, input.memberId),\n eq(npMemberMutes.targetId, input.targetId),\n eq(npMemberMutes.siteId, siteId),\n ),\n )\n .limit(1)) as Array<{ memberId: string }>;\n return !!row;\n}\n\n/**\n * Returns the set of `targetId`s the given member has muted on\n * the current site. Used to filter listComments output in one\n * DB round-trip rather than `isMuted()` per row.\n */\nexport async function getMutedTargetIds(memberId: string): Promise<Set<string>> {\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n const siteId = (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const rows = (await db\n .select({ targetId: npMemberMutes.targetId })\n .from(npMemberMutes)\n .where(and(eq(npMemberMutes.memberId, memberId), eq(npMemberMutes.siteId, siteId)))) as Array<{\n targetId: string;\n }>;\n return new Set(rows.map((r) => r.targetId));\n}\n\nexport interface NpMemberMuteSummary {\n targetId: string;\n handle: string;\n displayName: string;\n createdAt: string;\n}\n\nexport interface ListMutesOptions {\n /** Default 50, max 200. */\n limit?: number;\n}\n\n/**\n * Surfaces the muter's list with the muted member's display\n * info joined in, so the settings UI doesn't have to round-\n * trip through `/api/members/[handle]` for every row.\n */\nexport async function listMutes(\n memberId: string,\n options: ListMutesOptions = {},\n): Promise<NpMemberMuteSummary[]> {\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);\n // Phase 18 — settings list is per-site. The same muter can\n // see different lists on different tenants.\n const siteId = (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const rows = (await db\n .select({\n targetId: npMemberMutes.targetId,\n handle: npMembers.handle,\n displayName: npMembers.displayName,\n createdAt: npMemberMutes.createdAt,\n })\n .from(npMemberMutes)\n .innerJoin(npMembers, eq(npMemberMutes.targetId, npMembers.id))\n .where(and(eq(npMemberMutes.memberId, memberId), eq(npMemberMutes.siteId, siteId)))\n .orderBy(desc(npMemberMutes.createdAt))\n .limit(limit)) as Array<{\n targetId: string;\n handle: string;\n displayName: string;\n createdAt: Date;\n }>;\n return rows.map((r) => ({\n targetId: r.targetId,\n handle: r.handle,\n displayName: r.displayName,\n createdAt: r.createdAt.toISOString(),\n }));\n}\n"],"mappings":"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","names":[]}