@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,715 @@
1
+ // src/db/schema/system.ts
2
+ import {
3
+ boolean as boolean2,
4
+ index as index3,
5
+ integer as integer3,
6
+ jsonb as jsonb3,
7
+ pgEnum as pgEnum3,
8
+ pgTable as pgTable3,
9
+ primaryKey as primaryKey2,
10
+ text as text3,
11
+ timestamp as timestamp3,
12
+ unique as unique2,
13
+ uuid as uuid3
14
+ } from "drizzle-orm/pg-core";
15
+
16
+ // src/db/schema/media.ts
17
+ import {
18
+ bigint,
19
+ index as index2,
20
+ integer as integer2,
21
+ jsonb as jsonb2,
22
+ pgEnum as pgEnum2,
23
+ pgTable as pgTable2,
24
+ text as text2,
25
+ timestamp as timestamp2,
26
+ uuid as uuid2
27
+ } from "drizzle-orm/pg-core";
28
+
29
+ // src/db/schema/community.ts
30
+ import {
31
+ boolean,
32
+ index,
33
+ integer,
34
+ jsonb,
35
+ pgEnum,
36
+ pgTable,
37
+ primaryKey,
38
+ text,
39
+ timestamp,
40
+ unique,
41
+ uuid
42
+ } from "drizzle-orm/pg-core";
43
+ var npMemberStatusEnum = pgEnum("np_member_status", [
44
+ "active",
45
+ "pending",
46
+ "suspended",
47
+ "deleted",
48
+ "imported"
49
+ ]);
50
+ var npBanScopeEnum = pgEnum("np_ban_scope", ["site", "category", "collection"]);
51
+ var npBanKindEnum = pgEnum("np_ban_kind", ["temporary", "permanent"]);
52
+ var npCommentStatusEnum = pgEnum("np_comment_status", [
53
+ "visible",
54
+ "pending",
55
+ "hidden",
56
+ "deleted"
57
+ ]);
58
+ var npMemberRoleScopeEnum = pgEnum("np_member_role_scope", [
59
+ "site",
60
+ "category",
61
+ "collection",
62
+ "thread"
63
+ ]);
64
+ var npMembers = pgTable(
65
+ "np_members",
66
+ {
67
+ id: uuid("id").defaultRandom().primaryKey(),
68
+ handle: text("handle").notNull().unique(),
69
+ email: text("email").notNull().unique(),
70
+ emailVerified: boolean("email_verified").default(false).notNull(),
71
+ /** Argon2 hash. Nullable so SSO-only members can exist without a password. */
72
+ password: text("password"),
73
+ displayName: text("display_name").notNull(),
74
+ avatar: uuid("avatar").references(() => npMedia.id),
75
+ bio: text("bio"),
76
+ status: npMemberStatusEnum("status").default("pending").notNull(),
77
+ reputation: integer("reputation").default(0).notNull(),
78
+ loginAttempts: integer("login_attempts").default(0).notNull(),
79
+ lockUntil: timestamp("lock_until", { withTimezone: true, mode: "date" }),
80
+ /** Bumped to invalidate every issued JWT (logout-everywhere, password reset). */
81
+ tokenVersion: integer("token_version").default(0).notNull(),
82
+ passwordResetTokenHash: text("password_reset_token_hash"),
83
+ passwordResetExpiresAt: timestamp("password_reset_expires_at", {
84
+ withTimezone: true,
85
+ mode: "date"
86
+ }),
87
+ emailVerifyTokenHash: text("email_verify_token_hash"),
88
+ emailVerifyExpiresAt: timestamp("email_verify_expires_at", {
89
+ withTimezone: true,
90
+ mode: "date"
91
+ }),
92
+ /** Plugin-extensible bag — preferences, custom profile fields, etc. */
93
+ meta: jsonb("meta").$type().default({}).notNull(),
94
+ /**
95
+ * Phase 16.3 — per-member notification preferences. Shape:
96
+ * { disabled?: string[] } — kinds the member opted out of
97
+ * { digest?: "off"|"daily"|"weekly" } — email digest cadence (16.4)
98
+ * Empty default = every kind enabled, no email digest.
99
+ */
100
+ notificationPrefs: jsonb("notification_prefs").$type().default({}).notNull(),
101
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
102
+ updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
103
+ },
104
+ (table) => [index("np_members_status_idx").on(table.status)]
105
+ );
106
+ var npMemberSessions = pgTable("np_member_sessions", {
107
+ id: uuid("id").defaultRandom().primaryKey(),
108
+ memberId: uuid("member_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
109
+ tokenHash: text("token_hash").notNull(),
110
+ userAgent: text("user_agent"),
111
+ ip: text("ip"),
112
+ expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(),
113
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
114
+ });
115
+ var npMemberIdentities = pgTable(
116
+ "np_member_identities",
117
+ {
118
+ id: uuid("id").defaultRandom().primaryKey(),
119
+ memberId: uuid("member_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
120
+ provider: text("provider").notNull(),
121
+ subject: text("subject").notNull(),
122
+ email: text("email"),
123
+ /** Free-form per-provider metadata (avatar URL, scopes granted, etc.). */
124
+ metadata: jsonb("metadata").$type().default({}).notNull(),
125
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
126
+ updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
127
+ },
128
+ (table) => [
129
+ unique("np_member_identities_provider_subject_uq").on(table.provider, table.subject),
130
+ unique("np_member_identities_member_provider_uq").on(table.memberId, table.provider),
131
+ index("np_member_identities_member_idx").on(table.memberId)
132
+ ]
133
+ );
134
+ var npMemberRoles = pgTable(
135
+ "np_member_roles",
136
+ {
137
+ id: uuid("id").defaultRandom().primaryKey(),
138
+ memberId: uuid("member_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
139
+ role: text("role").notNull(),
140
+ scopeType: npMemberRoleScopeEnum("scope_type").notNull(),
141
+ /** Nullable for `scope_type='site'`. Otherwise an opaque string id. */
142
+ scopeId: text("scope_id"),
143
+ /**
144
+ * Phase 18 — the tenant the grant applies on. For
145
+ * `scope_type='site'` this column IS the site identifier
146
+ * (`scope_id` stays null because site is the root scope).
147
+ * For category / collection / thread grants, `site_id` says
148
+ * which tenant's category/collection/thread this row
149
+ * targets — the same slug exists on every site.
150
+ */
151
+ siteId: text("site_id").default("default").notNull(),
152
+ grantedBy: uuid("granted_by").references(() => npUsers.id),
153
+ grantedAt: timestamp("granted_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
154
+ expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" })
155
+ },
156
+ (table) => [
157
+ // Two indexes mirror the two access patterns: "what can this member
158
+ // do?" (memberId scan) and "who mods this scope?" (scope scan).
159
+ index("np_member_roles_member_idx").on(table.memberId),
160
+ index("np_member_roles_scope_idx").on(table.scopeType, table.scopeId),
161
+ index("np_member_roles_site_idx").on(table.siteId, table.memberId),
162
+ // `scope_id` is null for site-wide grants. NULLS NOT
163
+ // DISTINCT makes two null `scope_id`s collide so the
164
+ // unique constraint enforces "one grant per (member, role,
165
+ // scope, site)." `site_id` widens the key so the same
166
+ // member can hold the same role on different tenants.
167
+ unique("np_member_roles_grant_uq").on(table.memberId, table.role, table.scopeType, table.scopeId, table.siteId).nullsNotDistinct()
168
+ ]
169
+ );
170
+ var npComments = pgTable(
171
+ "np_comments",
172
+ {
173
+ id: uuid("id").defaultRandom().primaryKey(),
174
+ targetType: text("target_type").notNull(),
175
+ targetId: uuid("target_id").notNull(),
176
+ parentId: uuid("parent_id").references(() => npComments.id, {
177
+ onDelete: "cascade"
178
+ }),
179
+ memberId: uuid("member_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
180
+ bodyMd: text("body_md").notNull(),
181
+ bodyHtml: text("body_html").notNull(),
182
+ status: npCommentStatusEnum("status").default("visible").notNull(),
183
+ hiddenByUserId: uuid("hidden_by_user_id").references(() => npUsers.id),
184
+ hiddenByMemberId: uuid("hidden_by_member_id").references(() => npMembers.id),
185
+ hiddenReason: text("hidden_reason"),
186
+ editedAt: timestamp("edited_at", { withTimezone: true, mode: "date" }),
187
+ /**
188
+ * Phase 18 — site this comment belongs to. Filled at insert
189
+ * time from the target document's site (canonical) so a
190
+ * forged request resolver can't smuggle a comment into the
191
+ * wrong site. Defaults to `'default'` for legacy single-
192
+ * tenant rows so the migration backfill is a no-op.
193
+ */
194
+ siteId: text("site_id").default("default").notNull(),
195
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
196
+ },
197
+ (table) => [
198
+ index("np_comments_target_idx").on(table.targetType, table.targetId, table.createdAt),
199
+ index("np_comments_member_idx").on(table.memberId, table.createdAt),
200
+ index("np_comments_site_idx").on(table.siteId, table.createdAt)
201
+ ]
202
+ );
203
+ var npReactions = pgTable(
204
+ "np_reactions",
205
+ {
206
+ id: uuid("id").defaultRandom().primaryKey(),
207
+ targetType: text("target_type").notNull(),
208
+ targetId: uuid("target_id").notNull(),
209
+ memberId: uuid("member_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
210
+ kind: text("kind").notNull(),
211
+ /** Phase 18 — site this reaction belongs to (derived from target). */
212
+ siteId: text("site_id").default("default").notNull(),
213
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
214
+ },
215
+ (table) => [
216
+ index("np_reactions_target_idx").on(table.targetType, table.targetId),
217
+ index("np_reactions_site_idx").on(table.siteId),
218
+ unique("np_reactions_unique").on(table.targetType, table.targetId, table.memberId, table.kind)
219
+ ]
220
+ );
221
+ var npFollows = pgTable(
222
+ "np_follows",
223
+ {
224
+ id: uuid("id").defaultRandom().primaryKey(),
225
+ followerId: uuid("follower_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
226
+ targetType: text("target_type").notNull(),
227
+ targetId: text("target_id").notNull(),
228
+ /**
229
+ * Phase 18 — site the follow happened on. The same global
230
+ * member can follow on multiple sites and each row scopes
231
+ * to where the click happened (so site-scoped notifications
232
+ * + activity feeds don't leak cross-tenant). The unique
233
+ * key is widened to include site_id so the same follower
234
+ * can have parallel follow rows under different tenants.
235
+ */
236
+ siteId: text("site_id").default("default").notNull(),
237
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
238
+ },
239
+ (table) => [
240
+ index("np_follows_target_idx").on(table.targetType, table.targetId),
241
+ index("np_follows_site_idx").on(table.siteId),
242
+ unique("np_follows_unique").on(
243
+ table.followerId,
244
+ table.targetType,
245
+ table.targetId,
246
+ table.siteId
247
+ )
248
+ ]
249
+ );
250
+ var npMemberMutes = pgTable(
251
+ "np_member_mutes",
252
+ {
253
+ memberId: uuid("member_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
254
+ targetId: uuid("target_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
255
+ /**
256
+ * Phase 18 — site the mute applies to. A muter can choose
257
+ * to silence someone on one tenant without affecting their
258
+ * other tenants. PK is widened to include site_id so the
259
+ * same `(member, target)` pair can hold parallel rows per
260
+ * site.
261
+ */
262
+ siteId: text("site_id").default("default").notNull(),
263
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
264
+ },
265
+ (table) => [
266
+ primaryKey({ columns: [table.memberId, table.targetId, table.siteId] }),
267
+ index("np_member_mutes_target_idx").on(table.targetId)
268
+ ]
269
+ );
270
+ var npNotifications = pgTable(
271
+ "np_notifications",
272
+ {
273
+ id: uuid("id").defaultRandom().primaryKey(),
274
+ memberId: uuid("member_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
275
+ kind: text("kind").notNull(),
276
+ payload: jsonb("payload").$type().default({}).notNull(),
277
+ readAt: timestamp("read_at", { withTimezone: true, mode: "date" }),
278
+ /**
279
+ * Phase 18 — site this notification belongs to. A member
280
+ * who's active on multiple tenants gets one inbox per site
281
+ * (the inbox API filters by current site) so cross-tenant
282
+ * activity doesn't bleed into the wrong site's UI.
283
+ */
284
+ siteId: text("site_id").default("default").notNull(),
285
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
286
+ },
287
+ (table) => [
288
+ index("np_notifications_inbox_idx").on(table.memberId, table.readAt, table.createdAt),
289
+ index("np_notifications_site_inbox_idx").on(table.siteId, table.memberId, table.readAt)
290
+ ]
291
+ );
292
+ var npReports = pgTable(
293
+ "np_reports",
294
+ {
295
+ id: uuid("id").defaultRandom().primaryKey(),
296
+ reporterId: uuid("reporter_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
297
+ targetType: text("target_type").notNull(),
298
+ targetId: text("target_id").notNull(),
299
+ reason: text("reason").notNull(),
300
+ resolvedAt: timestamp("resolved_at", { withTimezone: true, mode: "date" }),
301
+ resolvedByUserId: uuid("resolved_by_user_id").references(() => npUsers.id),
302
+ resolvedByMemberId: uuid("resolved_by_member_id").references(() => npMembers.id),
303
+ resolution: text("resolution"),
304
+ /**
305
+ * Phase 18 — site this report belongs to. The mod queue
306
+ * is per-site so a category-mod on tenant A doesn't see
307
+ * tenant B's reports.
308
+ */
309
+ siteId: text("site_id").default("default").notNull(),
310
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
311
+ },
312
+ (table) => [
313
+ index("np_reports_queue_idx").on(table.resolvedAt, table.createdAt),
314
+ index("np_reports_target_idx").on(table.targetType, table.targetId),
315
+ index("np_reports_site_queue_idx").on(table.siteId, table.resolvedAt)
316
+ ]
317
+ );
318
+ var npAuditEvents = pgTable(
319
+ "np_audit_events",
320
+ {
321
+ id: uuid("id").defaultRandom().primaryKey(),
322
+ actorKind: text("actor_kind").notNull(),
323
+ actorUserId: uuid("actor_user_id").references(() => npUsers.id),
324
+ actorMemberId: uuid("actor_member_id").references(() => npMembers.id),
325
+ action: text("action").notNull(),
326
+ targetType: text("target_type"),
327
+ targetId: text("target_id"),
328
+ payload: jsonb("payload").$type().default({}).notNull(),
329
+ /**
330
+ * Phase 17 — site-scoped audit. Filled by `recordAuditEvent`
331
+ * from the current request's site (the multi-site resolver).
332
+ * Nullable for events that don't belong to a single site
333
+ * (super-admin actions, background jobs, scripts).
334
+ */
335
+ siteId: text("site_id"),
336
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
337
+ },
338
+ (table) => [
339
+ index("np_audit_target_idx").on(table.targetType, table.targetId, table.createdAt),
340
+ index("np_audit_actor_user_idx").on(table.actorUserId, table.createdAt),
341
+ index("np_audit_actor_member_idx").on(table.actorMemberId, table.createdAt),
342
+ index("np_audit_site_idx").on(table.siteId, table.createdAt)
343
+ ]
344
+ );
345
+ var npBans = pgTable(
346
+ "np_bans",
347
+ {
348
+ id: uuid("id").defaultRandom().primaryKey(),
349
+ memberId: uuid("member_id").notNull().references(() => npMembers.id, { onDelete: "cascade" }),
350
+ scopeType: npBanScopeEnum("scope_type").notNull(),
351
+ scopeId: text("scope_id"),
352
+ kind: npBanKindEnum("kind").notNull(),
353
+ expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }),
354
+ reason: text("reason"),
355
+ byUserId: uuid("by_user_id").references(() => npUsers.id),
356
+ byMemberId: uuid("by_member_id").references(() => npMembers.id),
357
+ /**
358
+ * Phase 18 — the tenant this ban applies to. Pre-Phase 18
359
+ * `scope_type='site'` rows had `scope_id=null` because
360
+ * "site" was the singular root scope; with multi-tenancy
361
+ * the column tells `assertNotBanned` WHICH site the ban
362
+ * blocks writes on. Category / collection scopes resolve
363
+ * per-site too — the same `posts` collection slug exists
364
+ * on every tenant.
365
+ */
366
+ siteId: text("site_id").default("default").notNull(),
367
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
368
+ },
369
+ (table) => [
370
+ index("np_bans_member_scope_idx").on(table.memberId, table.scopeType, table.scopeId),
371
+ index("np_bans_active_idx").on(table.memberId, table.expiresAt),
372
+ index("np_bans_site_idx").on(table.siteId, table.memberId)
373
+ ]
374
+ );
375
+
376
+ // src/db/schema/media.ts
377
+ var npMediaStatusEnum = pgEnum2("np_media_status", [
378
+ "processing",
379
+ "ready",
380
+ "error"
381
+ ]);
382
+ var npMediaFolders = pgTable2("np_media_folders", {
383
+ id: uuid2("id").defaultRandom().primaryKey(),
384
+ name: text2("name").notNull(),
385
+ parentId: uuid2("parent_id").references(() => npMediaFolders.id),
386
+ createdAt: timestamp2("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
387
+ });
388
+ var npMedia = pgTable2(
389
+ "np_media",
390
+ {
391
+ id: uuid2("id").defaultRandom().primaryKey(),
392
+ filename: text2("filename").notNull(),
393
+ originalFilename: text2("original_filename").notNull(),
394
+ mimeType: text2("mime_type").notNull(),
395
+ filesize: bigint("filesize", { mode: "number" }).notNull(),
396
+ width: integer2("width"),
397
+ height: integer2("height"),
398
+ alt: text2("alt"),
399
+ caption: jsonb2("caption").$type(),
400
+ focalPoint: jsonb2("focal_point").$type(),
401
+ sizes: jsonb2("sizes").$type(),
402
+ storageKey: text2("storage_key").notNull(),
403
+ hash: text2("hash").notNull(),
404
+ status: npMediaStatusEnum("status").notNull(),
405
+ folderId: uuid2("folder_id").references(() => npMediaFolders.id),
406
+ uploadedBy: uuid2("uploaded_by").references(() => npUsers.id),
407
+ /**
408
+ * Set when a member uploaded the row instead of a staff user
409
+ * (Phase 9.7j). Mutually exclusive with `uploadedBy`: a row
410
+ * has exactly one uploader. Member-side moderation tools key
411
+ * off this column to filter "uploads I should review."
412
+ * `ON DELETE SET NULL` so a member account deletion doesn't
413
+ * cascade-delete their uploads — staff still need them for
414
+ * the audit trail (just like `member_author_id` on
415
+ * collection tables).
416
+ */
417
+ uploadedByMemberId: uuid2("uploaded_by_member_id").references(
418
+ () => npMembers.id,
419
+ { onDelete: "set null" }
420
+ ),
421
+ createdAt: timestamp2("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
422
+ updatedAt: timestamp2("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
423
+ deletedAt: timestamp2("deleted_at", { withTimezone: true, mode: "date" })
424
+ },
425
+ (table) => ({
426
+ hashIdx: index2("np_media_hash_idx").on(table.hash),
427
+ statusIdx: index2("np_media_status_idx").on(table.status),
428
+ uploadedByMemberIdx: index2("np_media_uploaded_by_member_idx").on(
429
+ table.uploadedByMemberId
430
+ )
431
+ })
432
+ );
433
+ var npMediaRefs = pgTable2(
434
+ "np_media_refs",
435
+ {
436
+ id: uuid2("id").defaultRandom().primaryKey(),
437
+ mediaId: uuid2("media_id").notNull().references(() => npMedia.id, { onDelete: "cascade" }),
438
+ collection: text2("collection").notNull(),
439
+ documentId: text2("document_id").notNull(),
440
+ field: text2("field").notNull()
441
+ },
442
+ (table) => ({
443
+ mediaIdIdx: index2("np_media_refs_media_id_idx").on(table.mediaId),
444
+ documentIdIdx: index2("np_media_refs_document_id_idx").on(table.documentId)
445
+ })
446
+ );
447
+
448
+ // src/db/schema/system.ts
449
+ var npUserRoleEnum = pgEnum3("np_user_role", [
450
+ "admin",
451
+ "editor",
452
+ // 9.5: community moderator. Sits OUTSIDE the linear content-edit
453
+ // hierarchy — a moderator handles community moderation (hide
454
+ // comments, resolve reports, issue bans) but cannot author or edit
455
+ // collection content. ROLE_HIERARCHY in config/types.ts intentionally
456
+ // does not list this role; community-moderation paths check the role
457
+ // explicitly via `principalCan()`.
458
+ "moderator",
459
+ "author",
460
+ "viewer"
461
+ ]);
462
+ var npRevisionStatusEnum = pgEnum3("np_revision_status", [
463
+ "draft",
464
+ "published",
465
+ "autosave"
466
+ ]);
467
+ var npPasswordResetPurposeEnum = pgEnum3("np_password_reset_purpose", ["invite", "reset"]);
468
+ var npUsers = pgTable3("np_users", {
469
+ id: uuid3("id").defaultRandom().primaryKey(),
470
+ email: text3("email").notNull().unique(),
471
+ password: text3("password").notNull(),
472
+ name: text3("name").notNull(),
473
+ role: npUserRoleEnum("role").notNull(),
474
+ /**
475
+ * Phase 15.5 — super-admin flag. Bypasses per-site membership
476
+ * checks; the super-admin can manage every site including
477
+ * creating / deleting tenants. The flag is independent of
478
+ * the per-site `role` enum (a super-admin still needs a
479
+ * `role` field for non-multi-site contexts; multi-site
480
+ * permissions check `is_super_admin OR site_membership`).
481
+ */
482
+ isSuperAdmin: boolean2("is_super_admin").default(false).notNull(),
483
+ avatar: uuid3("avatar").references(() => npMedia.id),
484
+ loginAttempts: integer3("login_attempts").default(0).notNull(),
485
+ lockUntil: timestamp3("lock_until", { withTimezone: true, mode: "date" }),
486
+ tokenVersion: integer3("token_version").default(0).notNull(),
487
+ passwordResetTokenHash: text3("password_reset_token_hash"),
488
+ passwordResetExpiresAt: timestamp3("password_reset_expires_at", {
489
+ withTimezone: true,
490
+ mode: "date"
491
+ }),
492
+ passwordResetPurpose: npPasswordResetPurposeEnum("password_reset_purpose"),
493
+ createdAt: timestamp3("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
494
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
495
+ });
496
+ var npSiteMemberships = pgTable3(
497
+ "np_site_memberships",
498
+ {
499
+ siteId: text3("site_id").notNull(),
500
+ userId: uuid3("user_id").notNull().references(() => npUsers.id, { onDelete: "cascade" }),
501
+ role: npUserRoleEnum("role").notNull(),
502
+ createdAt: timestamp3("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
503
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
504
+ },
505
+ (table) => [primaryKey2({ columns: [table.siteId, table.userId] })]
506
+ );
507
+ var npUserOAuthIdentities = pgTable3(
508
+ "np_user_oauth_identities",
509
+ {
510
+ id: uuid3("id").defaultRandom().primaryKey(),
511
+ userId: uuid3("user_id").notNull().references(() => npUsers.id, { onDelete: "cascade" }),
512
+ provider: text3("provider").notNull(),
513
+ providerUserId: text3("provider_user_id").notNull(),
514
+ /** Free-form per-provider metadata (avatar URL, scopes granted, etc.). */
515
+ metadata: jsonb3("metadata").$type().default({}).notNull(),
516
+ createdAt: timestamp3("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
517
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
518
+ },
519
+ (table) => ({
520
+ providerSubjectUnique: unique2("np_user_oauth_identities_provider_subject_unique").on(
521
+ table.provider,
522
+ table.providerUserId
523
+ ),
524
+ userProviderUnique: unique2("np_user_oauth_identities_user_provider_unique").on(
525
+ table.userId,
526
+ table.provider
527
+ ),
528
+ userIdx: index3("np_user_oauth_identities_user_idx").on(table.userId)
529
+ })
530
+ );
531
+ var npSessions = pgTable3("np_sessions", {
532
+ id: uuid3("id").defaultRandom().primaryKey(),
533
+ userId: uuid3("user_id").notNull().references(() => npUsers.id, { onDelete: "cascade" }),
534
+ tokenHash: text3("token_hash").notNull(),
535
+ userAgent: text3("user_agent"),
536
+ ip: text3("ip"),
537
+ expiresAt: timestamp3("expires_at", { withTimezone: true, mode: "date" }).notNull(),
538
+ createdAt: timestamp3("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
539
+ });
540
+ var npRevisions = pgTable3(
541
+ "np_revisions",
542
+ {
543
+ id: uuid3("id").defaultRandom().primaryKey(),
544
+ collection: text3("collection").notNull(),
545
+ documentId: text3("document_id").notNull(),
546
+ version: integer3("version").notNull(),
547
+ status: npRevisionStatusEnum("status").notNull(),
548
+ snapshot: jsonb3("snapshot").$type().notNull(),
549
+ changedFields: text3("changed_fields").array().notNull(),
550
+ authorId: uuid3("author_id").references(() => npUsers.id),
551
+ createdAt: timestamp3("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
552
+ },
553
+ (table) => ({
554
+ documentVersionUnique: unique2("np_revisions_document_id_version_unique").on(
555
+ table.documentId,
556
+ table.version
557
+ ),
558
+ collectionIdx: index3("np_revisions_collection_idx").on(table.collection),
559
+ documentIdIdx: index3("np_revisions_document_id_idx").on(table.documentId)
560
+ })
561
+ );
562
+ var npSettings = pgTable3(
563
+ "np_settings",
564
+ {
565
+ siteId: text3("site_id").default("default").notNull(),
566
+ key: text3("key").notNull(),
567
+ value: jsonb3("value").$type().notNull(),
568
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
569
+ updatedBy: uuid3("updated_by").references(() => npUsers.id)
570
+ },
571
+ (table) => [primaryKey2({ columns: [table.siteId, table.key] })]
572
+ );
573
+ var npSlugHistory = pgTable3(
574
+ "np_slug_history",
575
+ {
576
+ id: uuid3("id").defaultRandom().primaryKey(),
577
+ siteId: text3("site_id").default("default").notNull(),
578
+ collection: text3("collection").notNull(),
579
+ documentId: text3("document_id").notNull(),
580
+ oldSlug: text3("old_slug").notNull(),
581
+ newSlug: text3("new_slug").notNull(),
582
+ createdAt: timestamp3("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
583
+ },
584
+ (table) => [
585
+ index3("np_slug_history_lookup_idx").on(table.siteId, table.collection, table.oldSlug),
586
+ index3("np_slug_history_doc_idx").on(table.siteId, table.collection, table.documentId)
587
+ ]
588
+ );
589
+ var npNavigation = pgTable3(
590
+ "np_navigation",
591
+ {
592
+ id: uuid3("id").defaultRandom().primaryKey(),
593
+ siteId: text3("site_id").default("default").notNull(),
594
+ location: text3("location").notNull(),
595
+ items: jsonb3("items").$type().notNull(),
596
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
597
+ updatedBy: uuid3("updated_by").references(() => npUsers.id)
598
+ },
599
+ (table) => [unique2("np_navigation_site_location_idx").on(table.siteId, table.location)]
600
+ );
601
+ var npStringOverrides = pgTable3(
602
+ "np_string_overrides",
603
+ {
604
+ siteId: text3("site_id").default("default").notNull(),
605
+ locale: text3("locale").notNull(),
606
+ key: text3("key").notNull(),
607
+ value: text3("value"),
608
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
609
+ updatedBy: uuid3("updated_by").references(() => npUsers.id)
610
+ },
611
+ (table) => [primaryKey2({ columns: [table.siteId, table.locale, table.key] })]
612
+ );
613
+ var npSites = pgTable3(
614
+ "np_sites",
615
+ {
616
+ id: text3("id").primaryKey(),
617
+ name: text3("name").notNull(),
618
+ hostname: text3("hostname"),
619
+ description: text3("description"),
620
+ settings: jsonb3("settings").$type().default({}).notNull(),
621
+ isDefault: boolean2("is_default").default(false).notNull(),
622
+ createdAt: timestamp3("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
623
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
624
+ },
625
+ (table) => [unique2("np_sites_hostname_idx").on(table.hostname)]
626
+ );
627
+ var npPlugins = pgTable3("np_plugins", {
628
+ id: text3("id").primaryKey(),
629
+ enabled: boolean2("enabled").default(true).notNull(),
630
+ installedAt: timestamp3("installed_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
631
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
632
+ });
633
+ var NP_GLOBAL_PLUGIN_SITE_ID = "_global_";
634
+ var npPluginStorage = pgTable3(
635
+ "np_plugin_storage",
636
+ {
637
+ pluginId: text3("plugin_id").notNull(),
638
+ siteId: text3("site_id").default(NP_GLOBAL_PLUGIN_SITE_ID).notNull(),
639
+ key: text3("key").notNull(),
640
+ value: jsonb3("value").$type().notNull(),
641
+ expiresAt: timestamp3("expires_at", { withTimezone: true, mode: "date" }),
642
+ updatedAt: timestamp3("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
643
+ },
644
+ (table) => ({
645
+ pk: primaryKey2({ columns: [table.pluginId, table.siteId, table.key] }),
646
+ pluginIdx: index3("np_plugin_storage_plugin_id_idx").on(table.pluginId),
647
+ siteIdx: index3("np_plugin_storage_site_idx").on(table.siteId)
648
+ })
649
+ );
650
+ var npWorkerHeartbeats = pgTable3("np_worker_heartbeats", {
651
+ id: text3("id").primaryKey(),
652
+ status: text3("status").default("running").notNull(),
653
+ startedAt: timestamp3("started_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
654
+ lastSeenAt: timestamp3("last_seen_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
655
+ /** Free-form metadata (worker version, hostname, env). */
656
+ meta: jsonb3("meta").$type().default({}).notNull()
657
+ });
658
+ var npJobLogs = pgTable3(
659
+ "np_job_logs",
660
+ {
661
+ id: uuid3("id").defaultRandom().primaryKey(),
662
+ jobId: text3("job_id").notNull(),
663
+ level: text3("level").notNull(),
664
+ message: text3("message").notNull(),
665
+ context: jsonb3("context").$type(),
666
+ createdAt: timestamp3("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull()
667
+ },
668
+ (table) => [
669
+ index3("np_job_logs_job_idx").on(table.jobId, table.createdAt),
670
+ index3("np_job_logs_created_idx").on(table.createdAt)
671
+ ]
672
+ );
673
+
674
+ export {
675
+ npUserRoleEnum,
676
+ npRevisionStatusEnum,
677
+ npPasswordResetPurposeEnum,
678
+ npUsers,
679
+ npSiteMemberships,
680
+ npUserOAuthIdentities,
681
+ npSessions,
682
+ npRevisions,
683
+ npSettings,
684
+ npSlugHistory,
685
+ npNavigation,
686
+ npStringOverrides,
687
+ npSites,
688
+ npPlugins,
689
+ NP_GLOBAL_PLUGIN_SITE_ID,
690
+ npPluginStorage,
691
+ npWorkerHeartbeats,
692
+ npJobLogs,
693
+ npMediaStatusEnum,
694
+ npMediaFolders,
695
+ npMedia,
696
+ npMediaRefs,
697
+ npMemberStatusEnum,
698
+ npBanScopeEnum,
699
+ npBanKindEnum,
700
+ npCommentStatusEnum,
701
+ npMemberRoleScopeEnum,
702
+ npMembers,
703
+ npMemberSessions,
704
+ npMemberIdentities,
705
+ npMemberRoles,
706
+ npComments,
707
+ npReactions,
708
+ npFollows,
709
+ npMemberMutes,
710
+ npNotifications,
711
+ npReports,
712
+ npAuditEvents,
713
+ npBans
714
+ };
715
+ //# sourceMappingURL=chunk-M43PGOQY.js.map