@nexpress/core 0.3.9 → 0.3.11

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 (61) hide show
  1. package/dist/auth.js +2 -2
  2. package/dist/{can-U5F4JBZ7.js → can-2CHRJ2FK.js} +3 -3
  3. package/dist/{chunk-6OUWW6JF.js → chunk-24JEFZMS.js} +3 -3
  4. package/dist/{chunk-SJ7M2VCC.js → chunk-3ZLWRPCQ.js} +7 -7
  5. package/dist/{chunk-XPD7EQML.js → chunk-5NCTIJEN.js} +3 -3
  6. package/dist/{chunk-EWVXP3GP.js → chunk-A3DPN2SP.js} +2 -2
  7. package/dist/{chunk-TSCXXBOM.js → chunk-AXOG2RJK.js} +15 -15
  8. package/dist/{chunk-TSCXXBOM.js.map → chunk-AXOG2RJK.js.map} +1 -1
  9. package/dist/{chunk-XU2GJJ6Z.js → chunk-BGEPMNVB.js} +2 -2
  10. package/dist/{chunk-QZ52U4ET.js → chunk-IPWHRKIC.js} +3 -3
  11. package/dist/{chunk-6MRTH734.js → chunk-KNSOZ2NN.js} +2 -2
  12. package/dist/{chunk-YEOQJ7WW.js → chunk-KS3S6TJS.js} +2 -2
  13. package/dist/{chunk-TIWJVQOO.js → chunk-LDY26JKB.js} +2 -2
  14. package/dist/{chunk-2X3GBJOT.js → chunk-MLQKHRW2.js} +2 -2
  15. package/dist/{chunk-U4QCCLAW.js → chunk-MYUCR3SE.js} +4 -3
  16. package/dist/{chunk-U4QCCLAW.js.map → chunk-MYUCR3SE.js.map} +1 -1
  17. package/dist/{chunk-3SW4L3DL.js → chunk-N2IW4JOR.js} +9 -9
  18. package/dist/{chunk-EAYUAXW3.js → chunk-PXVCVEXV.js} +2 -2
  19. package/dist/{chunk-K4CJ3KXB.js → chunk-X43DI2QJ.js} +2 -2
  20. package/dist/{chunk-2OWUHCFY.js → chunk-ZIG5A4TD.js} +2 -2
  21. package/dist/community.js +11 -11
  22. package/dist/{config-YDGNUDKP.js → config-V75MS3II.js} +5 -5
  23. package/dist/{digest-IWHMJPXI.js → digest-U4X45I74.js} +3 -3
  24. package/dist/{host-HG4QGD3L.js → host-IEQZ5A6K.js} +4 -4
  25. package/dist/i18n.js +2 -2
  26. package/dist/index.js +99 -18
  27. package/dist/index.js.map +1 -1
  28. package/dist/jobs.js +1 -1
  29. package/dist/media.js +2 -2
  30. package/dist/{mentions-LQRZWAGO.js → mentions-FRPM6LOO.js} +3 -3
  31. package/dist/{mutes-PQA6U5X7.js → mutes-2JEKADIG.js} +3 -3
  32. package/dist/{registry-WZVL5HH6.js → registry-JMMSOTJI.js} +2 -2
  33. package/dist/{scheduled-C2IKVZVK.js → scheduled-RFKXRC6T.js} +5 -5
  34. package/dist/seo.js +4 -4
  35. package/dist/{settings-NBAP7E5E.js → settings-5PH4SNIX.js} +2 -2
  36. package/dist/{strings-O2M7VSKV.js → strings-CKOSVH45.js} +3 -3
  37. package/package.json +1 -1
  38. /package/dist/{can-U5F4JBZ7.js.map → can-2CHRJ2FK.js.map} +0 -0
  39. /package/dist/{chunk-6OUWW6JF.js.map → chunk-24JEFZMS.js.map} +0 -0
  40. /package/dist/{chunk-SJ7M2VCC.js.map → chunk-3ZLWRPCQ.js.map} +0 -0
  41. /package/dist/{chunk-XPD7EQML.js.map → chunk-5NCTIJEN.js.map} +0 -0
  42. /package/dist/{chunk-EWVXP3GP.js.map → chunk-A3DPN2SP.js.map} +0 -0
  43. /package/dist/{chunk-XU2GJJ6Z.js.map → chunk-BGEPMNVB.js.map} +0 -0
  44. /package/dist/{chunk-QZ52U4ET.js.map → chunk-IPWHRKIC.js.map} +0 -0
  45. /package/dist/{chunk-6MRTH734.js.map → chunk-KNSOZ2NN.js.map} +0 -0
  46. /package/dist/{chunk-YEOQJ7WW.js.map → chunk-KS3S6TJS.js.map} +0 -0
  47. /package/dist/{chunk-TIWJVQOO.js.map → chunk-LDY26JKB.js.map} +0 -0
  48. /package/dist/{chunk-2X3GBJOT.js.map → chunk-MLQKHRW2.js.map} +0 -0
  49. /package/dist/{chunk-3SW4L3DL.js.map → chunk-N2IW4JOR.js.map} +0 -0
  50. /package/dist/{chunk-EAYUAXW3.js.map → chunk-PXVCVEXV.js.map} +0 -0
  51. /package/dist/{chunk-K4CJ3KXB.js.map → chunk-X43DI2QJ.js.map} +0 -0
  52. /package/dist/{chunk-2OWUHCFY.js.map → chunk-ZIG5A4TD.js.map} +0 -0
  53. /package/dist/{config-YDGNUDKP.js.map → config-V75MS3II.js.map} +0 -0
  54. /package/dist/{digest-IWHMJPXI.js.map → digest-U4X45I74.js.map} +0 -0
  55. /package/dist/{host-HG4QGD3L.js.map → host-IEQZ5A6K.js.map} +0 -0
  56. /package/dist/{mentions-LQRZWAGO.js.map → mentions-FRPM6LOO.js.map} +0 -0
  57. /package/dist/{mutes-PQA6U5X7.js.map → mutes-2JEKADIG.js.map} +0 -0
  58. /package/dist/{registry-WZVL5HH6.js.map → registry-JMMSOTJI.js.map} +0 -0
  59. /package/dist/{scheduled-C2IKVZVK.js.map → scheduled-RFKXRC6T.js.map} +0 -0
  60. /package/dist/{settings-NBAP7E5E.js.map → settings-5PH4SNIX.js.map} +0 -0
  61. /package/dist/{strings-O2M7VSKV.js.map → strings-CKOSVH45.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NP_DEFAULT_SITE_ID
3
- } from "./chunk-U4QCCLAW.js";
3
+ } from "./chunk-MYUCR3SE.js";
4
4
  import {
5
5
  getCurrentSiteId
6
6
  } from "./chunk-SBCVAC2Z.js";
@@ -176,4 +176,4 @@ export {
176
176
  withMemberWrite,
177
177
  memberCan
178
178
  };
