@nexpress/core 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/{audit-43OLHR3U.js → audit-ZLNKBIDO.js} +3 -3
  2. package/dist/auth.js +4 -4
  3. package/dist/{can-UJ2NAOIR.js → can-U5F4JBZ7.js} +2 -2
  4. package/dist/{chunk-OMGQZ4Q5.js → chunk-2OWUHCFY.js} +2 -2
  5. package/dist/{chunk-OMGQZ4Q5.js.map → chunk-2OWUHCFY.js.map} +1 -1
  6. package/dist/{chunk-ELK6AVW5.js → chunk-2X3GBJOT.js} +2 -2
  7. package/dist/{chunk-HNX7COHQ.js → chunk-3SW4L3DL.js} +12 -12
  8. package/dist/chunk-3SW4L3DL.js.map +1 -0
  9. package/dist/{chunk-ML2E3P3X.js → chunk-5C22NDW4.js} +2 -2
  10. package/dist/chunk-5C22NDW4.js.map +1 -0
  11. package/dist/{chunk-RKM4GDWM.js → chunk-6MRTH734.js} +1 -1
  12. package/dist/chunk-6MRTH734.js.map +1 -0
  13. package/dist/{chunk-PW43RCJK.js → chunk-6OUWW6JF.js} +2 -2
  14. package/dist/{chunk-QBIJZZ5V.js → chunk-CGLJBRRX.js} +2 -2
  15. package/dist/chunk-CGLJBRRX.js.map +1 -0
  16. package/dist/{chunk-2VZZ7M26.js → chunk-EAYUAXW3.js} +3 -3
  17. package/dist/chunk-EAYUAXW3.js.map +1 -0
  18. package/dist/{chunk-2N53KKIL.js → chunk-EWVXP3GP.js} +2 -2
  19. package/dist/{chunk-CAS4Z6IN.js → chunk-I4FSVEJK.js} +1 -1
  20. package/dist/chunk-I4FSVEJK.js.map +1 -0
  21. package/dist/{chunk-LN6NTH6E.js → chunk-K4CJ3KXB.js} +3 -3
  22. package/dist/chunk-K4CJ3KXB.js.map +1 -0
  23. package/dist/{chunk-B7DTNT4O.js → chunk-MWLSXK6Y.js} +2 -2
  24. package/dist/{chunk-NFHS7CFV.js → chunk-Q7MK5ZKG.js} +2 -2
  25. package/dist/{chunk-PUV3VZPD.js → chunk-QZ52U4ET.js} +2 -2
  26. package/dist/{chunk-2GXH7566.js → chunk-SJ7M2VCC.js} +10 -10
  27. package/dist/chunk-SJ7M2VCC.js.map +1 -0
  28. package/dist/{chunk-6UV2P5MW.js → chunk-TIWJVQOO.js} +3 -3
  29. package/dist/chunk-TIWJVQOO.js.map +1 -0
  30. package/dist/{chunk-MLXKZK6G.js → chunk-TSCXXBOM.js} +76 -28
  31. package/dist/chunk-TSCXXBOM.js.map +1 -0
  32. package/dist/{chunk-L6VG7IK6.js → chunk-VBVLYFSZ.js} +2 -2
  33. package/dist/chunk-VBVLYFSZ.js.map +1 -0
  34. package/dist/{chunk-RDTTK27V.js → chunk-XPD7EQML.js} +3 -3
  35. package/dist/chunk-XPD7EQML.js.map +1 -0
  36. package/dist/{chunk-RJ76SKWQ.js → chunk-XU2GJJ6Z.js} +1 -1
  37. package/dist/chunk-XU2GJJ6Z.js.map +1 -0
  38. package/dist/{chunk-WJJ5MBH5.js → chunk-YEOQJ7WW.js} +1 -1
  39. package/dist/chunk-YEOQJ7WW.js.map +1 -0
  40. package/dist/community.js +14 -14
  41. package/dist/{config-YHUEYQ66.js → config-YDGNUDKP.js} +5 -5
  42. package/dist/{digest-ZODDTXA2.js → digest-IWHMJPXI.js} +4 -4
  43. package/dist/{host-XBGYIQEE.js → host-HG4QGD3L.js} +4 -4
  44. package/dist/i18n.js +2 -2
  45. package/dist/index.js +21 -21
  46. package/dist/index.js.map +1 -1
  47. package/dist/{job-log-N3IGI4NA.js → job-log-UY6ERPQZ.js} +3 -3
  48. package/dist/jobs.js +3 -3
  49. package/dist/{logger-2WUTTELV.js → logger-6ZGEKEMK.js} +2 -2
  50. package/dist/media.js +3 -3
  51. package/dist/{mentions-U4JACYI6.js → mentions-LQRZWAGO.js} +2 -2
  52. package/dist/{mutes-MNQP6ACF.js → mutes-PQA6U5X7.js} +2 -2
  53. package/dist/{notification-prefs-H4HFVCL7.js → notification-prefs-62NX2GBF.js} +2 -2
  54. package/dist/observability.js +2 -2
  55. package/dist/{reputation-ICIXDGPM.js → reputation-5DJLDBZY.js} +3 -3
  56. package/dist/{scheduled-S6IO47JD.js → scheduled-C2IKVZVK.js} +5 -5
  57. package/dist/seo.js +4 -4
  58. package/dist/{settings-OZWM6L2K.js → settings-NBAP7E5E.js} +2 -2
  59. package/dist/{strings-4EWJYDOG.js → strings-O2M7VSKV.js} +3 -3
  60. package/package.json +1 -1
  61. package/dist/chunk-2GXH7566.js.map +0 -1
  62. package/dist/chunk-2VZZ7M26.js.map +0 -1
  63. package/dist/chunk-6UV2P5MW.js.map +0 -1
  64. package/dist/chunk-CAS4Z6IN.js.map +0 -1
  65. package/dist/chunk-HNX7COHQ.js.map +0 -1
  66. package/dist/chunk-L6VG7IK6.js.map +0 -1
  67. package/dist/chunk-LN6NTH6E.js.map +0 -1
  68. package/dist/chunk-ML2E3P3X.js.map +0 -1
  69. package/dist/chunk-MLXKZK6G.js.map +0 -1
  70. package/dist/chunk-QBIJZZ5V.js.map +0 -1
  71. package/dist/chunk-RDTTK27V.js.map +0 -1
  72. package/dist/chunk-RJ76SKWQ.js.map +0 -1
  73. package/dist/chunk-RKM4GDWM.js.map +0 -1
  74. package/dist/chunk-WJJ5MBH5.js.map +0 -1
  75. /package/dist/{audit-43OLHR3U.js.map → audit-ZLNKBIDO.js.map} +0 -0
  76. /package/dist/{can-UJ2NAOIR.js.map → can-U5F4JBZ7.js.map} +0 -0
  77. /package/dist/{chunk-ELK6AVW5.js.map → chunk-2X3GBJOT.js.map} +0 -0
  78. /package/dist/{chunk-PW43RCJK.js.map → chunk-6OUWW6JF.js.map} +0 -0
  79. /package/dist/{chunk-2N53KKIL.js.map → chunk-EWVXP3GP.js.map} +0 -0
  80. /package/dist/{chunk-B7DTNT4O.js.map → chunk-MWLSXK6Y.js.map} +0 -0
  81. /package/dist/{chunk-NFHS7CFV.js.map → chunk-Q7MK5ZKG.js.map} +0 -0
  82. /package/dist/{chunk-PUV3VZPD.js.map → chunk-QZ52U4ET.js.map} +0 -0
  83. /package/dist/{config-YHUEYQ66.js.map → config-YDGNUDKP.js.map} +0 -0
  84. /package/dist/{digest-ZODDTXA2.js.map → digest-IWHMJPXI.js.map} +0 -0
  85. /package/dist/{host-XBGYIQEE.js.map → host-HG4QGD3L.js.map} +0 -0
  86. /package/dist/{job-log-N3IGI4NA.js.map → job-log-UY6ERPQZ.js.map} +0 -0
  87. /package/dist/{logger-2WUTTELV.js.map → logger-6ZGEKEMK.js.map} +0 -0
  88. /package/dist/{mentions-U4JACYI6.js.map → mentions-LQRZWAGO.js.map} +0 -0
  89. /package/dist/{mutes-MNQP6ACF.js.map → mutes-PQA6U5X7.js.map} +0 -0
  90. /package/dist/{notification-prefs-H4HFVCL7.js.map → notification-prefs-62NX2GBF.js.map} +0 -0
  91. /package/dist/{reputation-ICIXDGPM.js.map → reputation-5DJLDBZY.js.map} +0 -0
  92. /package/dist/{scheduled-S6IO47JD.js.map → scheduled-C2IKVZVK.js.map} +0 -0
  93. /package/dist/{settings-OZWM6L2K.js.map → settings-NBAP7E5E.js.map} +0 -0
  94. /package/dist/{strings-4EWJYDOG.js.map → strings-O2M7VSKV.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getLogger
3
- } from "./chunk-NFHS7CFV.js";
3
+ } from "./chunk-Q7MK5ZKG.js";
4
4
  import {
5
5
  getDb
6
6
  } from "./chunk-XANPEOJC.js";
