@nexpress/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/audit-54XLVCWD.js +14 -0
  4. package/dist/audit-54XLVCWD.js.map +1 -0
  5. package/dist/auth.d.ts +640 -0
  6. package/dist/auth.js +94 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/can-YLUHRJAB.js +19 -0
  9. package/dist/can-YLUHRJAB.js.map +1 -0
  10. package/dist/chunk-2G264RCD.js +68 -0
  11. package/dist/chunk-2G264RCD.js.map +1 -0
  12. package/dist/chunk-2YDGE7YX.js +92 -0
  13. package/dist/chunk-2YDGE7YX.js.map +1 -0
  14. package/dist/chunk-473S4TER.js +538 -0
  15. package/dist/chunk-473S4TER.js.map +1 -0
  16. package/dist/chunk-4ZLMEKFX.js +18 -0
  17. package/dist/chunk-4ZLMEKFX.js.map +1 -0
  18. package/dist/chunk-55FU6WED.js +179 -0
  19. package/dist/chunk-55FU6WED.js.map +1 -0
  20. package/dist/chunk-6YI5K2TI.js +1959 -0
  21. package/dist/chunk-6YI5K2TI.js.map +1 -0
  22. package/dist/chunk-BHK3AD3Q.js +41 -0
  23. package/dist/chunk-BHK3AD3Q.js.map +1 -0
  24. package/dist/chunk-CRUQBZUF.js +39 -0
  25. package/dist/chunk-CRUQBZUF.js.map +1 -0
  26. package/dist/chunk-CTSQ7BRI.js +175 -0
  27. package/dist/chunk-CTSQ7BRI.js.map +1 -0
  28. package/dist/chunk-DK2JBJH7.js +81 -0
  29. package/dist/chunk-DK2JBJH7.js.map +1 -0
  30. package/dist/chunk-DP2PREDU.js +597 -0
  31. package/dist/chunk-DP2PREDU.js.map +1 -0
  32. package/dist/chunk-EQ2Z3KMD.js +24 -0
  33. package/dist/chunk-EQ2Z3KMD.js.map +1 -0
  34. package/dist/chunk-FZ7O6DWI.js +305 -0
  35. package/dist/chunk-FZ7O6DWI.js.map +1 -0
  36. package/dist/chunk-ISLYFQWL.js +1270 -0
  37. package/dist/chunk-ISLYFQWL.js.map +1 -0
  38. package/dist/chunk-JJL74ZPK.js +68 -0
  39. package/dist/chunk-JJL74ZPK.js.map +1 -0
  40. package/dist/chunk-JKXAPSU4.js +24 -0
  41. package/dist/chunk-JKXAPSU4.js.map +1 -0
  42. package/dist/chunk-KU5M27ZC.js +24 -0
  43. package/dist/chunk-KU5M27ZC.js.map +1 -0
  44. package/dist/chunk-LSHHRDVR.js +34 -0
  45. package/dist/chunk-LSHHRDVR.js.map +1 -0
  46. package/dist/chunk-M43PGOQY.js +715 -0
  47. package/dist/chunk-M43PGOQY.js.map +1 -0
  48. package/dist/chunk-MEJAHXIO.js +150 -0
  49. package/dist/chunk-MEJAHXIO.js.map +1 -0
  50. package/dist/chunk-NUCGHWCF.js +101 -0
  51. package/dist/chunk-NUCGHWCF.js.map +1 -0
  52. package/dist/chunk-OK5HOCQI.js +845 -0
  53. package/dist/chunk-OK5HOCQI.js.map +1 -0
  54. package/dist/chunk-OROPGO65.js +13 -0
  55. package/dist/chunk-OROPGO65.js.map +1 -0
  56. package/dist/chunk-PPAS4SZR.js +176 -0
  57. package/dist/chunk-PPAS4SZR.js.map +1 -0
  58. package/dist/chunk-PPBWRKO2.js +171 -0
  59. package/dist/chunk-PPBWRKO2.js.map +1 -0
  60. package/dist/chunk-PZ5AY32C.js +10 -0
  61. package/dist/chunk-PZ5AY32C.js.map +1 -0
  62. package/dist/chunk-QO7LAQZH.js +321 -0
  63. package/dist/chunk-QO7LAQZH.js.map +1 -0
  64. package/dist/chunk-QVJ2HCAX.js +225 -0
  65. package/dist/chunk-QVJ2HCAX.js.map +1 -0
  66. package/dist/chunk-RIPHIRPP.js +68 -0
  67. package/dist/chunk-RIPHIRPP.js.map +1 -0
  68. package/dist/chunk-S27S42QY.js +134 -0
  69. package/dist/chunk-S27S42QY.js.map +1 -0
  70. package/dist/chunk-SBCVAC2Z.js +40 -0
  71. package/dist/chunk-SBCVAC2Z.js.map +1 -0
  72. package/dist/chunk-TFJ4MKPH.js +694 -0
  73. package/dist/chunk-TFJ4MKPH.js.map +1 -0
  74. package/dist/chunk-THX3SHYA.js +75 -0
  75. package/dist/chunk-THX3SHYA.js.map +1 -0
  76. package/dist/chunk-UGQSQO5B.js +222 -0
  77. package/dist/chunk-UGQSQO5B.js.map +1 -0
  78. package/dist/chunk-V2UNHGAP.js +26 -0
  79. package/dist/chunk-V2UNHGAP.js.map +1 -0
  80. package/dist/chunk-VGTPQXNQ.js +2790 -0
  81. package/dist/chunk-VGTPQXNQ.js.map +1 -0
  82. package/dist/chunk-VNIHXQ7W.js +194 -0
  83. package/dist/chunk-VNIHXQ7W.js.map +1 -0
  84. package/dist/chunk-WV272MPW.js +31 -0
  85. package/dist/chunk-WV272MPW.js.map +1 -0
  86. package/dist/chunk-X5KKBOUS.js +26 -0
  87. package/dist/chunk-X5KKBOUS.js.map +1 -0
  88. package/dist/chunk-XANPEOJC.js +17 -0
  89. package/dist/chunk-XANPEOJC.js.map +1 -0
  90. package/dist/chunk-XPVQIHAQ.js +83 -0
  91. package/dist/chunk-XPVQIHAQ.js.map +1 -0
  92. package/dist/chunk-ZCINJSS4.js +75 -0
  93. package/dist/chunk-ZCINJSS4.js.map +1 -0
  94. package/dist/community.d.ts +1425 -0
  95. package/dist/community.js +206 -0
  96. package/dist/community.js.map +1 -0
  97. package/dist/config-2GDU7PCK.js +32 -0
  98. package/dist/config-2GDU7PCK.js.map +1 -0
  99. package/dist/context-MNZ4QXPC.js +16 -0
  100. package/dist/context-MNZ4QXPC.js.map +1 -0
  101. package/dist/db-schema.d.ts +4 -0
  102. package/dist/db-schema.js +102 -0
  103. package/dist/db-schema.js.map +1 -0
  104. package/dist/db.d.ts +7 -0
  105. package/dist/db.js +117 -0
  106. package/dist/db.js.map +1 -0
  107. package/dist/digest-SY42GQSU.js +17 -0
  108. package/dist/digest-SY42GQSU.js.map +1 -0
  109. package/dist/errors-5OS3S2J3.js +22 -0
  110. package/dist/errors-5OS3S2J3.js.map +1 -0
  111. package/dist/host-OBOI4MJK.js +51 -0
  112. package/dist/host-OBOI4MJK.js.map +1 -0
  113. package/dist/i18n.d.ts +301 -0
  114. package/dist/i18n.js +68 -0
  115. package/dist/i18n.js.map +1 -0
  116. package/dist/index-B6-_vr_m.d.ts +590 -0
  117. package/dist/index-CY55LC0u.d.ts +4722 -0
  118. package/dist/index-CeiTvwbp.d.ts +168 -0
  119. package/dist/index-XwP1ET8b.d.ts +61 -0
  120. package/dist/index.d.ts +2037 -0
  121. package/dist/index.js +2205 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/job-log-VZXWQUDK.js +24 -0
  124. package/dist/job-log-VZXWQUDK.js.map +1 -0
  125. package/dist/jobs.d.ts +4 -0
  126. package/dist/jobs.js +76 -0
  127. package/dist/jobs.js.map +1 -0
  128. package/dist/logger-DqGaOU_j.d.ts +29 -0
  129. package/dist/logger-S7REWDNE.js +16 -0
  130. package/dist/logger-S7REWDNE.js.map +1 -0
  131. package/dist/media.d.ts +5 -0
  132. package/dist/media.js +41 -0
  133. package/dist/media.js.map +1 -0
  134. package/dist/mentions-2IHFVSHW.js +23 -0
  135. package/dist/mentions-2IHFVSHW.js.map +1 -0
  136. package/dist/mutes-EWAE5FZR.js +21 -0
  137. package/dist/mutes-EWAE5FZR.js.map +1 -0
  138. package/dist/notification-prefs-VPJDU7I6.js +21 -0
  139. package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
  140. package/dist/observability.d.ts +156 -0
  141. package/dist/observability.js +32 -0
  142. package/dist/observability.js.map +1 -0
  143. package/dist/profanity-adapter-NU2JQSLX.js +12 -0
  144. package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
  145. package/dist/queue-XE5BC75T.js +14 -0
  146. package/dist/queue-XE5BC75T.js.map +1 -0
  147. package/dist/rate-limit.d.ts +99 -0
  148. package/dist/rate-limit.js +14 -0
  149. package/dist/rate-limit.js.map +1 -0
  150. package/dist/registry-XIXDEPVI.js +31 -0
  151. package/dist/registry-XIXDEPVI.js.map +1 -0
  152. package/dist/reputation-JRL2YQHM.js +11 -0
  153. package/dist/reputation-JRL2YQHM.js.map +1 -0
  154. package/dist/routes.d.ts +43 -0
  155. package/dist/routes.js +12 -0
  156. package/dist/routes.js.map +1 -0
  157. package/dist/scheduled-CIQM57HT.js +20 -0
  158. package/dist/scheduled-CIQM57HT.js.map +1 -0
  159. package/dist/seo.d.ts +410 -0
  160. package/dist/seo.js +44 -0
  161. package/dist/seo.js.map +1 -0
  162. package/dist/settings-FOBIESPB.js +17 -0
  163. package/dist/settings-FOBIESPB.js.map +1 -0
  164. package/dist/spam-adapter-XX3G737Z.js +12 -0
  165. package/dist/spam-adapter-XX3G737Z.js.map +1 -0
  166. package/dist/strings-VAE47B2C.js +29 -0
  167. package/dist/strings-VAE47B2C.js.map +1 -0
  168. package/dist/templates-IFVJMCJ6.js +12 -0
  169. package/dist/templates-IFVJMCJ6.js.map +1 -0
  170. package/dist/types-TlsbXS0T.d.ts +871 -0
  171. package/package.json +129 -0