179
- //# sourceMappingURL=chunk-XU2GJJ6Z.js.map
179
+ //# sourceMappingURL=chunk-BGEPMNVB.js.map
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  findDocuments
3
- } from "./chunk-TSCXXBOM.js";
3
+ } from "./chunk-AXOG2RJK.js";
4
4
  import {
5
5
  getI18nConfig
6
6
  } from "./chunk-4ZLMEKFX.js";
7
7
  import {
8
8
  getAllCollectionSlugs,
9
9
  getCollectionConfig
10
- } from "./chunk-U4QCCLAW.js";
10
+ } from "./chunk-MYUCR3SE.js";
11
11
  import {
12
12
  getDb
13
13
  } from "./chunk-XANPEOJC.js";
@@ -597,4 +597,4 @@ export {
597
597
  buildDiscussionForumPostingJsonLd,
598
598
  buildPersonJsonLd
599
599
  };
600
- //# sourceMappingURL=chunk-QZ52U4ET.js.map
600
+ //# sourceMappingURL=chunk-IPWHRKIC.js.map
@@ -41,7 +41,7 @@ function readQuota(raw) {
41
41
  async function getCommunitySettings() {
42
42
  const db = getDb();
43
43
  const { getCurrentSiteId } = await import("./context-MNZ4QXPC.js");
44
- const { NP_DEFAULT_SITE_ID } = await import("./registry-WZVL5HH6.js");
44
+ const { NP_DEFAULT_SITE_ID } = await import("./registry-JMMSOTJI.js");
45
45
  const siteId = await getCurrentSiteId() ?? NP_DEFAULT_SITE_ID;
46
46
  const [row] = await db.select().from(npSettings).where(and(eq(npSettings.siteId, siteId), eq(npSettings.key, SETTINGS_KEY))).limit(1);
47
47
  return mergeWithDefaults(row?.value);
@@ -168,4 +168,4 @@ export {
168
168
  validateCommunitySettingsPatch,
169
169
  updateCommunitySettings
170
170
  };
171
- //# sourceMappingURL=chunk-6MRTH734.js.map
171
+ //# sourceMappingURL=chunk-KNSOZ2NN.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NP_DEFAULT_SITE_ID
3
- } from "./chunk-U4QCCLAW.js";
3
+ } from "./chunk-MYUCR3SE.js";
4
4
  import {
5
5
  getCurrentSiteId,
6
6
  requireSiteId
@@ -98,4 +98,4 @@ export {
98
98
  getMutedTargetIds,
99
99
  listMutes
100
100
  };
101
- //# sourceMappingURL=chunk-YEOQJ7WW.js.map
101
+ //# sourceMappingURL=chunk-KS3S6TJS.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-5C22NDW4.js";
4
4
  import {
5
5
  getCommunitySettings
6
- } from "./chunk-6MRTH734.js";
6
+ } from "./chunk-KNSOZ2NN.js";
7
7
  import {
8
8
  NpAuthError,
9
9
  NpForbiddenError,
@@ -855,4 +855,4 @@ export {
855
855
  requestMemberPasswordReset,
856
856
  consumeMemberPasswordReset
857
857
  };
858
- //# sourceMappingURL=chunk-TIWJVQOO.js.map
858
+ //# sourceMappingURL=chunk-LDY26JKB.js.map
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-4ZLMEKFX.js";
4
4
  import {
5
5
  NP_DEFAULT_SITE_ID
6
- } from "./chunk-U4QCCLAW.js";
6
+ } from "./chunk-MYUCR3SE.js";
7
7
  import {
8
8
  getCurrentSiteId
9
9
  } from "./chunk-SBCVAC2Z.js";
@@ -222,4 +222,4 @@ export {
222
222
  tSync,
223
223
  resetTranslationCache
224
224
  };
225
- //# sourceMappingURL=chunk-2X3GBJOT.js.map
225
+ //# sourceMappingURL=chunk-MLQKHRW2.js.map
@@ -29,11 +29,12 @@ import { eq, asc, sql } from "drizzle-orm";
29
29
  // src/collections/registry.ts
30
30
  var registry = /* @__PURE__ */ new Map();
31
31
  function registerCollection(slug, table, config, opts) {
32
+ const existing = registry.get(slug);
32
33
  registry.set(slug, {
33
34
  config,
34
35
  table,
35
- childTables: opts?.childTables,
36
- joinTables: opts?.joinTables
36
+ childTables: opts?.childTables ?? existing?.childTables,
37
+ joinTables: opts?.joinTables ?? existing?.joinTables
37
38
  });
38
39
  }