@@ -72,4 +72,4 @@ export {
72
72
  resetReputationAdapter,
73
73
  applyReputation
74
74
  };
75
- //# sourceMappingURL=chunk-L6VG7IK6.js.map
75
+ //# sourceMappingURL=chunk-VBVLYFSZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/community/reputation.ts","../src/community/reputation-adapter.ts"],"sourcesContent":["import { eq, sql } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npMembers } from \"../db/schema/community.js\";\nimport { getLogger } from \"../observability/logger.js\";\n\nimport {\n getReputationAdapter,\n type NpReputationEvent,\n} from \"./reputation-adapter.js\";\n\n/**\n * Calls the registered reputation adapter for `event`, then applies\n * the returned delta to the affected member's reputation atomically:\n *\n * UPDATE np_members SET reputation = reputation + $delta\n * WHERE id = $memberId\n *\n * Failure modes are intentionally fail-soft — a buggy adapter that\n * throws, returns a non-finite value, or hits a transient DB error\n * MUST NOT block the underlying community write (comment insert,\n * reaction toggle, etc.). The caller's transactional state is not\n * touched; we just log + skip.\n */\nexport async function applyReputation(\n memberId: string,\n event: NpReputationEvent,\n): Promise<void> {\n let delta: number;\n try {\n delta = await getReputationAdapter().apply(event);\n } catch (err) {\n getLogger().warn(\"reputation adapter threw — skipping update\", {\n error: err instanceof Error ? err.message : String(err),\n kind: event.kind,\n memberId,\n });\n return;\n }\n\n if (!Number.isFinite(delta)) {\n getLogger().warn(\"reputation adapter returned non-finite delta\", {\n kind: event.kind,\n memberId,\n delta,\n });\n return;\n }\n const truncated = Math.trunc(delta);\n if (truncated === 0) return;\n\n const db = getDb();\n try {\n await db\n .update(npMembers)\n .set({\n reputation: sql`${npMembers.reputation} + ${truncated}`,\n updatedAt: new Date(),\n })\n .where(eq(npMembers.id, memberId));\n } catch (err) {\n getLogger().warn(\"reputation update failed — skipping\", {\n error: err instanceof Error ? err.message : String(err),\n kind: event.kind,\n memberId,\n delta: truncated,\n });\n }\n}\n","/**\n * Pluggable reputation-rules hook. Sites install an adapter via\n * `setReputationAdapter()` to compute reputation deltas in response\n * to community events; the framework then atomically applies the\n * delta to `np_members.reputation`.\n *\n * Default adapter is \"no-op\" (every event returns 0) — existing\n * sites' reputation values stay at zero until they opt in.\n *\n * Adapter is single-method by design: a tagged-union `event` is the\n * only argument, the return value is a signed integer delta. This\n * keeps the API surface small while letting sites encode arbitrary\n * weighting (e.g. \"+5 for a like on a comment, −10 for a moderator\n * hide, −0 if the reactor is a brand-new account, etc.\").\n *\n * Adapters can be sync or async — the framework awaits the result.\n * Throwing aborts only the reputation update, not the underlying\n * community write (fail-soft via observability hook, same pattern\n * as the spam adapter).\n */\nexport type NpReputationEvent =\n /** A new visible comment was inserted. Flagged / hidden / deleted\n * comments do NOT emit this event. */\n | {\n kind: \"comment.created\";\n commentId: string;\n memberId: string;\n targetType: string;\n targetId: string;\n }\n /** Mod (or member with the right grant) hid a comment. Adapters\n * typically penalize the author. */\n | {\n kind: \"comment.hidden\";\n commentId: string;\n memberId: string;\n byStaff: boolean;\n reason?: string | null;\n }\n /** Mod-side hard delete (`staffDeleteComment`). The body is wiped;\n * this is harsher than `hidden` and adapters usually penalize\n * more. */\n | {\n kind: \"comment.deleted\";\n commentId: string;\n memberId: string;\n byStaff: boolean;\n }\n /** Someone reacted to the recipient's content (comment / thread /\n * reply). `recipientId` is the content author; `reactorId` is the\n * member who clicked the reaction. Self-reactions are filtered\n * before the event fires. */\n | {\n kind: \"reaction.received\";\n reactionKind: string;\n recipientId: string;\n reactorId: string;\n targetType: string;\n targetId: string;\n }\n /** Reactor undid their reaction. Symmetric to `reaction.received`;\n * adapters typically return the negative of the corresponding\n * positive delta. */\n | {\n kind: \"reaction.removed\";\n reactionKind: string;\n recipientId: string;\n reactorId: string;\n targetType: string;\n targetId: string;\n }\n /** A member created a top-level document in a collection that\n * opted into `community.memberWrite.create` (Phase 9.7a). Fires\n * after the row + revision are persisted; adapters can credit\n * reputation for thread / post creation just like comments. */\n | {\n kind: \"document.created\";\n collectionSlug: string;\n documentId: string;\n memberId: string;\n }\n /** Author deleted their own document (`memberWrite.delete`,\n * Phase 9.7b). Symmetric to `document.created`; adapters\n * typically debit the original credit so a member can't farm\n * reputation by churn-creating and deleting threads. Mod-side\n * deletes are NOT covered here — those go through the staff\n * path which doesn't emit this event. */\n | {\n kind: \"document.deleted\";\n collectionSlug: string;\n documentId: string;\n memberId: string;\n };\n\nexport interface NpReputationAdapter {\n /** Returns the integer delta to apply to the affected member's\n * reputation. Sign matters: positive credits, negative debits.\n * Non-integer values are truncated; non-finite (NaN/Infinity)\n * values are skipped. Returning 0 is the no-op path. */\n apply(event: NpReputationEvent): number | Promise<number>;\n}\n\nconst NOOP_ADAPTER: NpReputationAdapter = { apply: () => 0 };\nlet currentAdapter: NpReputationAdapter = NOOP_ADAPTER;\n\nexport function setReputationAdapter(adapter: NpReputationAdapter): void {\n if (typeof adapter?.apply !== \"function\") {\n throw new Error(\"setReputationAdapter: adapter must implement apply()\");\n }\n currentAdapter = adapter;\n}\n\nexport function getReputationAdapter(): NpReputationAdapter {\n return currentAdapter;\n}\n\n/** Reset to the no-op adapter. Tests use this between cases. */\nexport function resetReputationAdapter(): void {\n currentAdapter = NOOP_ADAPTER;\n}\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,IAAI,WAAW;;;ACsGxB,IAAM,eAAoC,EAAE,OAAO,MAAM,EAAE;AAC3D,IAAI,iBAAsC;AAEnC,SAAS,qBAAqB,SAAoC;AACvE,MAAI,OAAO,SAAS,UAAU,YAAY;AACxC,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AACA,mBAAiB;AACnB;AAEO,SAAS,uBAA4C;AAC1D,SAAO;AACT;AAGO,SAAS,yBAA+B;AAC7C,mBAAiB;AACnB;;;AD/FA,eAAsB,gBACpB,UACA,OACe;AACf,MAAI;AACJ,MAAI;AACF,YAAQ,MAAM,qBAAqB,EAAE,MAAM,KAAK;AAAA,EAClD,SAAS,KAAK;AACZ,cAAU,EAAE,KAAK,mDAA8C;AAAA,MAC7D,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MACtD,MAAM,MAAM;AAAA,MACZ;AAAA,IACF,CAAC;AACD;AAAA,EACF;AAEA,MAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,cAAU,EAAE,KAAK,gDAAgD;AAAA,MAC/D,MAAM,MAAM;AAAA,MACZ;AAAA,MACA;AAAA,IACF,CAAC;AACD;AAAA,EACF;AACA,QAAM,YAAY,KAAK,MAAM,KAAK;AAClC,MAAI,cAAc,EAAG;AAErB,QAAM,KAAK,MAAM;AACjB,MAAI;AACF,UAAM,GACH,OAAO,SAAS,EAChB,IAAI;AAAA,MACH,YAAY,MAAM,UAAU,UAAU,MAAM,SAAS;AAAA,MACrD,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC,EACA,MAAM,GAAG,UAAU,IAAI,QAAQ,CAAC;AAAA,EACrC,SAAS,KAAK;AACZ,cAAU,EAAE,KAAK,4CAAuC;AAAA,MACtD,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MACtD,MAAM,MAAM;AAAA,MACZ;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;","names":[]}
@@ -24,7 +24,7 @@ import { inArray as inArray2 } from "drizzle-orm";
24
24
  import { and, count, desc, eq, isNull, inArray } from "drizzle-orm";
