@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.
- package/dist/{audit-43OLHR3U.js → audit-ZLNKBIDO.js} +3 -3
- package/dist/auth.js +4 -4
- package/dist/{can-UJ2NAOIR.js → can-U5F4JBZ7.js} +2 -2
- package/dist/{chunk-OMGQZ4Q5.js → chunk-2OWUHCFY.js} +2 -2
- package/dist/{chunk-OMGQZ4Q5.js.map → chunk-2OWUHCFY.js.map} +1 -1
- package/dist/{chunk-ELK6AVW5.js → chunk-2X3GBJOT.js} +2 -2
- package/dist/{chunk-HNX7COHQ.js → chunk-3SW4L3DL.js} +12 -12
- package/dist/chunk-3SW4L3DL.js.map +1 -0
- package/dist/{chunk-ML2E3P3X.js → chunk-5C22NDW4.js} +2 -2
- package/dist/chunk-5C22NDW4.js.map +1 -0
- package/dist/{chunk-RKM4GDWM.js → chunk-6MRTH734.js} +1 -1
- package/dist/chunk-6MRTH734.js.map +1 -0
- package/dist/{chunk-PW43RCJK.js → chunk-6OUWW6JF.js} +2 -2
- package/dist/{chunk-QBIJZZ5V.js → chunk-CGLJBRRX.js} +2 -2
- package/dist/chunk-CGLJBRRX.js.map +1 -0
- package/dist/{chunk-2VZZ7M26.js → chunk-EAYUAXW3.js} +3 -3
- package/dist/chunk-EAYUAXW3.js.map +1 -0
- package/dist/{chunk-2N53KKIL.js → chunk-EWVXP3GP.js} +2 -2
- package/dist/{chunk-CAS4Z6IN.js → chunk-I4FSVEJK.js} +1 -1
- package/dist/chunk-I4FSVEJK.js.map +1 -0
- package/dist/{chunk-LN6NTH6E.js → chunk-K4CJ3KXB.js} +3 -3
- package/dist/chunk-K4CJ3KXB.js.map +1 -0
- package/dist/{chunk-B7DTNT4O.js → chunk-MWLSXK6Y.js} +2 -2
- package/dist/{chunk-NFHS7CFV.js → chunk-Q7MK5ZKG.js} +2 -2
- package/dist/{chunk-PUV3VZPD.js → chunk-QZ52U4ET.js} +2 -2
- package/dist/{chunk-2GXH7566.js → chunk-SJ7M2VCC.js} +10 -10
- package/dist/chunk-SJ7M2VCC.js.map +1 -0
- package/dist/{chunk-6UV2P5MW.js → chunk-TIWJVQOO.js} +3 -3
- package/dist/chunk-TIWJVQOO.js.map +1 -0
- package/dist/{chunk-MLXKZK6G.js → chunk-TSCXXBOM.js} +76 -28
- package/dist/chunk-TSCXXBOM.js.map +1 -0
- package/dist/{chunk-L6VG7IK6.js → chunk-VBVLYFSZ.js} +2 -2
- package/dist/chunk-VBVLYFSZ.js.map +1 -0
- package/dist/{chunk-RDTTK27V.js → chunk-XPD7EQML.js} +3 -3
- package/dist/chunk-XPD7EQML.js.map +1 -0
- package/dist/{chunk-RJ76SKWQ.js → chunk-XU2GJJ6Z.js} +1 -1
- package/dist/chunk-XU2GJJ6Z.js.map +1 -0
- package/dist/{chunk-WJJ5MBH5.js → chunk-YEOQJ7WW.js} +1 -1
- package/dist/chunk-YEOQJ7WW.js.map +1 -0
- package/dist/community.js +14 -14
- package/dist/{config-YHUEYQ66.js → config-YDGNUDKP.js} +5 -5
- package/dist/{digest-ZODDTXA2.js → digest-IWHMJPXI.js} +4 -4
- package/dist/{host-XBGYIQEE.js → host-HG4QGD3L.js} +4 -4
- package/dist/i18n.js +2 -2
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/{job-log-N3IGI4NA.js → job-log-UY6ERPQZ.js} +3 -3
- package/dist/jobs.js +3 -3
- package/dist/{logger-2WUTTELV.js → logger-6ZGEKEMK.js} +2 -2
- package/dist/media.js +3 -3
- package/dist/{mentions-U4JACYI6.js → mentions-LQRZWAGO.js} +2 -2
- package/dist/{mutes-MNQP6ACF.js → mutes-PQA6U5X7.js} +2 -2
- package/dist/{notification-prefs-H4HFVCL7.js → notification-prefs-62NX2GBF.js} +2 -2
- package/dist/observability.js +2 -2
- package/dist/{reputation-ICIXDGPM.js → reputation-5DJLDBZY.js} +3 -3
- package/dist/{scheduled-S6IO47JD.js → scheduled-C2IKVZVK.js} +5 -5
- package/dist/seo.js +4 -4
- package/dist/{settings-OZWM6L2K.js → settings-NBAP7E5E.js} +2 -2
- package/dist/{strings-4EWJYDOG.js → strings-O2M7VSKV.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-2GXH7566.js.map +0 -1
- package/dist/chunk-2VZZ7M26.js.map +0 -1
- package/dist/chunk-6UV2P5MW.js.map +0 -1
- package/dist/chunk-CAS4Z6IN.js.map +0 -1
- package/dist/chunk-HNX7COHQ.js.map +0 -1
- package/dist/chunk-L6VG7IK6.js.map +0 -1
- package/dist/chunk-LN6NTH6E.js.map +0 -1
- package/dist/chunk-ML2E3P3X.js.map +0 -1
- package/dist/chunk-MLXKZK6G.js.map +0 -1
- package/dist/chunk-QBIJZZ5V.js.map +0 -1
- package/dist/chunk-RDTTK27V.js.map +0 -1
- package/dist/chunk-RJ76SKWQ.js.map +0 -1
- package/dist/chunk-RKM4GDWM.js.map +0 -1
- package/dist/chunk-WJJ5MBH5.js.map +0 -1
- /package/dist/{audit-43OLHR3U.js.map → audit-ZLNKBIDO.js.map} +0 -0
- /package/dist/{can-UJ2NAOIR.js.map → can-U5F4JBZ7.js.map} +0 -0
- /package/dist/{chunk-ELK6AVW5.js.map → chunk-2X3GBJOT.js.map} +0 -0
- /package/dist/{chunk-PW43RCJK.js.map → chunk-6OUWW6JF.js.map} +0 -0
- /package/dist/{chunk-2N53KKIL.js.map → chunk-EWVXP3GP.js.map} +0 -0
- /package/dist/{chunk-B7DTNT4O.js.map → chunk-MWLSXK6Y.js.map} +0 -0
- /package/dist/{chunk-NFHS7CFV.js.map → chunk-Q7MK5ZKG.js.map} +0 -0
- /package/dist/{chunk-PUV3VZPD.js.map → chunk-QZ52U4ET.js.map} +0 -0
- /package/dist/{config-YHUEYQ66.js.map → config-YDGNUDKP.js.map} +0 -0
- /package/dist/{digest-ZODDTXA2.js.map → digest-IWHMJPXI.js.map} +0 -0
- /package/dist/{host-XBGYIQEE.js.map → host-HG4QGD3L.js.map} +0 -0
- /package/dist/{job-log-N3IGI4NA.js.map → job-log-UY6ERPQZ.js.map} +0 -0
- /package/dist/{logger-2WUTTELV.js.map → logger-6ZGEKEMK.js.map} +0 -0
- /package/dist/{mentions-U4JACYI6.js.map → mentions-LQRZWAGO.js.map} +0 -0
- /package/dist/{mutes-MNQP6ACF.js.map → mutes-PQA6U5X7.js.map} +0 -0
- /package/dist/{notification-prefs-H4HFVCL7.js.map → notification-prefs-62NX2GBF.js.map} +0 -0
- /package/dist/{reputation-ICIXDGPM.js.map → reputation-5DJLDBZY.js.map} +0 -0
- /package/dist/{scheduled-S6IO47JD.js.map → scheduled-C2IKVZVK.js.map} +0 -0
- /package/dist/{settings-OZWM6L2K.js.map → settings-NBAP7E5E.js.map} +0 -0
- /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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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"]}
|
|
@@ -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":[]}
|
|
@@ -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-
|
|
37
|
-
import "./chunk-
|
|
36
|
+
} from "./chunk-3SW4L3DL.js";
|
|
37
|
+
import "./chunk-6OUWW6JF.js";
|
|
38
38
|
import {
|
|
39
39
|
buildDigestEmail,
|
|
40
40
|
runDigestSweep
|
|
41
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
79
|
+
} from "./chunk-XPD7EQML.js";
|
|
80
80
|
import {
|
|
81
81
|
applyReputation,
|
|
82
82
|
getReputationAdapter,
|
|
83
83
|
resetReputationAdapter,
|
|
84
84
|
setReputationAdapter
|
|
85
|
-
} from "./chunk-
|
|
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-
|
|
97
|
-
import "./chunk-
|
|
98
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
33
|
+
//# sourceMappingURL=config-YDGNUDKP.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildDigestEmail,
|
|
3
3
|
runDigestSweep
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
17
|
+
//# sourceMappingURL=digest-IWHMJPXI.js.map
|
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
runHookAndCollect,
|
|
17
17
|
runPluginScheduledTask,
|
|
18
18
|
schedulePluginTask
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
133
|
+
} from "./chunk-3SW4L3DL.js";
|
|
134
134
|
import {
|
|
135
135
|
publishScheduledDocuments
|
|
136
|
-
} from "./chunk-
|
|
136
|
+
} from "./chunk-6OUWW6JF.js";
|
|
137
137
|
import {
|
|
138
138
|
buildDigestEmail,
|
|
139
139
|
runDigestSweep
|
|
140
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
178
|
+
} from "./chunk-XPD7EQML.js";
|
|
179
179
|
import {
|
|
180
180
|
applyReputation,
|
|
181
181
|
getReputationAdapter,
|
|
182
182
|
resetReputationAdapter,
|
|
183
183
|
setReputationAdapter
|
|
184
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
405
|
+
} from "./chunk-Q7MK5ZKG.js";
|
|
406
406
|
import {
|
|
407
407
|
getDb,
|
|
408
408
|
setDb
|