39
40
  function getCollectionConfig(slug) {
@@ -302,4 +303,4 @@ export {
302
303
  deleteSite,
303
304
  NP_DEFAULT_SITE_ID
304
305
  };
305
- //# sourceMappingURL=chunk-U4QCCLAW.js.map
306
+ //# sourceMappingURL=chunk-MYUCR3SE.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sites/registry.ts","../src/collections/registry.ts"],"sourcesContent":["import { type and, eq, asc, sql } from \"drizzle-orm\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\n\nimport {\n getAllCollectionSlugs,\n getCollectionConfig,\n getCollectionTable,\n} from \"../collections/registry.js\";\nimport { getDb } from \"../db/runtime.js\";\nimport {\n npAuditEvents,\n npBans,\n npComments,\n npFollows,\n npMemberMutes,\n npMemberRoles,\n npNotifications,\n npReactions,\n npReports,\n} from \"../db/schema/community.js\";\nimport {\n npNavigation,\n npPluginStorage,\n npSettings,\n npSiteMemberships,\n npSites,\n npStringOverrides,\n} from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\n\n/**\n * Phase 15.1 — multi-site registry. The framework treats\n * sites as long-lived rows in `np_sites`; the bootstrap calls\n * `ensureDefaultSite()` at boot to guarantee at least one row\n * exists so single-tenant installs (the existing reference\n * app shape) keep working without operator intervention.\n *\n * 15.1 ships the model + lookup helpers; 15.2 wires\n * collection queries through `siteId`; 15.3 ships the\n * super-admin UI for creating / managing sites. Until 15.2\n * lands, nothing in the existing pipeline knows or cares\n * about which site a row belongs to — the columns just\n * exist and the default site backfills.\n */\n\nexport interface NpSite {\n id: string;\n name: string;\n hostname: string | null;\n description: string | null;\n settings: Record<string, unknown>;\n isDefault: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nconst DEFAULT_SITE_ID = \"default\";\n\nfunction rowToSite(row: typeof npSites.$inferSelect): NpSite {\n return {\n id: row.id,\n name: row.name,\n hostname: row.hostname,\n description: row.description,\n settings: row.settings ?? {},\n isDefault: row.isDefault,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/**\n * Idempotently create the default site if no sites exist.\n * Bootstrap calls this once during framework init; tests\n * that truncate `np_sites` between cases re-trigger it.\n */\nexport async function ensureDefaultSite(): Promise<NpSite> {\n const db = getDb();\n const existingDefault = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, DEFAULT_SITE_ID))\n .limit(1);\n if (existingDefault[0]) return rowToSite(existingDefault[0]);\n\n const now = new Date();\n const [created] = await db\n .insert(npSites)\n .values({\n id: DEFAULT_SITE_ID,\n name: \"Default site\",\n hostname: null,\n isDefault: true,\n settings: {},\n createdAt: now,\n updatedAt: now,\n })\n .onConflictDoNothing()\n .returning();\n if (created) return rowToSite(created);\n\n // Conflict path: another worker raced us. Re-read.\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, DEFAULT_SITE_ID))\n .limit(1);\n if (!row) {\n throw new Error(\"Failed to create or read the default site\");\n }\n return rowToSite(row);\n}\n\nexport async function listSites(): Promise<NpSite[]> {\n const db = getDb();\n const rows = await db.select().from(npSites).orderBy(asc(npSites.createdAt));\n return rows.map(rowToSite);\n}\n\nexport async function getSiteById(id: string): Promise<NpSite | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, id))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\n/**\n * Hostname-based lookup. Returns the matching site, or the\n * default site when no row matches (so a request hitting\n * an unconfigured host still gets served by the canonical\n * site rather than 404'ing). Case-insensitive on the host\n * string.\n */\nexport async function getSiteByHostname(\n hostname: string,\n): Promise<NpSite | null> {\n const db = getDb();\n const lower = hostname.toLowerCase();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.hostname, lower))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\nexport async function getDefaultSite(): Promise<NpSite | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.isDefault, true))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\n/**\n * Resolve which site a request belongs to. Tries hostname\n * lookup first; falls back to the default site. Returns\n * `null` only when the database has no sites at all (which\n * shouldn't happen post-bootstrap).\n */\nexport async function resolveSiteForHostname(\n hostname: string | null | undefined,\n): Promise<NpSite | null> {\n if (hostname) {\n const matched = await getSiteByHostname(hostname);\n if (matched) return matched;\n }\n return getDefaultSite();\n}\n\nexport interface CreateSiteInput {\n id: string;\n name: string;\n hostname?: string | null;\n description?: string | null;\n settings?: Record<string, unknown>;\n}\n\nexport async function createSite(input: CreateSiteInput): Promise<NpSite> {\n if (!/^[a-z][a-z0-9-]*$/.test(input.id)) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"id\",\n message:\n \"Site id must be lowercase alphanumeric + hyphens, starting with a letter\",\n },\n ]);\n }\n const db = getDb();\n const now = new Date();\n const [row] = await db\n .insert(npSites)\n .values({\n id: input.id,\n name: input.name,\n hostname: input.hostname?.toLowerCase() ?? null,\n description: input.description ?? null,\n settings: input.settings ?? {},\n isDefault: false,\n createdAt: now,\n updatedAt: now,\n })\n .returning();\n if (!row) {\n throw new Error(\"Failed to create site\");\n }\n return rowToSite(row);\n}\n\nexport async function updateSite(\n id: string,\n patch: Partial<Pick<NpSite, \"name\" | \"hostname\" | \"description\" | \"settings\">>,\n): Promise<NpSite> {\n const db = getDb();\n const updates: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) updates.name = patch.name;\n if (patch.hostname !== undefined) {\n updates.hostname = patch.hostname ? patch.hostname.toLowerCase() : null;\n }\n if (patch.description !== undefined) updates.description = patch.description;\n if (patch.settings !== undefined) updates.settings = patch.settings;\n const [row] = await db\n .update(npSites)\n .set(updates)\n .where(eq(npSites.id, id))\n .returning();\n if (!row) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"id\", message: `Site \"${id}\" not found` },\n ]);\n }\n return rowToSite(row);\n}\n\n/**\n * Phase 15.9 — count of every site-scoped row attached to a\n * given site. Surfaces in the admin delete-site dialog so\n * operators see what they're about to nuke (or leave behind\n * as orphans, in the cascade=false path).\n *\n * Includes:\n * - per-collection row counts (codegen'd `np_c_*` tables)\n * - system tables that carry `site_id`: settings,\n * navigation, memberships, string overrides, plugin\n * storage (Issue #220)\n * - community tables that carry `site_id`: comments,\n * reactions, follows, mutes, notifications, reports,\n * audit events, bans, member roles (Issue #220)\n *\n * Does NOT include things that aren't site-scoped:\n * - users (`np_users` is global)\n * - members (`np_members` is global; per-site enrollment\n * happens through the site-scoped `bans` / `member_roles`\n * tables which DO appear in usage)\n * - media (`np_media` is global)\n * - audit events with `site_id IS NULL` — those are\n * intentional super-admin / background-job events that\n * don't belong to any tenant.\n */\nexport interface NpSiteUsage {\n collections: Record<string, number>;\n settings: number;\n navigation: number;\n memberships: number;\n stringOverrides: number;\n /** Issue #220 — newly-included site-scoped tables. */\n pluginStorage: number;\n comments: number;\n reactions: number;\n follows: number;\n mutes: number;\n notifications: number;\n reports: number;\n auditEvents: number;\n bans: number;\n memberRoles: number;\n /** Sum of every count above. Convenience for \"is anything here?\" checks. */\n total: number;\n}\n\nexport async function getSiteUsageSummary(id: string): Promise<NpSiteUsage> {\n const db = getDb();\n const collections: Record<string, number> = {};\n for (const slug of getAllCollectionSlugs()) {\n try {\n const config = getCollectionConfig(slug);\n void config;\n const table = getCollectionTable(slug) as PgTable;\n const idCol = (table as unknown as Record<string, unknown>).siteId;\n if (!idCol) continue;\n const [row] = (await db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(eq(idCol as never, id))) as Array<{ count: number }>;\n collections[slug] = row?.count ?? 0;\n } catch {\n // Collection without a registered table — skip silently.\n }\n }\n\n const countWhere = async (\n table: PgTable,\n where: ReturnType<typeof eq> | ReturnType<typeof and>,\n ): Promise<number> => {\n const [row] = (await db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(where)) as Array<{ count: number }>;\n return row?.count ?? 0;\n };\n\n const settings = await countWhere(npSettings, eq(npSettings.siteId, id));\n const navigation = await countWhere(npNavigation, eq(npNavigation.siteId, id));\n const memberships = await countWhere(\n npSiteMemberships,\n eq(npSiteMemberships.siteId, id),\n );\n const stringOverrides = await countWhere(\n npStringOverrides,\n eq(npStringOverrides.siteId, id),\n );\n // Issue #220 — include the tables that landed after Phase 15.9\n // shipped. Without them a site looks \"empty\" in the admin\n // even though it owns thousands of community rows; deleting\n // it would silently leave them orphaned.\n const pluginStorage = await countWhere(\n npPluginStorage,\n eq(npPluginStorage.siteId, id),\n );\n const comments = await countWhere(npComments, eq(npComments.siteId, id));\n const reactions = await countWhere(npReactions, eq(npReactions.siteId, id));\n const follows = await countWhere(npFollows, eq(npFollows.siteId, id));\n const mutes = await countWhere(npMemberMutes, eq(npMemberMutes.siteId, id));\n const notifications = await countWhere(\n npNotifications,\n eq(npNotifications.siteId, id),\n );\n const reports = await countWhere(npReports, eq(npReports.siteId, id));\n // Audit events with `site_id IS NULL` are the cross-tenant /\n // background-job rows; we deliberately don't count them here.\n const auditEvents = await countWhere(\n npAuditEvents,\n eq(npAuditEvents.siteId, id),\n );\n const bans = await countWhere(npBans, eq(npBans.siteId, id));\n const memberRoles = await countWhere(npMemberRoles, eq(npMemberRoles.siteId, id));\n\n const collectionsTotal = Object.values(collections).reduce(\n (sum, n) => sum + n,\n 0,\n );\n\n return {\n collections,\n settings,\n navigation,\n memberships,\n stringOverrides,\n pluginStorage,\n comments,\n reactions,\n follows,\n mutes,\n notifications,\n reports,\n auditEvents,\n bans,\n memberRoles,\n total:\n collectionsTotal +\n settings +\n navigation +\n memberships +\n stringOverrides +\n pluginStorage +\n comments +\n reactions +\n follows +\n mutes +\n notifications +\n reports +\n auditEvents +\n bans +\n memberRoles,\n };\n}\n\nexport interface NpDeleteSiteOptions {\n /**\n * Phase 15.9 — when `true`, cascade-delete every site-scoped\n * row (collection content, settings, navigation, memberships,\n * string overrides) before dropping the `np_sites` row.\n *\n * When `false` (default, safe), the call refuses if any\n * site-scoped data still exists. The admin UI uses this to\n * force operators to confirm cascade explicitly so an\n * accidental delete can't quietly orphan thousands of rows.\n */\n cascade?: boolean;\n}\n\n/**\n * Delete a non-default site. The default site can't be\n * deleted (the framework's invariant is \"at least one site\n * always exists\"); operators who want to retire the default\n * promote a different site to default first.\n *\n * Phase 15.9 — `options.cascade` controls whether site-scoped\n * data is deleted alongside. Defaults to `false` for safety;\n * the admin UI surfaces a usage summary first so the operator\n * sees what cascade would touch.\n */\nexport async function deleteSite(\n id: string,\n options?: NpDeleteSiteOptions,\n): Promise<void> {\n const db = getDb();\n const [target] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, id))\n .limit(1);\n if (!target) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"id\", message: `Site \"${id}\" not found` },\n ]);\n }\n if (target.isDefault) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"id\",\n message:\n \"Cannot delete the default site. Promote another site to default first.\",\n },\n ]);\n }\n\n const usage = await getSiteUsageSummary(id);\n if (usage.total > 0 && !options?.cascade) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"cascade\",\n message: `Site \"${id}\" has ${usage.total} attached row(s). Pass cascade=true to delete them, or clear them manually first.`,\n },\n ]);\n }\n\n if (options?.cascade) {\n // Order: collection content first, then community rows that\n // reference comments / members polymorphically (so we don't\n // leave orphan reactions pointing at deleted comments mid-\n // sweep), then community parent tables, then system tables.\n // Collection deletes go through the raw table (no hook\n // firing) — site teardown isn't a pipeline write and there's\n // no doc-level afterDelete hook expected here.\n for (const slug of Object.keys(usage.collections)) {\n try {\n const table = getCollectionTable(slug) as PgTable;\n const siteIdCol = (table as unknown as Record<string, unknown>).siteId;\n if (!siteIdCol) continue;\n await db.delete(table).where(eq(siteIdCol as never, id));\n } catch {\n // Ignore — the collection might have been\n // unregistered between the usage scan and the delete.\n }\n }\n // Issue #220 — community rows. Order:\n // reactions/follows/mutes/notifications/reports/audit/bans/\n // member_roles → comments → string_overrides/navigation/\n // settings/plugin_storage/memberships → np_sites.\n // Reactions reference comment ids polymorphically, so they\n // go before comments to keep the DB clean even though there's\n // no FK to enforce ordering.\n await db.delete(npReactions).where(eq(npReactions.siteId, id));\n await db.delete(npFollows).where(eq(npFollows.siteId, id));\n await db.delete(npMemberMutes).where(eq(npMemberMutes.siteId, id));\n await db.delete(npNotifications).where(eq(npNotifications.siteId, id));\n await db.delete(npReports).where(eq(npReports.siteId, id));\n await db.delete(npAuditEvents).where(eq(npAuditEvents.siteId, id));\n await db.delete(npBans).where(eq(npBans.siteId, id));\n await db.delete(npMemberRoles).where(eq(npMemberRoles.siteId, id));\n await db.delete(npComments).where(eq(npComments.siteId, id));\n\n await db.delete(npStringOverrides).where(eq(npStringOverrides.siteId, id));\n await db.delete(npNavigation).where(eq(npNavigation.siteId, id));\n await db.delete(npSettings).where(eq(npSettings.siteId, id));\n await db.delete(npPluginStorage).where(eq(npPluginStorage.siteId, id));\n await db.delete(npSiteMemberships).where(eq(npSiteMemberships.siteId, id));\n }\n\n await db.delete(npSites).where(eq(npSites.id, id));\n}\n\nexport const NP_DEFAULT_SITE_ID = DEFAULT_SITE_ID;\n","import type { NpCollectionConfig } from \"../config/types.js\";\n\nimport { NpNotFoundError } from \"../errors.js\";\n\nexport interface CollectionRegistration {\n config: NpCollectionConfig;\n table: unknown;\n childTables?: Record<string, unknown>;\n joinTables?: Record<string, unknown>;\n}\n\nconst registry = new Map<string, CollectionRegistration>();\n\nexport function registerCollection(\n slug: string,\n table: unknown,\n config: NpCollectionConfig,\n opts?: { childTables?: Record<string, unknown>; joinTables?: Record<string, unknown> },\n): void {\n registry.set(slug, {\n config,\n table,\n childTables: opts?.childTables,\n joinTables: opts?.joinTables,\n });\n}\n\nexport function getCollectionConfig(slug: string): NpCollectionConfig {\n return getCollectionRegistration(slug).config;\n}\n\nexport function getCollectionTable(slug: string): unknown {\n return getCollectionRegistration(slug).table;\n}\n\nexport function getCollectionRegistration(slug: string): CollectionRegistration {\n const registration = registry.get(slug);\n\n if (!registration) {\n throw new NpNotFoundError(\"collection\", slug);\n }\n\n return registration;\n}\n\nexport function getAllCollectionSlugs(): string[] {\n return [...registry.keys()];\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sites/registry.ts","../src/collections/registry.ts"],"sourcesContent":["import { type and, eq, asc, sql } from \"drizzle-orm\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\n\nimport {\n getAllCollectionSlugs,\n getCollectionConfig,\n getCollectionTable,\n} from \"../collections/registry.js\";\nimport { getDb } from \"../db/runtime.js\";\nimport {\n npAuditEvents,\n npBans,\n npComments,\n npFollows,\n npMemberMutes,\n npMemberRoles,\n npNotifications,\n npReactions,\n npReports,\n} from \"../db/schema/community.js\";\nimport {\n npNavigation,\n npPluginStorage,\n npSettings,\n npSiteMemberships,\n npSites,\n npStringOverrides,\n} from \"../db/schema/system.js\";\nimport { NpValidationError } from \"../errors.js\";\n\n/**\n * Phase 15.1 — multi-site registry. The framework treats\n * sites as long-lived rows in `np_sites`; the bootstrap calls\n * `ensureDefaultSite()` at boot to guarantee at least one row\n * exists so single-tenant installs (the existing reference\n * app shape) keep working without operator intervention.\n *\n * 15.1 ships the model + lookup helpers; 15.2 wires\n * collection queries through `siteId`; 15.3 ships the\n * super-admin UI for creating / managing sites. Until 15.2\n * lands, nothing in the existing pipeline knows or cares\n * about which site a row belongs to — the columns just\n * exist and the default site backfills.\n */\n\nexport interface NpSite {\n id: string;\n name: string;\n hostname: string | null;\n description: string | null;\n settings: Record<string, unknown>;\n isDefault: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nconst DEFAULT_SITE_ID = \"default\";\n\nfunction rowToSite(row: typeof npSites.$inferSelect): NpSite {\n return {\n id: row.id,\n name: row.name,\n hostname: row.hostname,\n description: row.description,\n settings: row.settings ?? {},\n isDefault: row.isDefault,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/**\n * Idempotently create the default site if no sites exist.\n * Bootstrap calls this once during framework init; tests\n * that truncate `np_sites` between cases re-trigger it.\n */\nexport async function ensureDefaultSite(): Promise<NpSite> {\n const db = getDb();\n const existingDefault = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, DEFAULT_SITE_ID))\n .limit(1);\n if (existingDefault[0]) return rowToSite(existingDefault[0]);\n\n const now = new Date();\n const [created] = await db\n .insert(npSites)\n .values({\n id: DEFAULT_SITE_ID,\n name: \"Default site\",\n hostname: null,\n isDefault: true,\n settings: {},\n createdAt: now,\n updatedAt: now,\n })\n .onConflictDoNothing()\n .returning();\n if (created) return rowToSite(created);\n\n // Conflict path: another worker raced us. Re-read.\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, DEFAULT_SITE_ID))\n .limit(1);\n if (!row) {\n throw new Error(\"Failed to create or read the default site\");\n }\n return rowToSite(row);\n}\n\nexport async function listSites(): Promise<NpSite[]> {\n const db = getDb();\n const rows = await db.select().from(npSites).orderBy(asc(npSites.createdAt));\n return rows.map(rowToSite);\n}\n\nexport async function getSiteById(id: string): Promise<NpSite | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, id))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\n/**\n * Hostname-based lookup. Returns the matching site, or the\n * default site when no row matches (so a request hitting\n * an unconfigured host still gets served by the canonical\n * site rather than 404'ing). Case-insensitive on the host\n * string.\n */\nexport async function getSiteByHostname(\n hostname: string,\n): Promise<NpSite | null> {\n const db = getDb();\n const lower = hostname.toLowerCase();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.hostname, lower))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\nexport async function getDefaultSite(): Promise<NpSite | null> {\n const db = getDb();\n const [row] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.isDefault, true))\n .limit(1);\n return row ? rowToSite(row) : null;\n}\n\n/**\n * Resolve which site a request belongs to. Tries hostname\n * lookup first; falls back to the default site. Returns\n * `null` only when the database has no sites at all (which\n * shouldn't happen post-bootstrap).\n */\nexport async function resolveSiteForHostname(\n hostname: string | null | undefined,\n): Promise<NpSite | null> {\n if (hostname) {\n const matched = await getSiteByHostname(hostname);\n if (matched) return matched;\n }\n return getDefaultSite();\n}\n\nexport interface CreateSiteInput {\n id: string;\n name: string;\n hostname?: string | null;\n description?: string | null;\n settings?: Record<string, unknown>;\n}\n\nexport async function createSite(input: CreateSiteInput): Promise<NpSite> {\n if (!/^[a-z][a-z0-9-]*$/.test(input.id)) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"id\",\n message:\n \"Site id must be lowercase alphanumeric + hyphens, starting with a letter\",\n },\n ]);\n }\n const db = getDb();\n const now = new Date();\n const [row] = await db\n .insert(npSites)\n .values({\n id: input.id,\n name: input.name,\n hostname: input.hostname?.toLowerCase() ?? null,\n description: input.description ?? null,\n settings: input.settings ?? {},\n isDefault: false,\n createdAt: now,\n updatedAt: now,\n })\n .returning();\n if (!row) {\n throw new Error(\"Failed to create site\");\n }\n return rowToSite(row);\n}\n\nexport async function updateSite(\n id: string,\n patch: Partial<Pick<NpSite, \"name\" | \"hostname\" | \"description\" | \"settings\">>,\n): Promise<NpSite> {\n const db = getDb();\n const updates: Record<string, unknown> = { updatedAt: new Date() };\n if (patch.name !== undefined) updates.name = patch.name;\n if (patch.hostname !== undefined) {\n updates.hostname = patch.hostname ? patch.hostname.toLowerCase() : null;\n }\n if (patch.description !== undefined) updates.description = patch.description;\n if (patch.settings !== undefined) updates.settings = patch.settings;\n const [row] = await db\n .update(npSites)\n .set(updates)\n .where(eq(npSites.id, id))\n .returning();\n if (!row) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"id\", message: `Site \"${id}\" not found` },\n ]);\n }\n return rowToSite(row);\n}\n\n/**\n * Phase 15.9 — count of every site-scoped row attached to a\n * given site. Surfaces in the admin delete-site dialog so\n * operators see what they're about to nuke (or leave behind\n * as orphans, in the cascade=false path).\n *\n * Includes:\n * - per-collection row counts (codegen'd `np_c_*` tables)\n * - system tables that carry `site_id`: settings,\n * navigation, memberships, string overrides, plugin\n * storage (Issue #220)\n * - community tables that carry `site_id`: comments,\n * reactions, follows, mutes, notifications, reports,\n * audit events, bans, member roles (Issue #220)\n *\n * Does NOT include things that aren't site-scoped:\n * - users (`np_users` is global)\n * - members (`np_members` is global; per-site enrollment\n * happens through the site-scoped `bans` / `member_roles`\n * tables which DO appear in usage)\n * - media (`np_media` is global)\n * - audit events with `site_id IS NULL` — those are\n * intentional super-admin / background-job events that\n * don't belong to any tenant.\n */\nexport interface NpSiteUsage {\n collections: Record<string, number>;\n settings: number;\n navigation: number;\n memberships: number;\n stringOverrides: number;\n /** Issue #220 — newly-included site-scoped tables. */\n pluginStorage: number;\n comments: number;\n reactions: number;\n follows: number;\n mutes: number;\n notifications: number;\n reports: number;\n auditEvents: number;\n bans: number;\n memberRoles: number;\n /** Sum of every count above. Convenience for \"is anything here?\" checks. */\n total: number;\n}\n\nexport async function getSiteUsageSummary(id: string): Promise<NpSiteUsage> {\n const db = getDb();\n const collections: Record<string, number> = {};\n for (const slug of getAllCollectionSlugs()) {\n try {\n const config = getCollectionConfig(slug);\n void config;\n const table = getCollectionTable(slug) as PgTable;\n const idCol = (table as unknown as Record<string, unknown>).siteId;\n if (!idCol) continue;\n const [row] = (await db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(eq(idCol as never, id))) as Array<{ count: number }>;\n collections[slug] = row?.count ?? 0;\n } catch {\n // Collection without a registered table — skip silently.\n }\n }\n\n const countWhere = async (\n table: PgTable,\n where: ReturnType<typeof eq> | ReturnType<typeof and>,\n ): Promise<number> => {\n const [row] = (await db\n .select({ count: sql<number>`count(*)::int` })\n .from(table)\n .where(where)) as Array<{ count: number }>;\n return row?.count ?? 0;\n };\n\n const settings = await countWhere(npSettings, eq(npSettings.siteId, id));\n const navigation = await countWhere(npNavigation, eq(npNavigation.siteId, id));\n const memberships = await countWhere(\n npSiteMemberships,\n eq(npSiteMemberships.siteId, id),\n );\n const stringOverrides = await countWhere(\n npStringOverrides,\n eq(npStringOverrides.siteId, id),\n );\n // Issue #220 — include the tables that landed after Phase 15.9\n // shipped. Without them a site looks \"empty\" in the admin\n // even though it owns thousands of community rows; deleting\n // it would silently leave them orphaned.\n const pluginStorage = await countWhere(\n npPluginStorage,\n eq(npPluginStorage.siteId, id),\n );\n const comments = await countWhere(npComments, eq(npComments.siteId, id));\n const reactions = await countWhere(npReactions, eq(npReactions.siteId, id));\n const follows = await countWhere(npFollows, eq(npFollows.siteId, id));\n const mutes = await countWhere(npMemberMutes, eq(npMemberMutes.siteId, id));\n const notifications = await countWhere(\n npNotifications,\n eq(npNotifications.siteId, id),\n );\n const reports = await countWhere(npReports, eq(npReports.siteId, id));\n // Audit events with `site_id IS NULL` are the cross-tenant /\n // background-job rows; we deliberately don't count them here.\n const auditEvents = await countWhere(\n npAuditEvents,\n eq(npAuditEvents.siteId, id),\n );\n const bans = await countWhere(npBans, eq(npBans.siteId, id));\n const memberRoles = await countWhere(npMemberRoles, eq(npMemberRoles.siteId, id));\n\n const collectionsTotal = Object.values(collections).reduce(\n (sum, n) => sum + n,\n 0,\n );\n\n return {\n collections,\n settings,\n navigation,\n memberships,\n stringOverrides,\n pluginStorage,\n comments,\n reactions,\n follows,\n mutes,\n notifications,\n reports,\n auditEvents,\n bans,\n memberRoles,\n total:\n collectionsTotal +\n settings +\n navigation +\n memberships +\n stringOverrides +\n pluginStorage +\n comments +\n reactions +\n follows +\n mutes +\n notifications +\n reports +\n auditEvents +\n bans +\n memberRoles,\n };\n}\n\nexport interface NpDeleteSiteOptions {\n /**\n * Phase 15.9 — when `true`, cascade-delete every site-scoped\n * row (collection content, settings, navigation, memberships,\n * string overrides) before dropping the `np_sites` row.\n *\n * When `false` (default, safe), the call refuses if any\n * site-scoped data still exists. The admin UI uses this to\n * force operators to confirm cascade explicitly so an\n * accidental delete can't quietly orphan thousands of rows.\n */\n cascade?: boolean;\n}\n\n/**\n * Delete a non-default site. The default site can't be\n * deleted (the framework's invariant is \"at least one site\n * always exists\"); operators who want to retire the default\n * promote a different site to default first.\n *\n * Phase 15.9 — `options.cascade` controls whether site-scoped\n * data is deleted alongside. Defaults to `false` for safety;\n * the admin UI surfaces a usage summary first so the operator\n * sees what cascade would touch.\n */\nexport async function deleteSite(\n id: string,\n options?: NpDeleteSiteOptions,\n): Promise<void> {\n const db = getDb();\n const [target] = await db\n .select()\n .from(npSites)\n .where(eq(npSites.id, id))\n .limit(1);\n if (!target) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"id\", message: `Site \"${id}\" not found` },\n ]);\n }\n if (target.isDefault) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"id\",\n message:\n \"Cannot delete the default site. Promote another site to default first.\",\n },\n ]);\n }\n\n const usage = await getSiteUsageSummary(id);\n if (usage.total > 0 && !options?.cascade) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"cascade\",\n message: `Site \"${id}\" has ${usage.total} attached row(s). Pass cascade=true to delete them, or clear them manually first.`,\n },\n ]);\n }\n\n if (options?.cascade) {\n // Order: collection content first, then community rows that\n // reference comments / members polymorphically (so we don't\n // leave orphan reactions pointing at deleted comments mid-\n // sweep), then community parent tables, then system tables.\n // Collection deletes go through the raw table (no hook\n // firing) — site teardown isn't a pipeline write and there's\n // no doc-level afterDelete hook expected here.\n for (const slug of Object.keys(usage.collections)) {\n try {\n const table = getCollectionTable(slug) as PgTable;\n const siteIdCol = (table as unknown as Record<string, unknown>).siteId;\n if (!siteIdCol) continue;\n await db.delete(table).where(eq(siteIdCol as never, id));\n } catch {\n // Ignore — the collection might have been\n // unregistered between the usage scan and the delete.\n }\n }\n // Issue #220 — community rows. Order:\n // reactions/follows/mutes/notifications/reports/audit/bans/\n // member_roles → comments → string_overrides/navigation/\n // settings/plugin_storage/memberships → np_sites.\n // Reactions reference comment ids polymorphically, so they\n // go before comments to keep the DB clean even though there's\n // no FK to enforce ordering.\n await db.delete(npReactions).where(eq(npReactions.siteId, id));\n await db.delete(npFollows).where(eq(npFollows.siteId, id));\n await db.delete(npMemberMutes).where(eq(npMemberMutes.siteId, id));\n await db.delete(npNotifications).where(eq(npNotifications.siteId, id));\n await db.delete(npReports).where(eq(npReports.siteId, id));\n await db.delete(npAuditEvents).where(eq(npAuditEvents.siteId, id));\n await db.delete(npBans).where(eq(npBans.siteId, id));\n await db.delete(npMemberRoles).where(eq(npMemberRoles.siteId, id));\n await db.delete(npComments).where(eq(npComments.siteId, id));\n\n await db.delete(npStringOverrides).where(eq(npStringOverrides.siteId, id));\n await db.delete(npNavigation).where(eq(npNavigation.siteId, id));\n await db.delete(npSettings).where(eq(npSettings.siteId, id));\n await db.delete(npPluginStorage).where(eq(npPluginStorage.siteId, id));\n await db.delete(npSiteMemberships).where(eq(npSiteMemberships.siteId, id));\n }\n\n await db.delete(npSites).where(eq(npSites.id, id));\n}\n\nexport const NP_DEFAULT_SITE_ID = DEFAULT_SITE_ID;\n","import type { NpCollectionConfig } from \"../config/types.js\";\n\nimport { NpNotFoundError } from \"../errors.js\";\n\nexport interface CollectionRegistration {\n config: NpCollectionConfig;\n table: unknown;\n childTables?: Record<string, unknown>;\n joinTables?: Record<string, unknown>;\n}\n\nconst registry = new Map<string, CollectionRegistration>();\n\nexport function registerCollection(\n slug: string,\n table: unknown,\n config: NpCollectionConfig,\n opts?: { childTables?: Record<string, unknown>; joinTables?: Record<string, unknown> },\n): void {\n const existing = registry.get(slug);\n registry.set(slug, {\n config,\n table,\n childTables: opts?.childTables ?? existing?.childTables,\n joinTables: opts?.joinTables ?? existing?.joinTables,\n });\n}\n\nexport function getCollectionConfig(slug: string): NpCollectionConfig {\n return getCollectionRegistration(slug).config;\n}\n\nexport function getCollectionTable(slug: string): unknown {\n return getCollectionRegistration(slug).table;\n}\n\nexport function getCollectionRegistration(slug: string): CollectionRegistration {\n const registration = registry.get(slug);\n\n if (!registration) {\n throw new NpNotFoundError(\"collection\", slug);\n }\n\n return registration;\n}\n\nexport function getAllCollectionSlugs(): string[] {\n return [...registry.keys()];\n}\n"],"mappings":"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","names":[]}
@@ -2,15 +2,15 @@ import {
2
2
  getCommunityRole,
3
3
  memberCan,
4
4
  withMemberWrite
5
- } from "./chunk-XU2GJJ6Z.js";
5
+ } from "./chunk-BGEPMNVB.js";
6
6
  import {
7
7
  getMutedTargetIds
8
- } from "./chunk-YEOQJ7WW.js";
8
+ } from "./chunk-KS3S6TJS.js";
9
9
  import {
10
10
  createNotification,
11
11
  extractMentionHandles,
12
12
  fanOutMentionNotifications
13
- } from "./chunk-XPD7EQML.js";
13
+ } from "./chunk-5NCTIJEN.js";
14
14
  import {
15
15
  applyReputation
16
16
  } from "./chunk-VBVLYFSZ.js";