25
25
  async function createNotification(input) {
26
26
  if (input.actorMemberId && input.actorMemberId !== input.memberId) {
27
- const { isMuted } = await import("./mutes-MNQP6ACF.js");
27
+ const { isMuted } = await import("./mutes-PQA6U5X7.js");
28
28
  const muted = await isMuted({
29
29
  memberId: input.memberId,
30
30
  targetId: input.actorMemberId
@@ -32,7 +32,7 @@ async function createNotification(input) {
32
32
  if (muted) return null;
33
33
  }
34
34
  {
35
- const { isNotificationKindEnabled } = await import("./notification-prefs-H4HFVCL7.js");
35
+ const { isNotificationKindEnabled } = await import("./notification-prefs-62NX2GBF.js");
36
36
  const enabled = await isNotificationKindEnabled(input.memberId, input.kind);
37
37
  if (!enabled) return null;
38
38
  }
@@ -219,4 +219,4 @@ export {
219
219
  resolveMentionedMembers,
220
220
  fanOutMentionNotifications
221
221
  };
222
- //# sourceMappingURL=chunk-RDTTK27V.js.map
222
+ //# sourceMappingURL=chunk-XPD7EQML.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/community/mentions.ts","../src/community/notifications.ts"],"sourcesContent":["import { inArray } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npMembers } from \"../db/schema/community.js\";\n\nimport { createNotification } from \"./notifications.js\";\n\n/**\n * Phase 16.2 — @mention extraction + notification fan-out.\n *\n * The mention vocabulary mirrors the handle constraint enforced\n * during registration (`/^[a-z0-9][a-z0-9_-]{2,29}$/`). The matcher\n * uses a negative lookbehind so `email@host.com` doesn't trigger a\n * mention, plus a negative lookahead so `@alice-` (handle followed\n * by a hyphen that's not part of the handle) is rejected — handles\n * end at non-handle characters, never mid-symbol.\n *\n * Fan-out semantics:\n * - Self-mentions are skipped (the author already knows).\n * - Caller-supplied `exclude` set lets the comment write path\n * skip the parent author so they don't get both `comment.reply`\n * AND `comment.mention`.\n * - Caller-supplied `previousHandles` lets the edit path only\n * notify newly-added mentions (otherwise toggling a single\n * other word in a comment would re-notify everyone).\n * - Inactive / banned / deleted members are filtered out at\n * resolve time.\n * - Mute is enforced inside `createNotification` (the\n * recipient's mute list drops actor-keyed notifications).\n */\n\n/** Source-of-truth handle pattern, kept in sync with `apps/web` register routes. */\nexport const MENTION_HANDLE_RE = /^[a-z0-9][a-z0-9_-]{2,29}$/;\n\nconst MENTION_PATTERN = /(?<![A-Za-z0-9_])@([a-z0-9][a-z0-9_-]{2,29})(?![A-Za-z0-9_-])/g;\n\nexport interface NpMentionTarget {\n id: string;\n handle: string;\n}\n\n/**\n * Extract unique mention handles from plain text or markdown source.\n * Order is preserved (first appearance wins) so a UI that wants to\n * display \"you mentioned @alice and @bob\" gets the same order as\n * the body text.\n */\nexport function extractMentionHandles(source: string): string[] {\n if (!source) return [];\n const seen = new Set<string>();\n const out: string[] = [];\n for (const match of source.matchAll(MENTION_PATTERN)) {\n const handle = match[1]?.toLowerCase();\n if (!handle || seen.has(handle)) continue;\n seen.add(handle);\n out.push(handle);\n }\n return out;\n}\n\n/**\n * Walk a Lexical-shaped rich-text payload, concatenate its text\n * nodes, and run the mention extractor over the joined result.\n * Mirrors the search-index walker (`collections/search.ts`) so a\n * mention split across two adjacent text spans (e.g. `@` and\n * `alice` in different runs because of formatting toggles) still\n * resolves correctly — text nodes are joined without separators.\n */\nexport function extractMentionHandlesFromRichText(content: unknown): string[] {\n if (!content || typeof content !== \"object\") return [];\n const root = (content as { root?: { children?: unknown } }).root;\n if (!root || !Array.isArray(root.children)) return [];\n const parts: string[] = [];\n walkRichTextNodes(root.children, parts);\n return extractMentionHandles(parts.join(\"\"));\n}\n\nfunction walkRichTextNodes(nodes: unknown[], parts: string[]): void {\n for (const node of nodes) {\n if (!node || typeof node !== \"object\") continue;\n const n = node as Record<string, unknown>;\n if (typeof n.text === \"string\") parts.push(n.text);\n if (Array.isArray(n.children)) walkRichTextNodes(n.children, parts);\n }\n}\n\n/**\n * Scan a collection-document data payload (the same shape passed\n * to `createMemberDocument` / `updateMemberDocument`) and pull\n * out every mention handle it contains. String values are scanned\n * with the markdown extractor; object values shaped like Lexical\n * rich text (`{ root: { children: [...] } }`) are walked. Other\n * values are ignored.\n *\n * Field names are not assumed: any string or rich-text field\n * contributes. The mention pattern is anchored to `@<handle>`\n * with handle-shape constraints, so unrelated string fields\n * (`category: \"news\"`) won't trigger false positives.\n */\nexport function extractMentionHandlesFromDocData(data: Record<string, unknown>): string[] {\n if (!data || typeof data !== \"object\") return [];\n const seen = new Set<string>();\n for (const value of Object.values(data)) {\n if (typeof value === \"string\") {\n for (const h of extractMentionHandles(value)) seen.add(h);\n continue;\n }\n if (value && typeof value === \"object\") {\n const root = (value as { root?: { children?: unknown } }).root;\n if (root && Array.isArray(root.children)) {\n for (const h of extractMentionHandlesFromRichText(value)) seen.add(h);\n }\n }\n }\n return Array.from(seen);\n}\n\n/**\n * Resolve handles to active member ids. Inactive / banned /\n * deleted members are filtered out so a mention of an account\n * the site no longer wants to notify is silently dropped (rather\n * than raising an error to the writer — the writer can't tell the\n * difference between \"typo\" and \"account closed\", and either way\n * the right behaviour is \"no notification\").\n *\n * Lookups are case-insensitive on the handle (the storage column\n * stores the canonical lowercased form).\n */\nexport async function resolveMentionedMembers(handles: string[]): Promise<NpMentionTarget[]> {\n if (handles.length === 0) return [];\n const lower = Array.from(new Set(handles.map((h) => h.toLowerCase())));\n const db = getDb();\n const rows = (await db\n .select({ id: npMembers.id, handle: npMembers.handle, status: npMembers.status })\n .from(npMembers)\n .where(inArray(npMembers.handle, lower))) as Array<{\n id: string;\n handle: string;\n status: string;\n }>;\n return rows.filter((r) => r.status === \"active\").map((r) => ({ id: r.id, handle: r.handle }));\n}\n\nexport interface FanOutMentionsInput {\n /** The author whose write triggered the fan-out. Self-mentions are skipped. */\n actorMemberId: string;\n /** Notification `kind` (e.g. `\"comment.mention\"`, `\"discussion.mention\"`). */\n kind: string;\n /**\n * Plain text or markdown to scan. Either `source` or `content`\n * (or both) must be provided; if both are set the handles are\n * unioned.\n */\n source?: string;\n /** Lexical-shaped rich-text JSON to scan. */\n content?: unknown;\n /**\n * Collection-document data payload to scan. All string +\n * rich-text fields contribute. Useful for the\n * `createMemberDocument` / `updateMemberDocument` paths.\n */\n data?: Record<string, unknown>;\n /**\n * Recipients that already received a notification for this same\n * event (e.g. the parent author got `comment.reply`). They are\n * skipped to avoid the \"two pings for one comment\" pattern.\n */\n exclude?: ReadonlySet<string>;\n /** Merged into the notification payload. `mentionedMemberId` is added automatically. */\n payload?: Record<string, unknown>;\n /**\n * Edit path: handles that were present in the prior revision\n * are skipped so toggling unrelated words doesn't re-notify\n * everyone already mentioned.\n */\n previousHandles?: ReadonlySet<string>;\n}\n\n/**\n * Fan-out mention notifications. Returns the number of\n * notifications actually inserted (mute / inactive / self / dedup\n * exclusions all reduce the count).\n */\nexport async function fanOutMentionNotifications(input: FanOutMentionsInput): Promise<number> {\n const handles = new Set<string>();\n if (input.source) {\n for (const h of extractMentionHandles(input.source)) handles.add(h);\n }\n if (input.content !== undefined) {\n for (const h of extractMentionHandlesFromRichText(input.content)) handles.add(h);\n }\n if (input.data) {\n for (const h of extractMentionHandlesFromDocData(input.data)) handles.add(h);\n }\n if (input.previousHandles) {\n for (const prev of input.previousHandles) handles.delete(prev);\n }\n if (handles.size === 0) return 0;\n\n const targets = await resolveMentionedMembers(Array.from(handles));\n let fired = 0;\n for (const t of targets) {\n if (t.id === input.actorMemberId) continue;\n if (input.exclude?.has(t.id)) continue;\n const row = await createNotification({\n memberId: t.id,\n kind: input.kind,\n actorMemberId: input.actorMemberId,\n payload: {\n ...(input.payload ?? {}),\n mentionedMemberId: t.id,\n mentionedHandle: t.handle,\n },\n });\n if (row) fired += 1;\n }\n return fired;\n}\n","import { and, count, desc, eq, isNull, inArray } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npNotifications } from \"../db/schema/community.js\";\nimport { NpForbiddenError, NpValidationError } from \"../errors.js\";\nimport { getCurrentSiteId, requireSiteId } from \"../sites/context.js\";\nimport { NP_DEFAULT_SITE_ID } from \"../sites/registry.js\";\n\n/**\n * Per-member notification inbox. v1 is synchronous: every event that\n * generates a notification writes a row immediately. The inbox is\n * in-app only — email fan-out and per-member frequency preferences\n * are out of scope for the shipped roadmap.\n *\n * `kind` is a free-form string. The current vocabulary:\n * - `comment.reply` — your comment got a reply\n * - `reaction.received` — someone reacted to your content\n * - `follow.received` — someone followed you\n * Plugins can write their own kinds; the recipient UI fans them out\n * to whichever rendering it knows.\n */\n\nexport interface NpNotificationRow {\n id: string;\n memberId: string;\n kind: string;\n payload: Record<string, unknown>;\n readAt: Date | null;\n createdAt: Date;\n}\n\nexport interface CreateNotificationInput {\n /** The recipient — whose inbox this lands in. */\n memberId: string;\n kind: string;\n payload?: Record<string, unknown>;\n /**\n * Phase 16.1 — the member whose action triggered the\n * notification (e.g. the comment author, the reactor, the\n * follower). When set, the recipient's mute list is\n * consulted: if the recipient has muted the actor, the\n * notification is silently dropped. Returns `null` from\n * the call site.\n *\n * Optional because some kinds are actor-less (system\n * notices, scheduled reminders).\n */\n actorMemberId?: string | null;\n}\n\nexport async function createNotification(\n input: CreateNotificationInput,\n): Promise<NpNotificationRow | null> {\n // Mute check — defer the import to avoid a notifications →\n // mutes circular at module load. Mutes module imports\n // nothing back from here, but TypeScript sometimes flags\n // the cycle anyway depending on resolver order.\n if (input.actorMemberId && input.actorMemberId !== input.memberId) {\n const { isMuted } = await import(\"./mutes.js\");\n const muted = await isMuted({\n memberId: input.memberId,\n targetId: input.actorMemberId,\n });\n if (muted) return null;\n }\n\n // Phase 16.3 — recipient-controlled kind toggle. Fails open\n // on read error (transient DB blip shouldn't silently swallow\n // notifications). Deferred import for the same reason as\n // mutes.\n {\n const { isNotificationKindEnabled } = await import(\"./notification-prefs.js\");\n const enabled = await isNotificationKindEnabled(input.memberId, input.kind);\n if (!enabled) return null;\n }\n\n const db = getDb();\n // Phase 18 — site comes from the request resolver. The\n // notification belongs to the tenant where the actor's\n // action happened (a reaction on tenant A → notification\n // shows up in the recipient's tenant-A inbox).\n // #272 — write: must NOT silently fall through; an actor on\n // tenant A would otherwise create a notification on the\n // default tenant.\n const siteId = await requireSiteId();\n const [row] = (await db\n .insert(npNotifications)\n .values({\n memberId: input.memberId,\n kind: input.kind,\n payload: input.payload ?? {},\n siteId,\n })\n .returning()) as NpNotificationRow[];\n if (!row) throw new Error(\"Notification insert returned no row\");\n return row;\n}\n\nexport interface ListNotificationsOptions {\n /** Default 50, max 200. */\n limit?: number;\n /** Default 0. */\n offset?: number;\n /** When true, returns only unread. */\n unreadOnly?: boolean;\n}\n\nexport interface NpNotificationListResult {\n notifications: NpNotificationRow[];\n totalDocs: number;\n unread: number;\n}\n\nexport async function listNotifications(\n memberId: string,\n options: ListNotificationsOptions = {},\n): Promise<NpNotificationListResult> {\n const db = getDb();\n const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);\n const offset = Math.max(options.offset ?? 0, 0);\n\n // Phase 18 — inbox is per-site. A member who's active on\n // multiple tenants sees a separate notification list on each.\n const siteId = (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const baseWhere = and(eq(npNotifications.memberId, memberId), eq(npNotifications.siteId, siteId));\n const where = options.unreadOnly ? and(baseWhere, isNull(npNotifications.readAt)) : baseWhere;\n\n const rows = (await db\n .select()\n .from(npNotifications)\n .where(where)\n .orderBy(desc(npNotifications.createdAt))\n .limit(limit)\n .offset(offset)) as NpNotificationRow[];\n\n const [totalRow] = (await db\n .select({ total: count() })\n .from(npNotifications)\n .where(where)) as Array<{ total: number | string }>;\n\n const [unreadRow] = (await db\n .select({ total: count() })\n .from(npNotifications)\n .where(and(baseWhere, isNull(npNotifications.readAt)))) as Array<{\n total: number | string;\n }>;\n\n return {\n notifications: rows,\n totalDocs: Number(totalRow?.total ?? 0),\n unread: Number(unreadRow?.total ?? 0),\n };\n}\n\nexport async function unreadNotificationCount(memberId: string): Promise<number> {\n const db = getDb();\n // Phase 18 — count only notifications on the current site.\n const siteId = (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const [row] = (await db\n .select({ total: count() })\n .from(npNotifications)\n .where(\n and(\n eq(npNotifications.memberId, memberId),\n eq(npNotifications.siteId, siteId),\n isNull(npNotifications.readAt),\n ),\n )) as Array<{ total: number | string }>;\n return Number(row?.total ?? 0);\n}\n\nexport interface MarkReadInput {\n memberId: string;\n notificationIds: string[];\n}\n\nexport async function markNotificationsRead(input: MarkReadInput): Promise<number> {\n if (input.notificationIds.length === 0) return 0;\n if (input.notificationIds.length > 200) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"notificationIds\", message: \"Up to 200 ids per request\" },\n ]);\n }\n const db = getDb();\n // Issue #219 — scope the update to the current site so a member\n // active on multiple tenants can't mark IDs read across tenants\n // by passing a site-A request that names site-B notification ids.\n // The caller's existing `memberId` predicate covered ownership\n // but not tenant; without this, unread counts on the other site\n // would silently drop. Using `returning({ id })` also gives us\n // an exact count instead of a follow-up SELECT — replaces the\n // pre-existing best-effort COUNT round trip.\n // #272 — write: must NOT silently fall through.\n const siteId = await requireSiteId();\n const updated = (await db\n .update(npNotifications)\n .set({ readAt: new Date() })\n .where(\n and(\n eq(npNotifications.memberId, input.memberId),\n eq(npNotifications.siteId, siteId),\n inArray(npNotifications.id, input.notificationIds),\n isNull(npNotifications.readAt),\n ),\n )\n .returning({ id: npNotifications.id })) as Array<{ id: string }>;\n return updated.length;\n}\n\nexport async function markAllNotificationsRead(memberId: string): Promise<number> {\n const db = getDb();\n // Phase 18 — \"mark all read\" only marks the current site's\n // inbox so a member doesn't accidentally clear another\n // tenant's unread count when toggling on this one.\n // #272 — write: must NOT silently fall through.\n const siteId = await requireSiteId();\n const before = await unreadNotificationCount(memberId);\n await db\n .update(npNotifications)\n .set({ readAt: new Date() })\n .where(\n and(\n eq(npNotifications.memberId, memberId),\n eq(npNotifications.siteId, siteId),\n isNull(npNotifications.readAt),\n ),\n );\n return before;\n}\n\n/**\n * Internal sanity check used by the API: throws when one principal\n * tries to read another member's notification. Centralised here\n * because every per-id route gets the same rule.\n */\nexport async function assertOwnsNotification(\n memberId: string,\n notificationId: string,\n): Promise<void> {\n const db = getDb();\n const [row] = (await db\n .select({ memberId: npNotifications.memberId })\n .from(npNotifications)\n .where(eq(npNotifications.id, notificationId))\n .limit(1)) as Array<{ memberId: string }>;\n if (!row || row.memberId !== memberId) {\n throw new NpForbiddenError(\"notification\", \"read\");\n }\n}\n"],"mappings":"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","names":["inArray","inArray"]}
@@ -176,4 +176,4 @@ export {
176
176
  withMemberWrite,
177
177
  memberCan
178
178
  };
179
- //# sourceMappingURL=chunk-RJ76SKWQ.js.map
179
+ //# sourceMappingURL=chunk-XU2GJJ6Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/community/can.ts","../src/community/roles.ts"],"sourcesContent":["import { and, eq, gt, isNull, or } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { NpForbiddenError } from \"../errors.js\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npBans, npMemberRoles } from \"../db/schema/community.js\";\nimport { getCurrentSiteId } from \"../sites/context.js\";\nimport { NP_DEFAULT_SITE_ID } from \"../sites/registry.js\";\nimport { type CommunityCapability, type CommunityScope, getCommunityRole } from \"./roles.js\";\n\n/**\n * Active-ban probe shared by `memberCan` and direct write-path\n * callers. The community write services (`createComment`,\n * `addReaction`, `fileReport`, `follow`) call `assertNotBanned`\n * straight away — they never went through `memberCan`, so without\n * this gate banned members could still write community content\n * even though their bans were recorded. (#53)\n *\n * Ban-match rules:\n * - `site` ban → blocks every write.\n * - `category` / `collection` ban → blocks when the action's scope\n * chain contains the matching scope.\n *\n * The `or()` helper is required for the `expires_at IS NULL OR\n * expires_at > now` clause; the previous raw `sql` template let\n * Postgres' AND-binds-tighter-than-OR rule re-associate and leak\n * other members' bans (same precedence trap as #006 in 9.5\n * postmortem).\n */\nexport async function isMemberBanned(\n memberId: string,\n scopes: ReadonlyArray<{ type: CommunityScope; id: string }> = [],\n db?: NodePgDatabase<Record<string, unknown>>,\n now: Date = new Date(),\n): Promise<boolean> {\n const handle = db ?? (getDb());\n // Phase 18 — bans are tenant-scoped. A site-wide ban on\n // tenant A doesn't block writes on tenant B; the ban row\n // includes `site_id` and we filter by the resolver's\n // current value.\n const siteId = (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const bans = (await handle\n .select({\n scopeType: npBans.scopeType,\n scopeId: npBans.scopeId,\n })\n .from(npBans)\n .where(\n and(\n eq(npBans.memberId, memberId),\n eq(npBans.siteId, siteId),\n or(isNull(npBans.expiresAt), gt(npBans.expiresAt, now)),\n ),\n )) as Array<{\n scopeType: \"site\" | \"category\" | \"collection\";\n scopeId: string | null;\n }>;\n\n return bans.some((ban) => {\n if (ban.scopeType === \"site\") return true;\n return scopes.some((s) => s.type === ban.scopeType && s.id === ban.scopeId);\n });\n}\n\n/**\n * Throws `NpForbiddenError` if the member is currently banned for any\n * scope in the chain. Used at the top of community write services\n * before any DB mutation. Pre-existing `memberCan` enforces the same\n * rule for permission-based actions; this helper is the catch-all\n * for write paths that don't go through capability checks.\n */\nexport async function assertNotBanned(\n memberId: string,\n scopes: ReadonlyArray<{ type: CommunityScope; id: string }> = [],\n): Promise<void> {\n if (await isMemberBanned(memberId, scopes)) {\n throw new NpForbiddenError(\"community\", \"banned\");\n }\n}\n\n/**\n * Structural enforcement of the ban-check gate (#311). Every\n * community write service should run inside this wrapper — the ban\n * check fires before `fn` and a service author can't accidentally\n * ship a new write path that skips it.\n *\n * Pre-validation that doesn't write (input shape, target lookup\n * existence) can run *before* this call; the gate is specifically\n * for the moment between \"we know enough to attempt the write\" and\n * the first DB mutation.\n *\n * `scopes` is the same chain `assertNotBanned` accepts — pass\n * `[{ type: \"collection\", id: targetType }]` for collection-scoped\n * actions, leave empty for site-wide-only enforcement (e.g. follows,\n * polymorphic-target reactions where no obvious scope chain exists).\n */\nexport async function withMemberWrite<T>(\n memberId: string,\n scopes: ReadonlyArray<{ type: CommunityScope; id: string }>,\n fn: () => Promise<T>,\n): Promise<T> {\n await assertNotBanned(memberId, scopes);\n return fn();\n}\n\n/**\n * Action a member is attempting. Most actions are real\n * `CommunityCapability` literals — those map 1:1 to a role's\n * capability list. The two exceptions are `\"edit-own\"` and\n * `\"delete-own\"`, which short-circuit on ownership without consulting\n * grants at all.\n */\nexport type MemberAction = CommunityCapability | \"edit-own\" | \"delete-own\";\n\n/**\n * Caller-provided context for a permission check. The caller — the\n * comment service, a future thread service, etc. — provides the\n * target's ownership + scope chain rather than `memberCan` looking\n * it up via a polymorphic join. This keeps the resolver decoupled\n * from the per-target table layout, and lets the surface evolve\n * without touching this resolver.\n */\nexport interface MemberCanTarget {\n /** Free-form target type — `\"comment\" | \"thread\" | \"reply\" | \"category\" | \"report\" | \"member\"`. */\n type: string;\n /** Stable id for logs / future denial reasons. */\n id: string;\n /** Member id of the target's author. Required for own-action checks. */\n ownerId?: string;\n /**\n * Scope chain from most specific to least specific. A reply might\n * provide `[{ type: \"thread\", id: \"<threadId>\" }, { type: \"category\",\n * id: \"<categoryId>\" }]`; the resolver also checks site-wide grants\n * regardless of what's in the chain.\n */\n scopes?: ReadonlyArray<{ type: CommunityScope; id: string }>;\n}\n\ninterface MemberCanOptions {\n /** Override the DB handle (tests). Defaults to `getDb()`. */\n db?: NodePgDatabase<Record<string, unknown>>;\n /** Reference time for ban/grant expiry checks. Defaults to `new Date()`. */\n now?: Date;\n}\n\n/**\n * Returns true when `memberId` is allowed to perform `action` on\n * `target`. Walk order:\n *\n * 1. Active scoped ban → deny everything.\n * 2. `edit-own` / `delete-own` → allow only when `target.ownerId === memberId`.\n * 3. Site-wide grants whose role's capability list includes `action`.\n * 4. Scoped grants matching any element of `target.scopes`, whose role\n * includes `action`.\n * 5. Otherwise deny.\n *\n * The resolver ignores staff (`np_users`) entirely. Staff bypass is the\n * caller's responsibility — typically `principalCan(principal, …)` at\n * the API layer, which routes to `memberCan` only when the principal\n * is a member.\n */\nexport async function memberCan(\n memberId: string,\n action: MemberAction,\n target: MemberCanTarget,\n options: MemberCanOptions = {},\n): Promise<boolean> {\n const db = options.db ?? (getDb());\n const now = options.now ?? new Date();\n const scopes = target.scopes ?? [];\n\n // Step 1: ban check. Site-wide bans always apply; scoped bans match\n // when the target's scope chain contains the ban's scope.\n const isBanned = await isMemberBanned(memberId, scopes, db, now);\n if (isBanned) return false;\n\n // Step 2: ownership shortcut for own-content actions.\n if (action === \"edit-own\" || action === \"delete-own\") {\n return Boolean(target.ownerId) && target.ownerId === memberId;\n }\n\n // Step 3+4: walk grants. Pull the member's unexpired grants\n // on the current tenant only — a community-mod on tenant A\n // shouldn't authorize actions on tenant B. Site-wide grants\n // (scope_type='site') still match every action on the\n // resolved tenant.\n const siteId = (await getCurrentSiteId()) ?? NP_DEFAULT_SITE_ID;\n const grants = (await db\n .select({\n role: npMemberRoles.role,\n scopeType: npMemberRoles.scopeType,\n scopeId: npMemberRoles.scopeId,\n })\n .from(npMemberRoles)\n .where(\n and(\n eq(npMemberRoles.memberId, memberId),\n eq(npMemberRoles.siteId, siteId),\n or(isNull(npMemberRoles.expiresAt), gt(npMemberRoles.expiresAt, now)),\n ),\n )) as Array<{\n role: string;\n scopeType: CommunityScope;\n scopeId: string | null;\n }>;\n\n for (const grant of grants) {\n const def = getCommunityRole(grant.role, grant.scopeType);\n if (!def) continue;\n if (!def.capabilities.includes(action)) continue;\n\n if (grant.scopeType === \"site\") {\n return true;\n }\n const matchesTargetScope = scopes.some(\n (s) => s.type === grant.scopeType && s.id === grant.scopeId,\n );\n if (matchesTargetScope) return true;\n }\n\n return false;\n}\n","/**\n * Community role registry. Maps a role name + scope type to the\n * capabilities a grant of that role unlocks. Plugins extend the registry\n * via `registerCommunityRole(...)` (gated by the `members:write` or\n * `community:moderate` capability — enforced at registration time, not\n * here).\n *\n * The capability vocabulary is the single source of truth for \"what\n * actions exist in the community.\" `memberCan()` (../community/can.ts)\n * looks up grants and matches their roles' capability lists against the\n * requested action.\n */\n\nexport type CommunityScope = \"site\" | \"category\" | \"collection\" | \"thread\";\n\n/**\n * Action vocabulary. Adding new actions later is fine, but rename with\n * care — built-in role definitions reference these literals and a\n * silent typo widens permissions instead of narrowing them.\n */\nexport type CommunityCapability =\n | \"hide-comment\"\n | \"restore-comment\"\n | \"edit-any-comment\"\n | \"delete-any-comment\"\n | \"hide-thread\"\n | \"restore-thread\"\n | \"lock-thread\"\n | \"unlock-thread\"\n | \"pin-thread\"\n | \"unpin-thread\"\n | \"edit-any-thread\"\n | \"delete-any-thread\"\n | \"edit-own-thread\"\n | \"lock-own-thread\"\n | \"ban-member\"\n | \"unban-member\"\n | \"resolve-report\"\n | \"manage-category\"\n | \"view-staff-tools\";\n\nexport interface CommunityRoleDefinition {\n /** e.g. `\"category-mod\"`. Plugins can ship custom roles like `\"tag-mod\"`. */\n role: string;\n /** What kind of scope a grant of this role applies to. */\n scopeType: CommunityScope;\n /** Capabilities a grant of this role unlocks within its scope. */\n capabilities: readonly CommunityCapability[];\n /**\n * Human-readable label for admin UIs that surface a role picker. Falls\n * back to `role` when omitted.\n */\n label?: string;\n /** Optional plugin id that registered this role; null for built-ins. */\n source?: string;\n}\n\nconst ALL_MOD_CAPS: readonly CommunityCapability[] = [\n \"hide-comment\",\n \"restore-comment\",\n \"edit-any-comment\",\n \"delete-any-comment\",\n \"hide-thread\",\n \"restore-thread\",\n \"lock-thread\",\n \"unlock-thread\",\n \"pin-thread\",\n \"unpin-thread\",\n \"edit-any-thread\",\n \"delete-any-thread\",\n \"ban-member\",\n \"unban-member\",\n \"resolve-report\",\n \"view-staff-tools\",\n];\n\nconst builtInRoles: CommunityRoleDefinition[] = [\n {\n role: \"community-mod\",\n scopeType: \"site\",\n label: \"Community moderator\",\n capabilities: [...ALL_MOD_CAPS, \"manage-category\"],\n },\n {\n role: \"category-mod\",\n scopeType: \"category\",\n label: \"Category moderator\",\n capabilities: ALL_MOD_CAPS,\n },\n {\n role: \"collection-mod\",\n scopeType: \"collection\",\n label: \"Collection moderator\",\n // Collection-mods only have authority over the comments under a\n // collection's documents. Thread-only capabilities don't apply, so\n // they're omitted on purpose.\n capabilities: [\n \"hide-comment\",\n \"restore-comment\",\n \"edit-any-comment\",\n \"delete-any-comment\",\n \"ban-member\",\n \"unban-member\",\n \"resolve-report\",\n \"view-staff-tools\",\n ],\n },\n {\n role: \"thread-author\",\n scopeType: \"thread\",\n label: \"Thread author\",\n // Auto-granted on thread create. Lets the OP edit / lock their own\n // thread without giving them broader powers.\n capabilities: [\"edit-own-thread\", \"lock-own-thread\"],\n },\n];\n\nconst customRoles: CommunityRoleDefinition[] = [];\n\nfunction key(role: string, scopeType: CommunityScope): string {\n return `${scopeType}:${role}`;\n}\n\n/**\n * Plugins call this from setup() to add their own role kinds. Throws\n * when the (role, scopeType) pair is already registered to keep the\n * registry deterministic — a plugin overriding a built-in role would\n * silently widen permissions and is almost always a mistake.\n */\nexport function registerCommunityRole(definition: CommunityRoleDefinition): void {\n const composite = key(definition.role, definition.scopeType);\n if (\n builtInRoles.some((b) => key(b.role, b.scopeType) === composite) ||\n customRoles.some((c) => key(c.role, c.scopeType) === composite)\n ) {\n throw new Error(\n `[community] role \"${definition.role}\" already registered for scope \"${definition.scopeType}\".`,\n );\n }\n customRoles.push({ ...definition });\n}\n\n/** Look up a role by `(role, scopeType)`. Returns undefined when unknown. */\nexport function getCommunityRole(\n role: string,\n scopeType: CommunityScope,\n): CommunityRoleDefinition | undefined {\n const composite = key(role, scopeType);\n return (\n builtInRoles.find((b) => key(b.role, b.scopeType) === composite) ??\n customRoles.find((c) => key(c.role, c.scopeType) === composite)\n );\n}\n\n/**\n * Returns every role currently registered, built-ins first then\n * plugin-defined. Used by the admin role picker to render selectable\n * options for a given scope.\n */\nexport function listCommunityRoles(scopeType?: CommunityScope): CommunityRoleDefinition[] {\n const all = [...builtInRoles, ...customRoles];\n return scopeType ? all.filter((r) => r.scopeType === scopeType) : all;\n}\n\n/** Tests reset state between cases; production callers should never need this. */\nexport function resetCommunityRoles(): void {\n customRoles.length = 0;\n}\n"],"mappings":"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","names":[]}
@@ -98,4 +98,4 @@ export {
98
98
  getMutedTargetIds,
99
99
  listMutes
100
100
  };
101
- //# sourceMappingURL=chunk-WJJ5MBH5.js.map
101
+ //# sourceMappingURL=chunk-YEOQJ7WW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/community/mutes.ts"],"sourcesContent":["import { and, desc, eq } from \"drizzle-orm\";\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();\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();\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();\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();\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();\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":[]}
package/dist/community.js CHANGED
@@ -33,12 +33,12 @@ import {
33
33
  unfollow,
34
34
  unresolvedReportCount,
35
35
  updateComment
36
- } from "./chunk-HNX7COHQ.js";
37
- import "./chunk-PW43RCJK.js";
36
+ } from "./chunk-3SW4L3DL.js";
37
+ import "./chunk-6OUWW6JF.js";
38
38
  import {
39
39
  buildDigestEmail,
40
40
  runDigestSweep
41
- } from "./chunk-LN6NTH6E.js";
41
+ } from "./chunk-K4CJ3KXB.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-RJ76SKWQ.js";
50
+ } from "./chunk-XU2GJJ6Z.js";
51
51
  import {
52
52
  getMutedTargetIds,
53
53
  isMuted,
54
54
  listMutes,
55
55
  muteMember,
56
56
  unmuteMember
57
- } from "./chunk-WJJ5MBH5.js";
57
+ } from "./chunk-YEOQJ7WW.js";
58
58
  import {
59
59
  getMemberNotificationPrefs,
60
60
  isNotificationKindEnabled,
@@ -62,7 +62,7 @@ import {
62
62
  recordDigestSent,
63
63
  registerNotificationKind,
64
64
  setMemberNotificationPrefs
65
- } from "./chunk-CAS4Z6IN.js";
65
+ } from "./chunk-I4FSVEJK.js";
66
66
  import {
67
67
  MENTION_HANDLE_RE,
68
68
  assertOwnsNotification,
@@ -76,13 +76,13 @@ import {
76
76
  markNotificationsRead,
77
77
  resolveMentionedMembers,
78
78
  unreadNotificationCount
79
- } from "./chunk-RDTTK27V.js";
79
+ } from "./chunk-XPD7EQML.js";
80
80
  import {
81
81
  applyReputation,
82
82
  getReputationAdapter,
83
83
  resetReputationAdapter,
84
84
  setReputationAdapter
85
- } from "./chunk-L6VG7IK6.js";
85
+ } from "./chunk-VBVLYFSZ.js";
86
86
  import {
87
87
  getSpamAdapter,
88
88
  resetSpamAdapter,
@@ -93,20 +93,20 @@ import {
93
93
  resetProfanityAdapter,
94
94
  setProfanityAdapter
95
95
  } from "./chunk-KU5M27ZC.js";
96
- import "./chunk-2N53KKIL.js";
97
- import "./chunk-MLXKZK6G.js";
98
- import "./chunk-2VZZ7M26.js";
96
+ import "./chunk-EWVXP3GP.js";
97
+ import "./chunk-TSCXXBOM.js";
98
+ import "./chunk-EAYUAXW3.js";
99
99
  import "./chunk-EQ2Z3KMD.js";
100
100
  import {
101
101
  listAuditEvents,
102
102
  recordAuditEvent
103
- } from "./chunk-ML2E3P3X.js";
103
+ } from "./chunk-5C22NDW4.js";
104
104
  import {
105
105
  DEFAULT_COMMUNITY_SETTINGS,
106
106
  getCommunitySettings,
107
107
  updateCommunitySettings,
108
108
  validateCommunitySettingsPatch
109
- } from "./chunk-RKM4GDWM.js";
109
+ } from "./chunk-6MRTH734.js";
110
110
  import "./chunk-EFZH6UPY.js";