@@ -0,0 +1,175 @@
1
+ import {
2
+ NpNotFoundError,
3
+ NpValidationError
4
+ } from "./chunk-ZCINJSS4.js";
5
+ import {
6
+ getDb
7
+ } from "./chunk-XANPEOJC.js";
8
+ import {
9
+ npMembers
10
+ } from "./chunk-M43PGOQY.js";
11
+
12
+ // src/community/notification-prefs.ts
13
+ import { eq } from "drizzle-orm";
14
+ var builtinKinds = [
15
+ {
16
+ kind: "comment.reply",
17
+ label: "Replies",
18
+ description: "Someone replied to one of your comments."
19
+ },
20
+ {
21
+ kind: "comment.mention",
22
+ label: "Mentions in comments",
23
+ description: "Someone @-mentioned you in a comment."
24
+ },
25
+ {
26
+ kind: "document.mention",
27
+ label: "Mentions in discussions",
28
+ description: "Someone @-mentioned you in a discussion / thread."
29
+ },
30
+ {
31
+ kind: "reaction.received",
32
+ label: "Reactions",
33
+ description: "Someone reacted to your comment or document."
34
+ },
35
+ {
36
+ kind: "follow.received",
37
+ label: "New followers",
38
+ description: "Someone started following you."
39
+ }
40
+ ];
41
+ var dynamicKinds = [];
42
+ function registerNotificationKind(meta) {
43
+ if (builtinKinds.some((k) => k.kind === meta.kind)) return;
44
+ const idx = dynamicKinds.findIndex((k) => k.kind === meta.kind);
45
+ if (idx >= 0) {
46
+ dynamicKinds[idx] = meta;
47
+ } else {
48
+ dynamicKinds.push(meta);
49
+ }
50
+ }
51
+ function listNotificationKinds() {
52
+ return [...builtinKinds, ...dynamicKinds];
53
+ }
54
+ var DIGEST_CADENCES = ["off", "daily", "weekly"];
55
+ var EMPTY_PREFS = {
56
+ disabled: [],
57
+ digest: "off",
58
+ lastDigestAt: null,
59
+ lastDigestAtBySite: {}
60
+ };
61
+ function normalizeDigest(raw) {
62
+ return DIGEST_CADENCES.includes(raw) ? raw : "off";
63
+ }
64
+ function normalizeLastDigestAt(raw) {
65
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
66
+ }
67
+ function normalizeLastDigestBySite(raw) {
68
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
69
+ const out = {};
70
+ for (const [siteId, value] of Object.entries(raw)) {
71
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
72
+ const inner = {};
73
+ for (const [cadence, ts] of Object.entries(value)) {
74
+ if (!DIGEST_CADENCES.includes(cadence)) continue;
75
+ if (typeof ts === "string" && ts.length > 0) {
76
+ inner[cadence] = ts;
77
+ }
78
+ }
79
+ if (Object.keys(inner).length > 0) out[siteId] = inner;
80
+ }
81
+ return out;
82
+ }
83
+ function normalizePrefs(raw) {
84
+ if (!raw || typeof raw !== "object") return { ...EMPTY_PREFS, lastDigestAtBySite: {} };
85
+ const obj = raw;
86
+ const disabled = Array.isArray(obj.disabled) ? obj.disabled.filter((k) => typeof k === "string") : [];
87
+ return {
88
+ disabled,
89
+ digest: normalizeDigest(obj.digest),
90
+ lastDigestAt: normalizeLastDigestAt(obj.lastDigestAt),
91
+ lastDigestAtBySite: normalizeLastDigestBySite(obj.lastDigestAtBySite)
92
+ };
93
+ }
94
+ async function getMemberNotificationPrefs(memberId) {
95
+ const db = getDb();
96
+ const [row] = await db.select({ prefs: npMembers.notificationPrefs }).from(npMembers).where(eq(npMembers.id, memberId)).limit(1);
97
+ if (!row) throw new NpNotFoundError("member", memberId);
98
+ return normalizePrefs(row.prefs);
99
+ }
100
+ async function setMemberNotificationPrefs(input) {
101
+ const known = new Set(listNotificationKinds().map((k) => k.kind));
102
+ let cleanedDisabled;
103
+ if (input.disabled !== void 0) {
104
+ cleanedDisabled = [];
105
+ const seen = /* @__PURE__ */ new Set();
106
+ for (const raw of input.disabled) {
107
+ if (typeof raw !== "string") {
108
+ throw new NpValidationError("Invalid input", [
109
+ { field: "disabled", message: "Each entry must be a string" }
110
+ ]);
111
+ }
112
+ if (!known.has(raw)) {
113
+ throw new NpValidationError("Invalid input", [
114
+ { field: "disabled", message: `Unknown notification kind: ${raw}` }
115
+ ]);
116
+ }
117
+ if (seen.has(raw)) continue;
118
+ seen.add(raw);
119
+ cleanedDisabled.push(raw);
120
+ }
121
+ }
122
+ if (input.digest !== void 0 && !DIGEST_CADENCES.includes(input.digest)) {
123
+ throw new NpValidationError("Invalid input", [
124
+ {
125
+ field: "digest",
126
+ message: `digest must be one of: ${DIGEST_CADENCES.join(", ")}`
127
+ }
128
+ ]);
129
+ }
130
+ const db = getDb();
131
+ const [existing] = await db.select({ prefs: npMembers.notificationPrefs }).from(npMembers).where(eq(npMembers.id, input.memberId)).limit(1);
132
+ if (!existing) throw new NpNotFoundError("member", input.memberId);
133
+ const merged = { ...existing.prefs ?? {} };
134
+ if (cleanedDisabled !== void 0) merged.disabled = cleanedDisabled;
135
+ if (input.digest !== void 0) merged.digest = input.digest;
136
+ await db.update(npMembers).set({ notificationPrefs: merged, updatedAt: /* @__PURE__ */ new Date() }).where(eq(npMembers.id, input.memberId));
137
+ return normalizePrefs(merged);
138
+ }
139
+ async function recordDigestSent(memberId, sentAt, scope) {
140
+ const db = getDb();
141
+ const [existing] = await db.select({ prefs: npMembers.notificationPrefs }).from(npMembers).where(eq(npMembers.id, memberId)).limit(1);
142
+ if (!existing) return;
143
+ const prior = existing.prefs ?? {};
144
+ const merged = {
145
+ ...prior,
146
+ lastDigestAt: sentAt.toISOString()
147
+ };
148
+ if (scope) {
149
+ const priorBySite = normalizeLastDigestBySite(
150
+ prior.lastDigestAtBySite
151
+ );
152
+ const siteSlot = { ...priorBySite[scope.siteId] ?? {} };
153
+ siteSlot[scope.cadence] = sentAt.toISOString();
154
+ merged.lastDigestAtBySite = { ...priorBySite, [scope.siteId]: siteSlot };
155
+ }
156
+ await db.update(npMembers).set({ notificationPrefs: merged, updatedAt: /* @__PURE__ */ new Date() }).where(eq(npMembers.id, memberId));
157
+ }
158
+ async function isNotificationKindEnabled(memberId, kind) {
159
+ try {
160
+ const prefs = await getMemberNotificationPrefs(memberId);
161
+ return !prefs.disabled.includes(kind);
162
+ } catch {
163
+ return true;
164
+ }
165
+ }
166
+
167
+ export {
168
+ registerNotificationKind,
169
+ listNotificationKinds,
170
+ getMemberNotificationPrefs,
171
+ setMemberNotificationPrefs,
172
+ recordDigestSent,
173
+ isNotificationKindEnabled
174
+ };
175
+ //# sourceMappingURL=chunk-CTSQ7BRI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/community/notification-prefs.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npMembers } from \"../db/schema/community.js\";\nimport { NpNotFoundError, NpValidationError } from \"../errors.js\";\n\n/**\n * Phase 16.3 — per-member notification preferences.\n *\n * The persisted shape is a JSONB blob on `np_members.notification_prefs`\n * so adding fields (digest cadence in 16.4, channel toggles later)\n * stays a typescript-only change. Today we honor:\n *\n * - `disabled: string[]` — kinds the member opted out of. The\n * `createNotification` gate consults this and silently drops\n * the row. Default empty (= every kind enabled).\n *\n * The vocabulary of `kinds` is defined here so the UI has a single\n * source of truth — settings page renders a toggle for each entry,\n * and the API only accepts kinds that appear in the list (so a\n * forged client can't disable arbitrary strings to bloat the JSONB).\n */\n\nexport interface NpNotificationKindMeta {\n kind: string;\n /** Short human label. */\n label: string;\n /** Description rendered next to the toggle. */\n description: string;\n}\n\n/**\n * Closed vocabulary of toggle-able kinds. New notification kinds\n * land here when they ship; plugins that want their own\n * preferences register entries via `registerNotificationKind`.\n */\nconst builtinKinds: NpNotificationKindMeta[] = [\n {\n kind: \"comment.reply\",\n label: \"Replies\",\n description: \"Someone replied to one of your comments.\",\n },\n {\n kind: \"comment.mention\",\n label: \"Mentions in comments\",\n description: \"Someone @-mentioned you in a comment.\",\n },\n {\n kind: \"document.mention\",\n label: \"Mentions in discussions\",\n description: \"Someone @-mentioned you in a discussion / thread.\",\n },\n {\n kind: \"reaction.received\",\n label: \"Reactions\",\n description: \"Someone reacted to your comment or document.\",\n },\n {\n kind: \"follow.received\",\n label: \"New followers\",\n description: \"Someone started following you.\",\n },\n];\n\nconst dynamicKinds: NpNotificationKindMeta[] = [];\n\n/** Plugin-extensible registration. Idempotent on `kind`. */\nexport function registerNotificationKind(meta: NpNotificationKindMeta): void {\n if (builtinKinds.some((k) => k.kind === meta.kind)) return;\n const idx = dynamicKinds.findIndex((k) => k.kind === meta.kind);\n if (idx >= 0) {\n dynamicKinds[idx] = meta;\n } else {\n dynamicKinds.push(meta);\n }\n}\n\n/** Returns the union of builtin + plugin-registered kinds. */\nexport function listNotificationKinds(): NpNotificationKindMeta[] {\n return [...builtinKinds, ...dynamicKinds];\n}\n\nexport type NpDigestCadence = \"off\" | \"daily\" | \"weekly\";\n\nconst DIGEST_CADENCES: readonly NpDigestCadence[] = [\"off\", \"daily\", \"weekly\"] as const;\n\nexport interface NpNotificationPrefs {\n /** Kinds the member opted out of. Empty / missing = all kinds enabled. */\n disabled: string[];\n /**\n * Phase 16.4 — email digest cadence. `off` (default) disables\n * the digest. `daily` and `weekly` opt the member into a\n * batched email of unread notifications, scheduled by the\n * `notifications:sendDigest` recurring job.\n */\n digest: NpDigestCadence;\n /**\n * Set when the digest sweep last sent an email to this member.\n * Used to scope each digest to \"unread since the last send\" so\n * members aren't repeatedly emailed about the same row. Stored\n * as ISO-8601 string in the JSONB blob; `null` for accounts\n * that have never received a digest.\n *\n * Issue #218 — superseded by `lastDigestAtBySite` once a member\n * receives a digest under the per-site fan-out path. The legacy\n * field is preserved for forward-compat reads (single-site\n * deploys still see + write it via the fallback chain) and as\n * a \"any digest, ever?\" marker for analytics.\n */\n lastDigestAt: string | null;\n /**\n * Issue #218 — per-(site, cadence) timestamp map. Replaces the\n * single `lastDigestAt` for multi-site deployments. Empty when\n * the member has never received a digest under the site-scoped\n * sweep.\n */\n lastDigestAtBySite: Record<string, Partial<Record<NpDigestCadence, string>>>;\n}\n\nconst EMPTY_PREFS: NpNotificationPrefs = {\n disabled: [],\n digest: \"off\",\n lastDigestAt: null,\n lastDigestAtBySite: {},\n};\n\nfunction normalizeDigest(raw: unknown): NpDigestCadence {\n return DIGEST_CADENCES.includes(raw as NpDigestCadence) ? (raw as NpDigestCadence) : \"off\";\n}\n\nfunction normalizeLastDigestAt(raw: unknown): string | null {\n return typeof raw === \"string\" && raw.length > 0 ? raw : null;\n}\n\nfunction normalizeLastDigestBySite(\n raw: unknown,\n): Record<string, Partial<Record<NpDigestCadence, string>>> {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const out: Record<string, Partial<Record<NpDigestCadence, string>>> = {};\n for (const [siteId, value] of Object.entries(raw as Record<string, unknown>)) {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) continue;\n const inner: Partial<Record<NpDigestCadence, string>> = {};\n for (const [cadence, ts] of Object.entries(value as Record<string, unknown>)) {\n if (!DIGEST_CADENCES.includes(cadence as NpDigestCadence)) continue;\n if (typeof ts === \"string\" && ts.length > 0) {\n inner[cadence as NpDigestCadence] = ts;\n }\n }\n if (Object.keys(inner).length > 0) out[siteId] = inner;\n }\n return out;\n}\n\nfunction normalizePrefs(raw: unknown): NpNotificationPrefs {\n if (!raw || typeof raw !== \"object\") return { ...EMPTY_PREFS, lastDigestAtBySite: {} };\n const obj = raw as Record<string, unknown>;\n const disabled = Array.isArray(obj.disabled)\n ? obj.disabled.filter((k): k is string => typeof k === \"string\")\n : [];\n return {\n disabled,\n digest: normalizeDigest(obj.digest),\n lastDigestAt: normalizeLastDigestAt(obj.lastDigestAt),\n lastDigestAtBySite: normalizeLastDigestBySite(obj.lastDigestAtBySite),\n };\n}\n\nexport async function getMemberNotificationPrefs(memberId: string): Promise<NpNotificationPrefs> {\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n const [row] = (await db\n .select({ prefs: npMembers.notificationPrefs })\n .from(npMembers)\n .where(eq(npMembers.id, memberId))\n .limit(1)) as Array<{ prefs: Record<string, unknown> }>;\n if (!row) throw new NpNotFoundError(\"member\", memberId);\n return normalizePrefs(row.prefs);\n}\n\nexport interface SetMemberNotificationPrefsInput {\n memberId: string;\n /**\n * Replacement deny-list. Only kinds listed in\n * `listNotificationKinds()` are accepted; unknown strings\n * raise NpValidationError so a forged client can't bloat the\n * JSONB or hide future framework kinds via a stale list.\n * Optional — when omitted the existing list is preserved.\n */\n disabled?: string[];\n /**\n * Phase 16.4 — email digest cadence. Optional; when omitted\n * the existing setting is preserved. `off` clears the\n * member's enrollment.\n */\n digest?: NpDigestCadence;\n}\n\nexport async function setMemberNotificationPrefs(\n input: SetMemberNotificationPrefsInput,\n): Promise<NpNotificationPrefs> {\n const known = new Set(listNotificationKinds().map((k) => k.kind));\n let cleanedDisabled: string[] | undefined;\n if (input.disabled !== undefined) {\n cleanedDisabled = [];\n const seen = new Set<string>();\n for (const raw of input.disabled) {\n if (typeof raw !== \"string\") {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"disabled\", message: \"Each entry must be a string\" },\n ]);\n }\n if (!known.has(raw)) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"disabled\", message: `Unknown notification kind: ${raw}` },\n ]);\n }\n if (seen.has(raw)) continue;\n seen.add(raw);\n cleanedDisabled.push(raw);\n }\n }\n if (input.digest !== undefined && !DIGEST_CADENCES.includes(input.digest)) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"digest\",\n message: `digest must be one of: ${DIGEST_CADENCES.join(\", \")}`,\n },\n ]);\n }\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n\n // Read-then-merge so we don't clobber other JSONB keys\n // (lastDigestAt, future channel toggles, etc.).\n const [existing] = (await db\n .select({ prefs: npMembers.notificationPrefs })\n .from(npMembers)\n .where(eq(npMembers.id, input.memberId))\n .limit(1)) as Array<{ prefs: Record<string, unknown> }>;\n if (!existing) throw new NpNotFoundError(\"member\", input.memberId);\n\n const merged: Record<string, unknown> = { ...(existing.prefs ?? {}) };\n if (cleanedDisabled !== undefined) merged.disabled = cleanedDisabled;\n if (input.digest !== undefined) merged.digest = input.digest;\n\n await db\n .update(npMembers)\n .set({ notificationPrefs: merged, updatedAt: new Date() })\n .where(eq(npMembers.id, input.memberId));\n\n return normalizePrefs(merged);\n}\n\n/**\n * Phase 16.4 — bookkeeping helper called by the digest sweep\n * after a successful email send. Stamps `lastDigestAt` so the\n * next run scopes its query to the correct window. Read-merge\n * to preserve other JSONB keys.\n *\n * Issue #218 — when a `siteId` + `cadence` pair is supplied,\n * the per-site / per-cadence map is updated so the next sweep\n * for that tenant scopes to the correct \"since\" window. The\n * legacy single `lastDigestAt` field is also stamped for\n * forward-compat with single-site deploys (and as a \"received\n * any digest, ever?\" marker for analytics).\n */\nexport async function recordDigestSent(\n memberId: string,\n sentAt: Date,\n scope?: { siteId: string; cadence: NpDigestCadence },\n): Promise<void> {\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n const [existing] = (await db\n .select({ prefs: npMembers.notificationPrefs })\n .from(npMembers)\n .where(eq(npMembers.id, memberId))\n .limit(1)) as Array<{ prefs: Record<string, unknown> }>;\n if (!existing) return;\n const prior = existing.prefs ?? {};\n const merged: Record<string, unknown> = {\n ...prior,\n lastDigestAt: sentAt.toISOString(),\n };\n if (scope) {\n const priorBySite = normalizeLastDigestBySite(\n (prior as { lastDigestAtBySite?: unknown }).lastDigestAtBySite,\n );\n const siteSlot = { ...(priorBySite[scope.siteId] ?? {}) };\n siteSlot[scope.cadence] = sentAt.toISOString();\n merged.lastDigestAtBySite = { ...priorBySite, [scope.siteId]: siteSlot };\n }\n await db\n .update(npMembers)\n .set({ notificationPrefs: merged, updatedAt: new Date() })\n .where(eq(npMembers.id, memberId));\n}\n\n/**\n * Inbox-side gate consulted by `createNotification`. Returns\n * `false` when the recipient explicitly opted out of `kind`.\n * Errors fail-open (return `true`) so a transient DB blip\n * doesn't silently swallow notifications.\n */\nexport async function isNotificationKindEnabled(memberId: string, kind: string): Promise<boolean> {\n try {\n const prefs = await getMemberNotificationPrefs(memberId);\n return !prefs.disabled.includes(kind);\n } catch {\n return true;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,81 @@
1
+ import {
2
+ readEnvPositiveInt
3
+ } from "./chunk-OROPGO65.js";
4
+ import {
5
+ getLogger
6
+ } from "./chunk-JJL74ZPK.js";
7
+ import {
8
+ getDb
9
+ } from "./chunk-XANPEOJC.js";
10
+ import {
11
+ npJobLogs
12
+ } from "./chunk-M43PGOQY.js";
13
+
14
+ // src/jobs/job-log.ts
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ import { and, asc, eq, gte, lt } from "drizzle-orm";
17
+ var jobLogStorage = new AsyncLocalStorage();
18
+ function runInJobContext(jobId, fn) {
19
+ return jobLogStorage.run({ jobId }, fn);
20
+ }
21
+ function getCurrentJobId() {
22
+ const store = jobLogStorage.getStore();
23
+ return store?.jobId ?? null;
24
+ }
25
+ async function recordJobLog(level, message, context) {
26
+ const jobId = getCurrentJobId();
27
+ if (!jobId) return;
28
+ try {
29
+ const db = getDb();
30
+ await db.insert(npJobLogs).values({
31
+ jobId,
32
+ level,
33
+ message,
34
+ context: context ?? null
35
+ });
36
+ } catch (err) {
37
+ getLogger().warn("recordJobLog failed", {
38
+ jobId,
39
+ level,
40
+ message,
41
+ error: err instanceof Error ? err.message : String(err)
42
+ });
43
+ }
44
+ }
45
+ async function listJobLogs(jobId, options = {}) {
46
+ const limit = Math.min(Math.max(1, options.limit ?? 200), 1e3);
47
+ const offset = Math.max(0, options.offset ?? 0);
48
+ const db = getDb();
49
+ const rows = await db.select().from(npJobLogs).where(eq(npJobLogs.jobId, jobId)).orderBy(asc(npJobLogs.createdAt)).limit(limit).offset(offset);
50
+ return rows.map((row) => ({
51
+ id: row.id,
52
+ jobId: row.jobId,
53
+ level: row.level,
54
+ message: row.message,
55
+ context: row.context,
56
+ createdAt: row.createdAt
57
+ }));
58
+ }
59
+ var DEFAULT_JOB_LOG_RETENTION_MS = readEnvPositiveInt("NP_JOB_LOG_RETENTION_DAYS", 14) * 24 * 60 * 60 * 1e3;
60
+ async function pruneJobLogsOlderThan(cutoff) {
61
+ const db = getDb();
62
+ const deleted = await db.delete(npJobLogs).where(lt(npJobLogs.createdAt, cutoff)).returning({ id: npJobLogs.id });
63
+ return deleted.length;
64
+ }
65
+ async function countJobLogs(jobId, sinceCreatedAt) {
66
+ const db = getDb();
67
+ const where = sinceCreatedAt ? and(eq(npJobLogs.jobId, jobId), gte(npJobLogs.createdAt, sinceCreatedAt)) : eq(npJobLogs.jobId, jobId);
68
+ const rows = await db.select({ id: npJobLogs.id }).from(npJobLogs).where(where);
69
+ return rows.length;
70
+ }
71
+
72
+ export {
73
+ runInJobContext,
74
+ getCurrentJobId,
75
+ recordJobLog,
76
+ listJobLogs,
77
+ DEFAULT_JOB_LOG_RETENTION_MS,
78
+ pruneJobLogsOlderThan,
79
+ countJobLogs
80
+ };
81
+ //# sourceMappingURL=chunk-DK2JBJH7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/jobs/job-log.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport { and, asc, eq, gte, lt } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { readEnvPositiveInt } from \"../config/env.js\";\nimport { npJobLogs } from \"../db/schema/system.js\";\nimport { type NpLogLevel, getLogger } from \"../observability/logger.js\";\n\n/**\n * Phase 20.3 — per-job log capture.\n *\n * Each handler invocation runs inside an AsyncLocalStorage context\n * keyed on the pg-boss job id. While inside the context,\n * `recordJobLog()` writes to `np_job_logs` stamped with that id;\n * outside the context it no-ops, so the helper is safe to import\n * from non-handler code (and from plugins that don't know whether\n * they're inside a handler).\n *\n * The framework's pg-boss adapter sets the context automatically\n * (see `pg-boss-adapter.ts` — every `boss.work()` callback is\n * wrapped in `runInJobContext`). Handlers don't have to do\n * anything to opt in — calls to `recordJobLog()` just work.\n */\n\ninterface JobLogContext {\n jobId: string;\n}\n\nconst jobLogStorage = new AsyncLocalStorage<JobLogContext>();\n\nexport function runInJobContext<T>(jobId: string, fn: () => Promise<T> | T): Promise<T> | T {\n return jobLogStorage.run({ jobId }, fn);\n}\n\nexport function getCurrentJobId(): string | null {\n const store = jobLogStorage.getStore();\n return store?.jobId ?? null;\n}\n\n/**\n * Record one log entry for the currently-running job. Async because\n * it writes to Postgres; callers can `void` the promise if they\n * don't need to wait. No-ops outside a job context (returns\n * immediately without touching the DB).\n *\n * Errors writing to the log table are swallowed via the framework\n * logger at `warn` — a logging failure must never cascade into a\n * job failure or shutdown loop.\n */\nexport async function recordJobLog(\n level: NpLogLevel,\n message: string,\n context?: Record<string, unknown>,\n): Promise<void> {\n const jobId = getCurrentJobId();\n if (!jobId) return;\n\n try {\n const db = getDb();\n await db.insert(npJobLogs).values({\n jobId,\n level,\n message,\n context: context ?? null,\n });\n } catch (err) {\n // Don't throw from a logging path — just surface to whatever\n // sink the framework logger is wired to.\n getLogger().warn(\"recordJobLog failed\", {\n jobId,\n level,\n message,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n}\n\nexport interface NpJobLogEntry {\n id: string;\n jobId: string;\n level: NpLogLevel;\n message: string;\n context: Record<string, unknown> | null;\n createdAt: Date;\n}\n\nexport interface ListJobLogsOptions {\n /** Cap on rows returned. Default 200, max 1000 to keep the admin UI snappy. */\n limit?: number;\n /** Skip this many rows for pagination. */\n offset?: number;\n}\n\n/**\n * Fetch log entries for one job in chronological order. Paged so\n * a runaway handler doesn't blow up the admin UI.\n */\nexport async function listJobLogs(\n jobId: string,\n options: ListJobLogsOptions = {},\n): Promise<NpJobLogEntry[]> {\n const limit = Math.min(Math.max(1, options.limit ?? 200), 1000);\n const offset = Math.max(0, options.offset ?? 0);\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n\n const rows = (await db\n .select()\n .from(npJobLogs)\n .where(eq(npJobLogs.jobId, jobId))\n .orderBy(asc(npJobLogs.createdAt))\n .limit(limit)\n .offset(offset)) as Array<{\n id: string;\n jobId: string;\n level: string;\n message: string;\n context: Record<string, unknown> | null;\n createdAt: Date;\n }>;\n\n return rows.map((row) => ({\n id: row.id,\n jobId: row.jobId,\n level: row.level as NpLogLevel,\n message: row.message,\n context: row.context,\n createdAt: row.createdAt,\n }));\n}\n\n/**\n * How long per-job log rows survive before the cleanup handler\n * deletes them. Compliance regimes (GDPR, SOX) frequently dictate\n * a specific window — override via `NP_JOB_LOG_RETENTION_DAYS`.\n */\nexport const DEFAULT_JOB_LOG_RETENTION_MS =\n readEnvPositiveInt(\"NP_JOB_LOG_RETENTION_DAYS\", 14) * 24 * 60 * 60 * 1000;\n\n/**\n * Delete log rows older than the cutoff. Safe to call from a\n * scheduled handler — does not touch logs for active or recent\n * jobs unless they pre-date the cutoff.\n *\n * Returns the row count deleted so the cron handler can log a\n * useful retention summary.\n */\nexport async function pruneJobLogsOlderThan(cutoff: Date): Promise<number> {\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n const deleted = (await db\n .delete(npJobLogs)\n .where(lt(npJobLogs.createdAt, cutoff))\n .returning({ id: npJobLogs.id })) as Array<{ id: string }>;\n return deleted.length;\n}\n\n/**\n * Count entries for a job — drives the admin badge \"37 log lines\"\n * without paying for the page payload until the operator expands.\n */\nexport async function countJobLogs(jobId: string, sinceCreatedAt?: Date): Promise<number> {\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n const where = sinceCreatedAt\n ? and(eq(npJobLogs.jobId, jobId), gte(npJobLogs.createdAt, sinceCreatedAt))\n : eq(npJobLogs.jobId, jobId);\n const rows = (await db.select({ id: npJobLogs.id }).from(npJobLogs).where(where)) as Array<{\n id: string;\n }>;\n return rows.length;\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,yBAAyB;AAElC,SAAS,KAAK,KAAK,IAAI,KAAK,UAAU;AA4BtC,IAAM,gBAAgB,IAAI,kBAAiC;AAEpD,SAAS,gBAAmB,OAAe,IAA0C;AAC1F,SAAO,cAAc,IAAI,EAAE,MAAM,GAAG,EAAE;AACxC;AAEO,SAAS,kBAAiC;AAC/C,QAAM,QAAQ,cAAc,SAAS;AACrC,SAAO,OAAO,SAAS;AACzB;AAYA,eAAsB,aACpB,OACA,SACA,SACe;AACf,QAAM,QAAQ,gBAAgB;AAC9B,MAAI,CAAC,MAAO;AAEZ,MAAI;AACF,UAAM,KAAK,MAAM;AACjB,UAAM,GAAG,OAAO,SAAS,EAAE,OAAO;AAAA,MAChC;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,WAAW;AAAA,IACtB,CAAC;AAAA,EACH,SAAS,KAAK;AAGZ,cAAU,EAAE,KAAK,uBAAuB;AAAA,MACtC;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACxD,CAAC;AAAA,EACH;AACF;AAsBA,eAAsB,YACpB,OACA,UAA8B,CAAC,GACL;AAC1B,QAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAG,QAAQ,SAAS,GAAG,GAAG,GAAI;AAC9D,QAAM,SAAS,KAAK,IAAI,GAAG,QAAQ,UAAU,CAAC;AAC9C,QAAM,KAAK,MAAM;AAEjB,QAAM,OAAQ,MAAM,GACjB,OAAO,EACP,KAAK,SAAS,EACd,MAAM,GAAG,UAAU,OAAO,KAAK,CAAC,EAChC,QAAQ,IAAI,UAAU,SAAS,CAAC,EAChC,MAAM,KAAK,EACX,OAAO,MAAM;AAShB,SAAO,KAAK,IAAI,CAAC,SAAS;AAAA,IACxB,IAAI,IAAI;AAAA,IACR,OAAO,IAAI;AAAA,IACX,OAAO,IAAI;AAAA,IACX,SAAS,IAAI;AAAA,IACb,SAAS,IAAI;AAAA,IACb,WAAW,IAAI;AAAA,EACjB,EAAE;AACJ;AAOO,IAAM,+BACX,mBAAmB,6BAA6B,EAAE,IAAI,KAAK,KAAK,KAAK;AAUvE,eAAsB,sBAAsB,QAA+B;AACzE,QAAM,KAAK,MAAM;AACjB,QAAM,UAAW,MAAM,GACpB,OAAO,SAAS,EAChB,MAAM,GAAG,UAAU,WAAW,MAAM,CAAC,EACrC,UAAU,EAAE,IAAI,UAAU,GAAG,CAAC;AACjC,SAAO,QAAQ;AACjB;AAMA,eAAsB,aAAa,OAAe,gBAAwC;AACxF,QAAM,KAAK,MAAM;AACjB,QAAM,QAAQ,iBACV,IAAI,GAAG,UAAU,OAAO,KAAK,GAAG,IAAI,UAAU,WAAW,cAAc,CAAC,IACxE,GAAG,UAAU,OAAO,KAAK;AAC7B,QAAM,OAAQ,MAAM,GAAG,OAAO,EAAE,IAAI,UAAU,GAAG,CAAC,EAAE,KAAK,SAAS,EAAE,MAAM,KAAK;AAG/E,SAAO,KAAK;AACd;","names":[]}