@@ -22,17 +22,17 @@ import {
22
22
  } from "./chunk-KU5M27ZC.js";
23
23
  import {
24
24
  getMediaUrl
25
- } from "./chunk-EWVXP3GP.js";
25
+ } from "./chunk-A3DPN2SP.js";
26
26
  import {
27
27
  buildWeightedSearchVectorSql,
28
28
  deleteDocument,
29
29
  findDocuments,
30
30
  getDocumentById,
31
31
  saveDocument
32
- } from "./chunk-TSCXXBOM.js";
32
+ } from "./chunk-AXOG2RJK.js";
33
33
  import {
34
34
  deleteMedia
35
- } from "./chunk-EAYUAXW3.js";
35
+ } from "./chunk-PXVCVEXV.js";
36
36
  import {
37
37
  can
38
38
  } from "./chunk-EQ2Z3KMD.js";
@@ -41,7 +41,7 @@ import {
41
41
  } from "./chunk-5C22NDW4.js";
42
42
  import {
43
43
  getCommunitySettings
44
- } from "./chunk-6MRTH734.js";
44
+ } from "./chunk-KNSOZ2NN.js";
45
45
  import {
46
46
  getI18nConfig
47
47
  } from "./chunk-4ZLMEKFX.js";
@@ -51,7 +51,7 @@ import {
51
51
  getCollectionConfig,
52
52
  getCollectionRegistration,
53
53
  getCollectionTable
54
- } from "./chunk-U4QCCLAW.js";
54
+ } from "./chunk-MYUCR3SE.js";
55
55
  import {
56
56
  getCurrentSiteId,
57
57
  requireSiteId
@@ -1956,4 +1956,4 @@ export {
1956
1956
  revokeMemberRole,
1957
1957
  purgeMemberContent
1958
1958
  };
1959
- //# sourceMappingURL=chunk-3SW4L3DL.js.map
1959
+ //# sourceMappingURL=chunk-N2IW4JOR.js.map
@@ -170,7 +170,7 @@ async function uploadMedia(file, uploader, folderId) {
170
170
  return { id, status: "processing" };
171
171
  }
172
172
  async function assertMemberUploadQuota(memberId, txDb) {
173
- const { getCommunitySettings } = await import("./settings-NBAP7E5E.js");
173
+ const { getCommunitySettings } = await import("./settings-5PH4SNIX.js");
174
174
  const { NpRateLimitError } = await import("./errors-5OS3S2J3.js");
175
175
  const settings = await getCommunitySettings();
176
176
  const { perDay, total } = settings.memberUploadQuota;
@@ -535,4 +535,4 @@ export {
535
535
  listMedia,
536
536
  cleanupDeletedMedia
537
537
  };
538
- //# sourceMappingURL=chunk-EAYUAXW3.js.map
538
+ //# sourceMappingURL=chunk-PXVCVEXV.js.map
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  NP_DEFAULT_SITE_ID,
6
6
  listSites
7
- } from "./chunk-U4QCCLAW.js";
7
+ } from "./chunk-MYUCR3SE.js";
8
8
  import {
9
9
  getEmailAdapter
10
10
  } from "./chunk-LSHHRDVR.js";