111
111
  import "./chunk-4ZLMEKFX.js";
112
112
  import "./chunk-U4QCCLAW.js";
@@ -116,7 +116,7 @@ import "./chunk-LSHHRDVR.js";
116
116
  import "./chunk-V2UNHGAP.js";
117
117
  import "./chunk-WV272MPW.js";
118
118
  import "./chunk-OROPGO65.js";
119
- import "./chunk-NFHS7CFV.js";
119
+ import "./chunk-Q7MK5ZKG.js";
120
120
  import "./chunk-XANPEOJC.js";
121
121
  import "./chunk-X7K5F2UI.js";
122
122
  import "./chunk-PZ5AY32C.js";
@@ -5,9 +5,9 @@ import {
5
5
  isVersionedPluginConfig,
6
6
  pluginConfigCacheTag,
7
7
  setPluginConfig
8
- } from "./chunk-OMGQZ4Q5.js";
9
- import "./chunk-MLXKZK6G.js";
10
- import "./chunk-2VZZ7M26.js";
8
+ } from "./chunk-2OWUHCFY.js";
9
+ import "./chunk-TSCXXBOM.js";
10
+ import "./chunk-EAYUAXW3.js";
11
11
  import "./chunk-LMPYQLMH.js";
12
12
  import "./chunk-2KNG5KMM.js";
13
13
  import "./chunk-EFZH6UPY.js";
@@ -18,7 +18,7 @@ import "./chunk-ZCINJSS4.js";
18
18
  import "./chunk-V2UNHGAP.js";
19
19
  import "./chunk-WV272MPW.js";
20
20
  import "./chunk-OROPGO65.js";
21
- import "./chunk-NFHS7CFV.js";
21
+ import "./chunk-Q7MK5ZKG.js";
22
22
  import "./chunk-XANPEOJC.js";
23
23
  import "./chunk-X7K5F2UI.js";
24
24
  import "./chunk-PZ5AY32C.js";
@@ -30,4 +30,4 @@ export {
30
30
  pluginConfigCacheTag,
31
31
  setPluginConfig
32
32
  };
33
- //# sourceMappingURL=config-YHUEYQ66.js.map
33
+ //# sourceMappingURL=config-YDGNUDKP.js.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  buildDigestEmail,
3
3
  runDigestSweep
4
- } from "./chunk-LN6NTH6E.js";
5
- import "./chunk-CAS4Z6IN.js";
4
+ } from "./chunk-K4CJ3KXB.js";
5
+ import "./chunk-I4FSVEJK.js";
6
6
  import "./chunk-U4QCCLAW.js";
7
7
  import "./chunk-ZCINJSS4.js";