@@ -191,4 +191,4 @@ export {
191
191
  buildDigestEmail,
192
192
  runDigestSweep
193
193
  };
194
- //# sourceMappingURL=chunk-K4CJ3KXB.js.map
194
+ //# sourceMappingURL=chunk-X43DI2QJ.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getPluginRegistration
3
- } from "./chunk-TSCXXBOM.js";
3
+ } from "./chunk-AXOG2RJK.js";
4
4
  import {
5
5
  getCurrentSiteId
6
6
  } from "./chunk-SBCVAC2Z.js";
@@ -319,4 +319,4 @@ export {
319
319
  setPluginConfig,
320
320
  pluginConfigCacheTag
321
321
  };
322
- //# sourceMappingURL=chunk-2OWUHCFY.js.map
322
+ //# sourceMappingURL=chunk-ZIG5A4TD.js.map
package/dist/community.js CHANGED
@@ -33,12 +33,12 @@ import {
33
33
  unfollow,
34
34
  unresolvedReportCount,
35
35
  updateComment
36
- } from "./chunk-3SW4L3DL.js";
37
- import "./chunk-6OUWW6JF.js";
36
+ } from "./chunk-N2IW4JOR.js";
37
+ import "./chunk-24JEFZMS.js";
38
38
  import {
39
39
  buildDigestEmail,
40
40
  runDigestSweep
41
- } from "./chunk-K4CJ3KXB.js";
41
+ } from "./chunk-X43DI2QJ.js";
42
42
  import {
43
43
  assertNotBanned,
44
44
  getCommunityRole,
@@ -47,14 +47,14 @@ import {
47
47
  registerCommunityRole,
48
48
  resetCommunityRoles,
49
49
  withMemberWrite
50
- } from "./chunk-XU2GJJ6Z.js";
50
+ } from "./chunk-BGEPMNVB.js";
51
51
  import {
52
52
  getMutedTargetIds,
53
53
  isMuted,
54
54
  listMutes,
55
55
  muteMember,
56
56
  unmuteMember
57
- } from "./chunk-YEOQJ7WW.js";
57
+ } from "./chunk-KS3S6TJS.js";
58
58
  import {
59
59
  getMemberNotificationPrefs,
60
60
  isNotificationKindEnabled,
@@ -76,7 +76,7 @@ import {
76
76
  markNotificationsRead,
77
77
  resolveMentionedMembers,
78
78
  unreadNotificationCount
79
- } from "./chunk-XPD7EQML.js";
79
+ } from "./chunk-5NCTIJEN.js";
80
80
  import {
81
81
  applyReputation,
82
82
  getReputationAdapter,
@@ -93,9 +93,9 @@ import {
93
93
  resetProfanityAdapter,
94
94
  setProfanityAdapter
95
95
  } from "./chunk-KU5M27ZC.js";
96
- import "./chunk-EWVXP3GP.js";
97
- import "./chunk-TSCXXBOM.js";
98
- import "./chunk-EAYUAXW3.js";
96
+ import "./chunk-A3DPN2SP.js";
97
+ import "./chunk-AXOG2RJK.js";
98
+ import "./chunk-PXVCVEXV.js";
99
99
  import "./chunk-EQ2Z3KMD.js";
100
100
  import {
101
101
  listAuditEvents,
@@ -106,10 +106,10 @@ import {
106
106
  getCommunitySettings,
107
107
  updateCommunitySettings,
108
108
  validateCommunitySettingsPatch
109
- } from "./chunk-6MRTH734.js";
109
+ } from "./chunk-KNSOZ2NN.js";
110
110
  import "./chunk-EFZH6UPY.js";
111
111
  import "./chunk-4ZLMEKFX.js";
112
- import "./chunk-U4QCCLAW.js";
112
+ import "./chunk-MYUCR3SE.js";
113
113
  import "./chunk-SBCVAC2Z.js";
114
114
  import "./chunk-ZCINJSS4.js";
115
115
  import "./chunk-LSHHRDVR.js";
@@ -5,14 +5,14 @@ import {
5
5
  isVersionedPluginConfig,
6
6
  pluginConfigCacheTag,
7
7
  setPluginConfig
8
- } from "./chunk-2OWUHCFY.js";
9
- import "./chunk-TSCXXBOM.js";
10
- import "./chunk-EAYUAXW3.js";
8
+ } from "./chunk-ZIG5A4TD.js";
9
+ import "./chunk-AXOG2RJK.js";
10
+ import "./chunk-PXVCVEXV.js";
11
11
  import "./chunk-LMPYQLMH.js";