8
8
  import "./chunk-LSHHRDVR.js";
9
- import "./chunk-NFHS7CFV.js";
9
+ import "./chunk-Q7MK5ZKG.js";
10
10
  import "./chunk-XANPEOJC.js";
11
11
  import "./chunk-X7K5F2UI.js";
12
12
  import "./chunk-PZ5AY32C.js";
@@ -14,4 +14,4 @@ export {
14
14
  buildDigestEmail,
15
15
  runDigestSweep
16
16
  };
17
- //# sourceMappingURL=digest-ZODDTXA2.js.map
17
+ //# sourceMappingURL=digest-IWHMJPXI.js.map
@@ -16,8 +16,8 @@ import {
16
16
  runHookAndCollect,
17
17
  runPluginScheduledTask,
18
18
  schedulePluginTask
19
- } from "./chunk-MLXKZK6G.js";
20
- import "./chunk-2VZZ7M26.js";
19
+ } from "./chunk-TSCXXBOM.js";
20
+ import "./chunk-EAYUAXW3.js";
21
21
  import "./chunk-EFZH6UPY.js";
22
22
  import "./chunk-4ZLMEKFX.js";
23
23
  import "./chunk-U4QCCLAW.js";
@@ -26,7 +26,7 @@ import "./chunk-ZCINJSS4.js";
26
26
  import "./chunk-V2UNHGAP.js";
27
27
  import "./chunk-WV272MPW.js";
28
28
  import "./chunk-OROPGO65.js";
29
- import "./chunk-NFHS7CFV.js";
29
+ import "./chunk-Q7MK5ZKG.js";
30
30
  import "./chunk-XANPEOJC.js";
31
31
  import "./chunk-X7K5F2UI.js";
32
32
  import "./chunk-PZ5AY32C.js";
@@ -49,4 +49,4 @@ export {
49
49
  runPluginScheduledTask,
50
50
  schedulePluginTask
51
51
  };
52
- //# sourceMappingURL=host-XBGYIQEE.js.map
52
+ //# sourceMappingURL=host-HG4QGD3L.js.map
package/dist/i18n.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  setStrings,
25
25
  t,
26
26
  tSync
27
- } from "./chunk-ELK6AVW5.js";
27
+ } from "./chunk-2X3GBJOT.js";
28
28
  import {
29
29
  getI18nConfig,
30
30
  resetI18nConfig,
@@ -33,7 +33,7 @@ import {
33
33
  import "./chunk-U4QCCLAW.js";
34
34
  import "./chunk-SBCVAC2Z.js";
35
35
  import "./chunk-ZCINJSS4.js";
36
- import "./chunk-NFHS7CFV.js";
36
+ import "./chunk-Q7MK5ZKG.js";
37
37
  import "./chunk-XANPEOJC.js";
38
38
  import "./chunk-X7K5F2UI.js";
39
39
  import "./chunk-PZ5AY32C.js";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  introspectThemeSettingsSchema,
5
5
  pluginConfigCacheTag,
6
6
  setPluginConfig
7
- } from "./chunk-OMGQZ4Q5.js";
7
+ } from "./chunk-2OWUHCFY.js";
8
8
  import {
9
9
  getPluginTemplatesForCollection,
10
10
  registerPluginTemplates,
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-TETTWT56.js";
16
16
  import {
17
17
  verifyStartupSafety
18
- } from "./chunk-B7DTNT4O.js";
18
+ } from "./chunk-MWLSXK6Y.js";
19
19
  import {
20
20
  InMemoryRateLimiter,
21
21
  getOptionalRateLimiter,
@@ -41,7 +41,7 @@ import {
41
41
  renderSitemapIndexXml,
42
42
  renderSitemapXml,
43
43
  validateSeoSettingsPatch
44
- } from "./chunk-PUV3VZPD.js";
44
+ } from "./chunk-QZ52U4ET.js";
45
45
  import {
46
46
  ARGON2_OPTIONS,
47
47
  authenticated,
@@ -82,7 +82,7 @@ import {
82
82
  verifyPassword,
83
83
  verifyToken,
84
84
  verifyTokenFull
85
- } from "./chunk-6UV2P5MW.js";
85
+ } from "./chunk-TIWJVQOO.js";
86
86
  import {
87
87
  DEFAULT_REACTION_KINDS,
88
88
  addReaction,
@@ -130,14 +130,14 @@ import {
130
130
  unfollow,
131
131
  unresolvedReportCount,
132
132
  updateComment
133
- } from "./chunk-HNX7COHQ.js";
133
+ } from "./chunk-3SW4L3DL.js";
134
134
  import {
135
135
  publishScheduledDocuments
136
- } from "./chunk-PW43RCJK.js";
136
+ } from "./chunk-6OUWW6JF.js";
137
137
  import {
138
138
  buildDigestEmail,
139
139
  runDigestSweep
140
- } from "./chunk-LN6NTH6E.js";
140
+ } from "./chunk-K4CJ3KXB.js";
141
141
  import {
142
142
  assertNotBanned,
143
143
  getCommunityRole,
@@ -146,14 +146,14 @@ import {
146
146
  registerCommunityRole,
147
147
  resetCommunityRoles,
148
148
  withMemberWrite
149
- } from "./chunk-RJ76SKWQ.js";
149
+ } from "./chunk-XU2GJJ6Z.js";
150
150
  import {
151
151
  getMutedTargetIds,
152
152
  isMuted,
153
153
  listMutes,
154
154
  muteMember,
155
155
  unmuteMember
156
- } from "./chunk-WJJ5MBH5.js";
156
+ } from "./chunk-YEOQJ7WW.js";
157
157
  import {
158
158
  getMemberNotificationPrefs,
159
159
  isNotificationKindEnabled,
@@ -161,7 +161,7 @@ import {
161
161
  recordDigestSent,
162
162
  registerNotificationKind,
163
163
  setMemberNotificationPrefs
164
- } from "./chunk-CAS4Z6IN.js";
164
+ } from "./chunk-I4FSVEJK.js";
165
165
  import {
166
166
  MENTION_HANDLE_RE,
167
167
  assertOwnsNotification,
@@ -175,13 +175,13 @@ import {
175
175
  markNotificationsRead,
176
176
  resolveMentionedMembers,
177
177
  unreadNotificationCount
178
- } from "./chunk-RDTTK27V.js";
178
+ } from "./chunk-XPD7EQML.js";
179
179
  import {
180
180
  applyReputation,
181
181
  getReputationAdapter,
182
182
  resetReputationAdapter,
183
183
  setReputationAdapter
184
- } from "./chunk-L6VG7IK6.js";
184
+ } from "./chunk-VBVLYFSZ.js";
185
185
  import {
186
186
  getSpamAdapter,
187
187
  resetSpamAdapter,
@@ -194,7 +194,7 @@ import {
194
194
  } from "./chunk-KU5M27ZC.js";
195
195
  import {
196
196
  getMediaUrl
197
- } from "./chunk-2N53KKIL.js";
197
+ } from "./chunk-EWVXP3GP.js";
198
198
  import {
199
199
  autosaveRevision,
200
200
  buildSearchVector,
@@ -229,7 +229,7 @@ import {
229
229
  schedulePluginTask,
230
230
  updateMemberDocument,
231
231
  withDeferredPostCommit
232
- } from "./chunk-MLXKZK6G.js";
232
+ } from "./chunk-TSCXXBOM.js";
233
233
  import {
234
234
  DEFAULT_IMAGE_SIZES,
235
235
  cleanupDeletedMedia,
@@ -241,20 +241,20 @@ import {
241
241
  processMediaImage,
242
242
  setStorageAdapter,
243
243
  uploadMedia
244
- } from "./chunk-2VZZ7M26.js";
244
+ } from "./chunk-EAYUAXW3.js";
245
245
  import {
246
246
  can
247
247
  } from "./chunk-EQ2Z3KMD.js";