12
12
  import "./chunk-2KNG5KMM.js";
13
13
  import "./chunk-EFZH6UPY.js";
14
14
  import "./chunk-4ZLMEKFX.js";
15
- import "./chunk-U4QCCLAW.js";
15
+ import "./chunk-MYUCR3SE.js";
16
16
  import "./chunk-SBCVAC2Z.js";
17
17
  import "./chunk-ZCINJSS4.js";
18
18
  import "./chunk-V2UNHGAP.js";
@@ -30,4 +30,4 @@ export {
30
30
  pluginConfigCacheTag,
31
31
  setPluginConfig
32
32
  };
33
- //# sourceMappingURL=config-YDGNUDKP.js.map
33
+ //# sourceMappingURL=config-V75MS3II.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  buildDigestEmail,
3
3
  runDigestSweep
4
- } from "./chunk-K4CJ3KXB.js";
4
+ } from "./chunk-X43DI2QJ.js";
5
5
  import "./chunk-I4FSVEJK.js";
6
- import "./chunk-U4QCCLAW.js";
6
+ import "./chunk-MYUCR3SE.js";
7
7
  import "./chunk-ZCINJSS4.js";
8
8
  import "./chunk-LSHHRDVR.js";
9
9
  import "./chunk-Q7MK5ZKG.js";
@@ -14,4 +14,4 @@ export {
14
14
  buildDigestEmail,
15
15
  runDigestSweep
16
16
  };
17
- //# sourceMappingURL=digest-IWHMJPXI.js.map
17
+ //# sourceMappingURL=digest-U4X45I74.js.map
@@ -16,11 +16,11 @@ import {
16
16
  runHookAndCollect,
17
17
  runPluginScheduledTask,
18
18
  schedulePluginTask
19
- } from "./chunk-TSCXXBOM.js";
20
- import "./chunk-EAYUAXW3.js";
19
+ } from "./chunk-AXOG2RJK.js";
20
+ import "./chunk-PXVCVEXV.js";
21
21
  import "./chunk-EFZH6UPY.js";
22
22
  import "./chunk-4ZLMEKFX.js";
23
- import "./chunk-U4QCCLAW.js";
23
+ import "./chunk-MYUCR3SE.js";
24
24
  import "./chunk-SBCVAC2Z.js";
25
25
  import "./chunk-ZCINJSS4.js";
26
26
  import "./chunk-V2UNHGAP.js";
@@ -49,4 +49,4 @@ export {
49
49
  runPluginScheduledTask,
50
50
  schedulePluginTask
51
51
  };
52
- //# sourceMappingURL=host-HG4QGD3L.js.map
52
+ //# sourceMappingURL=host-IEQZ5A6K.js.map
package/dist/i18n.js CHANGED
@@ -24,13 +24,13 @@ import {
24
24
  setStrings,
25
25
  t,
26
26
  tSync
27
- } from "./chunk-2X3GBJOT.js";
27
+ } from "./chunk-MLQKHRW2.js";
28
28
  import {
29
29
  getI18nConfig,
30
30
  resetI18nConfig,
31
31
  setI18nConfig
32
32
  } from "./chunk-4ZLMEKFX.js";
33
- import "./chunk-U4QCCLAW.js";
33
+ import "./chunk-MYUCR3SE.js";
34
34
  import "./chunk-SBCVAC2Z.js";
35
35
  import "./chunk-ZCINJSS4.js";
36
36
  import "./chunk-Q7MK5ZKG.js";