248
248
  import {
249
249
  listAuditEvents,
250
250
  recordAuditEvent
251
- } from "./chunk-ML2E3P3X.js";
251
+ } from "./chunk-5C22NDW4.js";
252
252
  import {
253
253
  DEFAULT_COMMUNITY_SETTINGS,
254
254
  getCommunitySettings,
255
255
  updateCommunitySettings,
256
256
  validateCommunitySettingsPatch
257
- } from "./chunk-RKM4GDWM.js";
257
+ } from "./chunk-6MRTH734.js";
258
258
  import {
259
259
  createDbConnection,
260
260
  generateDocumentsModule,
@@ -303,7 +303,7 @@ import {
303
303
  setStrings,
304
304
  t,
305
305
  tSync
306
- } from "./chunk-ELK6AVW5.js";
306
+ } from "./chunk-2X3GBJOT.js";
307
307
  import {
308
308
  getI18nConfig,
309
309
  resetI18nConfig,
@@ -366,7 +366,7 @@ import {
366
366
  startWorker,
367
367
  stopProducer,
368
368
  stopWorker
369
- } from "./chunk-2GXH7566.js";
369
+ } from "./chunk-SJ7M2VCC.js";
370
370
  import {
371
371
  DEFAULT_JOB_LOG_RETENTION_MS,
372
372
  countJobLogs,
@@ -375,7 +375,7 @@ import {
375
375
  pruneJobLogsOlderThan,
376
376
  recordJobLog,
377
377
  runInJobContext
378
- } from "./chunk-QBIJZZ5V.js";
378
+ } from "./chunk-CGLJBRRX.js";
379
379
  import {
380
380
  NoopEmailAdapter,
381
381
  getEmailAdapter,
@@ -402,7 +402,7 @@ import {
402
402
  getScopedLogger,
403
403
  resetLogger,
404
404
  setLogger
405
- } from "./chunk-NFHS7CFV.js";
405
+ } from "./chunk-Q7MK5ZKG.js";
406
406
  import {
407
407
  getDb,
408
408
  setDb