@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.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/audit-54XLVCWD.js +14 -0
- package/dist/audit-54XLVCWD.js.map +1 -0
- package/dist/auth.d.ts +640 -0
- package/dist/auth.js +94 -0
- package/dist/auth.js.map +1 -0
- package/dist/can-YLUHRJAB.js +19 -0
- package/dist/can-YLUHRJAB.js.map +1 -0
- package/dist/chunk-2G264RCD.js +68 -0
- package/dist/chunk-2G264RCD.js.map +1 -0
- package/dist/chunk-2YDGE7YX.js +92 -0
- package/dist/chunk-2YDGE7YX.js.map +1 -0
- package/dist/chunk-473S4TER.js +538 -0
- package/dist/chunk-473S4TER.js.map +1 -0
- package/dist/chunk-4ZLMEKFX.js +18 -0
- package/dist/chunk-4ZLMEKFX.js.map +1 -0
- package/dist/chunk-55FU6WED.js +179 -0
- package/dist/chunk-55FU6WED.js.map +1 -0
- package/dist/chunk-6YI5K2TI.js +1959 -0
- package/dist/chunk-6YI5K2TI.js.map +1 -0
- package/dist/chunk-BHK3AD3Q.js +41 -0
- package/dist/chunk-BHK3AD3Q.js.map +1 -0
- package/dist/chunk-CRUQBZUF.js +39 -0
- package/dist/chunk-CRUQBZUF.js.map +1 -0
- package/dist/chunk-CTSQ7BRI.js +175 -0
- package/dist/chunk-CTSQ7BRI.js.map +1 -0
- package/dist/chunk-DK2JBJH7.js +81 -0
- package/dist/chunk-DK2JBJH7.js.map +1 -0
- package/dist/chunk-DP2PREDU.js +597 -0
- package/dist/chunk-DP2PREDU.js.map +1 -0
- package/dist/chunk-EQ2Z3KMD.js +24 -0
- package/dist/chunk-EQ2Z3KMD.js.map +1 -0
- package/dist/chunk-FZ7O6DWI.js +305 -0
- package/dist/chunk-FZ7O6DWI.js.map +1 -0
- package/dist/chunk-ISLYFQWL.js +1270 -0
- package/dist/chunk-ISLYFQWL.js.map +1 -0
- package/dist/chunk-JJL74ZPK.js +68 -0
- package/dist/chunk-JJL74ZPK.js.map +1 -0
- package/dist/chunk-JKXAPSU4.js +24 -0
- package/dist/chunk-JKXAPSU4.js.map +1 -0
- package/dist/chunk-KU5M27ZC.js +24 -0
- package/dist/chunk-KU5M27ZC.js.map +1 -0
- package/dist/chunk-LSHHRDVR.js +34 -0
- package/dist/chunk-LSHHRDVR.js.map +1 -0
- package/dist/chunk-M43PGOQY.js +715 -0
- package/dist/chunk-M43PGOQY.js.map +1 -0
- package/dist/chunk-MEJAHXIO.js +150 -0
- package/dist/chunk-MEJAHXIO.js.map +1 -0
- package/dist/chunk-NUCGHWCF.js +101 -0
- package/dist/chunk-NUCGHWCF.js.map +1 -0
- package/dist/chunk-OK5HOCQI.js +845 -0
- package/dist/chunk-OK5HOCQI.js.map +1 -0
- package/dist/chunk-OROPGO65.js +13 -0
- package/dist/chunk-OROPGO65.js.map +1 -0
- package/dist/chunk-PPAS4SZR.js +176 -0
- package/dist/chunk-PPAS4SZR.js.map +1 -0
- package/dist/chunk-PPBWRKO2.js +171 -0
- package/dist/chunk-PPBWRKO2.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-QO7LAQZH.js +321 -0
- package/dist/chunk-QO7LAQZH.js.map +1 -0
- package/dist/chunk-QVJ2HCAX.js +225 -0
- package/dist/chunk-QVJ2HCAX.js.map +1 -0
- package/dist/chunk-RIPHIRPP.js +68 -0
- package/dist/chunk-RIPHIRPP.js.map +1 -0
- package/dist/chunk-S27S42QY.js +134 -0
- package/dist/chunk-S27S42QY.js.map +1 -0
- package/dist/chunk-SBCVAC2Z.js +40 -0
- package/dist/chunk-SBCVAC2Z.js.map +1 -0
- package/dist/chunk-TFJ4MKPH.js +694 -0
- package/dist/chunk-TFJ4MKPH.js.map +1 -0
- package/dist/chunk-THX3SHYA.js +75 -0
- package/dist/chunk-THX3SHYA.js.map +1 -0
- package/dist/chunk-UGQSQO5B.js +222 -0
- package/dist/chunk-UGQSQO5B.js.map +1 -0
- package/dist/chunk-V2UNHGAP.js +26 -0
- package/dist/chunk-V2UNHGAP.js.map +1 -0
- package/dist/chunk-VGTPQXNQ.js +2790 -0
- package/dist/chunk-VGTPQXNQ.js.map +1 -0
- package/dist/chunk-VNIHXQ7W.js +194 -0
- package/dist/chunk-VNIHXQ7W.js.map +1 -0
- package/dist/chunk-WV272MPW.js +31 -0
- package/dist/chunk-WV272MPW.js.map +1 -0
- package/dist/chunk-X5KKBOUS.js +26 -0
- package/dist/chunk-X5KKBOUS.js.map +1 -0
- package/dist/chunk-XANPEOJC.js +17 -0
- package/dist/chunk-XANPEOJC.js.map +1 -0
- package/dist/chunk-XPVQIHAQ.js +83 -0
- package/dist/chunk-XPVQIHAQ.js.map +1 -0
- package/dist/chunk-ZCINJSS4.js +75 -0
- package/dist/chunk-ZCINJSS4.js.map +1 -0
- package/dist/community.d.ts +1425 -0
- package/dist/community.js +206 -0
- package/dist/community.js.map +1 -0
- package/dist/config-2GDU7PCK.js +32 -0
- package/dist/config-2GDU7PCK.js.map +1 -0
- package/dist/context-MNZ4QXPC.js +16 -0
- package/dist/context-MNZ4QXPC.js.map +1 -0
- package/dist/db-schema.d.ts +4 -0
- package/dist/db-schema.js +102 -0
- package/dist/db-schema.js.map +1 -0
- package/dist/db.d.ts +7 -0
- package/dist/db.js +117 -0
- package/dist/db.js.map +1 -0
- package/dist/digest-SY42GQSU.js +17 -0
- package/dist/digest-SY42GQSU.js.map +1 -0
- package/dist/errors-5OS3S2J3.js +22 -0
- package/dist/errors-5OS3S2J3.js.map +1 -0
- package/dist/host-OBOI4MJK.js +51 -0
- package/dist/host-OBOI4MJK.js.map +1 -0
- package/dist/i18n.d.ts +301 -0
- package/dist/i18n.js +68 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index-B6-_vr_m.d.ts +590 -0
- package/dist/index-CY55LC0u.d.ts +4722 -0
- package/dist/index-CeiTvwbp.d.ts +168 -0
- package/dist/index-XwP1ET8b.d.ts +61 -0
- package/dist/index.d.ts +2037 -0
- package/dist/index.js +2205 -0
- package/dist/index.js.map +1 -0
- package/dist/job-log-VZXWQUDK.js +24 -0
- package/dist/job-log-VZXWQUDK.js.map +1 -0
- package/dist/jobs.d.ts +4 -0
- package/dist/jobs.js +76 -0
- package/dist/jobs.js.map +1 -0
- package/dist/logger-DqGaOU_j.d.ts +29 -0
- package/dist/logger-S7REWDNE.js +16 -0
- package/dist/logger-S7REWDNE.js.map +1 -0
- package/dist/media.d.ts +5 -0
- package/dist/media.js +41 -0
- package/dist/media.js.map +1 -0
- package/dist/mentions-2IHFVSHW.js +23 -0
- package/dist/mentions-2IHFVSHW.js.map +1 -0
- package/dist/mutes-EWAE5FZR.js +21 -0
- package/dist/mutes-EWAE5FZR.js.map +1 -0
- package/dist/notification-prefs-VPJDU7I6.js +21 -0
- package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
- package/dist/observability.d.ts +156 -0
- package/dist/observability.js +32 -0
- package/dist/observability.js.map +1 -0
- package/dist/profanity-adapter-NU2JQSLX.js +12 -0
- package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
- package/dist/queue-XE5BC75T.js +14 -0
- package/dist/queue-XE5BC75T.js.map +1 -0
- package/dist/rate-limit.d.ts +99 -0
- package/dist/rate-limit.js +14 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/registry-XIXDEPVI.js +31 -0
- package/dist/registry-XIXDEPVI.js.map +1 -0
- package/dist/reputation-JRL2YQHM.js +11 -0
- package/dist/reputation-JRL2YQHM.js.map +1 -0
- package/dist/routes.d.ts +43 -0
- package/dist/routes.js +12 -0
- package/dist/routes.js.map +1 -0
- package/dist/scheduled-CIQM57HT.js +20 -0
- package/dist/scheduled-CIQM57HT.js.map +1 -0
- package/dist/seo.d.ts +410 -0
- package/dist/seo.js +44 -0
- package/dist/seo.js.map +1 -0
- package/dist/settings-FOBIESPB.js +17 -0
- package/dist/settings-FOBIESPB.js.map +1 -0
- package/dist/spam-adapter-XX3G737Z.js +12 -0
- package/dist/spam-adapter-XX3G737Z.js.map +1 -0
- package/dist/strings-VAE47B2C.js +29 -0
- package/dist/strings-VAE47B2C.js.map +1 -0
- package/dist/templates-IFVJMCJ6.js +12 -0
- package/dist/templates-IFVJMCJ6.js.map +1 -0
- package/dist/types-TlsbXS0T.d.ts +871 -0
- package/package.json +129 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCurrentSiteId
|
|
3
|
+
} from "./chunk-SBCVAC2Z.js";
|
|
4
|
+
import {
|
|
5
|
+
getLogger
|
|
6
|
+
} from "./chunk-JJL74ZPK.js";
|
|
7
|
+
import {
|
|
8
|
+
getDb
|
|
9
|
+
} from "./chunk-XANPEOJC.js";
|
|
10
|
+
import {
|
|
11
|
+
npAuditEvents
|
|
12
|
+
} from "./chunk-M43PGOQY.js";
|
|
13
|
+
|
|
14
|
+
// src/community/audit.ts
|
|
15
|
+
import { and, count, desc, eq, gte, lt } from "drizzle-orm";
|
|
16
|
+
async function recordAuditEvent(input) {
|
|
17
|
+
const db = getDb();
|
|
18
|
+
try {
|
|
19
|
+
const siteId = input.siteId === void 0 ? await getCurrentSiteId() : input.siteId;
|
|
20
|
+
await db.insert(npAuditEvents).values({
|
|
21
|
+
actorKind: input.actor.kind,
|
|
22
|
+
actorUserId: input.actor.userId ?? null,
|
|
23
|
+
actorMemberId: input.actor.memberId ?? null,
|
|
24
|
+
action: input.action,
|
|
25
|
+
targetType: input.targetType ?? null,
|
|
26
|
+
targetId: input.targetId ?? null,
|
|
27
|
+
payload: input.payload ?? {},
|
|
28
|
+
siteId
|
|
29
|
+
});
|
|
30
|
+
} catch (err) {
|
|
31
|
+
getLogger().error("audit insert failed", {
|
|
32
|
+
error: err instanceof Error ? err.message : String(err),
|
|
33
|
+
action: input.action,
|
|
34
|
+
targetType: input.targetType ?? null,
|
|
35
|
+
targetId: input.targetId ?? null
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function listAuditEvents(options = {}) {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);
|
|
42
|
+
const offset = Math.max(options.offset ?? 0, 0);
|
|
43
|
+
const filters = [];
|
|
44
|
+
if (options.targetType) filters.push(eq(npAuditEvents.targetType, options.targetType));
|
|
45
|
+
if (options.targetId) filters.push(eq(npAuditEvents.targetId, options.targetId));
|
|
46
|
+
if (options.actorUserId) filters.push(eq(npAuditEvents.actorUserId, options.actorUserId));
|
|
47
|
+
if (options.actorMemberId) filters.push(eq(npAuditEvents.actorMemberId, options.actorMemberId));
|
|
48
|
+
if (options.action) filters.push(eq(npAuditEvents.action, options.action));
|
|
49
|
+
if (options.since) filters.push(gte(npAuditEvents.createdAt, options.since));
|
|
50
|
+
if (options.until) filters.push(lt(npAuditEvents.createdAt, options.until));
|
|
51
|
+
if (options.siteId !== null) {
|
|
52
|
+
const resolvedSite = options.siteId !== void 0 ? options.siteId : await getCurrentSiteId();
|
|
53
|
+
if (resolvedSite !== null) {
|
|
54
|
+
filters.push(eq(npAuditEvents.siteId, resolvedSite));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const where = filters.length > 0 ? and(...filters) : void 0;
|
|
58
|
+
const rows = await db.select().from(npAuditEvents).where(where).orderBy(desc(npAuditEvents.createdAt)).limit(limit).offset(offset);
|
|
59
|
+
const [totalRow] = await db.select({ total: count() }).from(npAuditEvents).where(where);
|
|
60
|
+
const totalDocs = Number(totalRow?.total ?? 0);
|
|
61
|
+
return { events: rows, totalDocs };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
recordAuditEvent,
|
|
66
|
+
listAuditEvents
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=chunk-RIPHIRPP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/community/audit.ts"],"sourcesContent":["import { and, count, desc, eq, gte, lt } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npAuditEvents } from \"../db/schema/community.js\";\nimport { getLogger } from \"../observability/logger.js\";\nimport { getCurrentSiteId } from \"../sites/context.js\";\n\n/**\n * Append-only moderation audit log. Every hide / restore / ban /\n * role-grant write goes through here so admins can later answer\n * \"who took this action and when?\" without diffing application logs.\n *\n * Writes are best-effort: a failed audit insert MUST NOT prevent the\n * underlying mod action from succeeding (logged via the observability\n * hooks instead). Reads are paginated and indexed by target.\n */\n\nexport type AuditActorKind = \"staff\" | \"member\" | \"system\";\n\nexport interface AuditActor {\n kind: AuditActorKind;\n /** Set only for `kind: \"staff\"`. */\n userId?: string;\n /** Set only for `kind: \"member\"`. */\n memberId?: string;\n}\n\nexport interface RecordAuditEventInput {\n actor: AuditActor;\n action: string;\n targetType?: string;\n targetId?: string;\n payload?: Record<string, unknown>;\n /**\n * Phase 17 — site this event belongs to. When omitted the\n * writer reads `getCurrentSiteId()` so request-driven calls\n * automatically scope to the resolving tenant. Pass `null`\n * explicitly to record an unscoped event (super-admin\n * cross-site action, background job).\n */\n siteId?: string | null;\n}\n\nexport interface AuditEventRow {\n id: string;\n actorKind: AuditActorKind;\n actorUserId: string | null;\n actorMemberId: string | null;\n action: string;\n targetType: string | null;\n targetId: string | null;\n payload: Record<string, unknown>;\n siteId: string | null;\n createdAt: Date;\n}\n\nexport async function recordAuditEvent(input: RecordAuditEventInput): Promise<void> {\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n try {\n // Phase 17 — fill `site_id` from the request resolver when\n // the caller doesn't pin it explicitly. Resolver returns\n // null in non-request contexts (jobs, scripts), which we\n // record as a NULL site so super-admin queries can find\n // them via \"no site filter.\"\n const siteId = input.siteId === undefined ? await getCurrentSiteId() : input.siteId;\n await db.insert(npAuditEvents).values({\n actorKind: input.actor.kind,\n actorUserId: input.actor.userId ?? null,\n actorMemberId: input.actor.memberId ?? null,\n action: input.action,\n targetType: input.targetType ?? null,\n targetId: input.targetId ?? null,\n payload: input.payload ?? {},\n siteId,\n });\n } catch (err) {\n // Audit failures must not block the underlying mod action — but\n // they MUST surface, otherwise gaps in the forensic record go\n // unnoticed (column drift, FK violation, transient pg blip).\n getLogger().error(\"audit insert failed\", {\n error: err instanceof Error ? err.message : String(err),\n action: input.action,\n targetType: input.targetType ?? null,\n targetId: input.targetId ?? null,\n });\n }\n}\n\nexport interface ListAuditOptions {\n /** Filter to audit events targeting one specific row. */\n targetType?: string;\n targetId?: string;\n /** Filter to events caused by a specific actor. */\n actorUserId?: string;\n actorMemberId?: string;\n /**\n * Filter to events whose `action` matches. Common operational\n * query: \"show every ban issued this week\" →\n * `action=\"member.ban.issue\"` plus `since`.\n */\n action?: string;\n /** Lower-bound `created_at` (inclusive). */\n since?: Date;\n /** Upper-bound `created_at` (exclusive). */\n until?: Date;\n /**\n * Phase 17 — site filter. `undefined` means \"use current\n * request's site\" (the typical admin-page query). Pass an\n * explicit string to view another site's audit log\n * (super-admin cross-site triage). Pass `null` to skip the\n * filter entirely (every site's events).\n */\n siteId?: string | null;\n limit?: number;\n offset?: number;\n}\n\nexport async function listAuditEvents(\n options: ListAuditOptions = {},\n): Promise<{ events: AuditEventRow[]; totalDocs: number }> {\n const db = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);\n const offset = Math.max(options.offset ?? 0, 0);\n\n const filters = [];\n if (options.targetType) filters.push(eq(npAuditEvents.targetType, options.targetType));\n if (options.targetId) filters.push(eq(npAuditEvents.targetId, options.targetId));\n if (options.actorUserId) filters.push(eq(npAuditEvents.actorUserId, options.actorUserId));\n if (options.actorMemberId) filters.push(eq(npAuditEvents.actorMemberId, options.actorMemberId));\n if (options.action) filters.push(eq(npAuditEvents.action, options.action));\n if (options.since) filters.push(gte(npAuditEvents.createdAt, options.since));\n if (options.until) filters.push(lt(npAuditEvents.createdAt, options.until));\n\n // Phase 17 — site scope.\n // `undefined` (default) → use the resolver's current site if\n // any. Pass `null` to skip filtering\n // (cross-site, super-admin).\n if (options.siteId !== null) {\n const resolvedSite = options.siteId !== undefined ? options.siteId : await getCurrentSiteId();\n if (resolvedSite !== null) {\n filters.push(eq(npAuditEvents.siteId, resolvedSite));\n }\n }\n\n const where = filters.length > 0 ? and(...filters) : undefined;\n\n const rows = (await db\n .select()\n .from(npAuditEvents)\n .where(where)\n .orderBy(desc(npAuditEvents.createdAt))\n .limit(limit)\n .offset(offset)) as AuditEventRow[];\n\n const [totalRow] = (await db\n .select({ total: count() })\n .from(npAuditEvents)\n .where(where)) as Array<{ total: number }>;\n const totalDocs = Number(totalRow?.total ?? 0);\n return { events: rows, totalDocs };\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,KAAK,OAAO,MAAM,IAAI,KAAK,UAAU;AAyD9C,eAAsB,iBAAiB,OAA6C;AAClF,QAAM,KAAK,MAAM;AACjB,MAAI;AAMF,UAAM,SAAS,MAAM,WAAW,SAAY,MAAM,iBAAiB,IAAI,MAAM;AAC7E,UAAM,GAAG,OAAO,aAAa,EAAE,OAAO;AAAA,MACpC,WAAW,MAAM,MAAM;AAAA,MACvB,aAAa,MAAM,MAAM,UAAU;AAAA,MACnC,eAAe,MAAM,MAAM,YAAY;AAAA,MACvC,QAAQ,MAAM;AAAA,MACd,YAAY,MAAM,cAAc;AAAA,MAChC,UAAU,MAAM,YAAY;AAAA,MAC5B,SAAS,MAAM,WAAW,CAAC;AAAA,MAC3B;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AAIZ,cAAU,EAAE,MAAM,uBAAuB;AAAA,MACvC,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MACtD,QAAQ,MAAM;AAAA,MACd,YAAY,MAAM,cAAc;AAAA,MAChC,UAAU,MAAM,YAAY;AAAA,IAC9B,CAAC;AAAA,EACH;AACF;AA+BA,eAAsB,gBACpB,UAA4B,CAAC,GAC4B;AACzD,QAAM,KAAK,MAAM;AACjB,QAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,QAAQ,SAAS,IAAI,CAAC,GAAG,GAAG;AAC5D,QAAM,SAAS,KAAK,IAAI,QAAQ,UAAU,GAAG,CAAC;AAE9C,QAAM,UAAU,CAAC;AACjB,MAAI,QAAQ,WAAY,SAAQ,KAAK,GAAG,cAAc,YAAY,QAAQ,UAAU,CAAC;AACrF,MAAI,QAAQ,SAAU,SAAQ,KAAK,GAAG,cAAc,UAAU,QAAQ,QAAQ,CAAC;AAC/E,MAAI,QAAQ,YAAa,SAAQ,KAAK,GAAG,cAAc,aAAa,QAAQ,WAAW,CAAC;AACxF,MAAI,QAAQ,cAAe,SAAQ,KAAK,GAAG,cAAc,eAAe,QAAQ,aAAa,CAAC;AAC9F,MAAI,QAAQ,OAAQ,SAAQ,KAAK,GAAG,cAAc,QAAQ,QAAQ,MAAM,CAAC;AACzE,MAAI,QAAQ,MAAO,SAAQ,KAAK,IAAI,cAAc,WAAW,QAAQ,KAAK,CAAC;AAC3E,MAAI,QAAQ,MAAO,SAAQ,KAAK,GAAG,cAAc,WAAW,QAAQ,KAAK,CAAC;AAM1E,MAAI,QAAQ,WAAW,MAAM;AAC3B,UAAM,eAAe,QAAQ,WAAW,SAAY,QAAQ,SAAS,MAAM,iBAAiB;AAC5F,QAAI,iBAAiB,MAAM;AACzB,cAAQ,KAAK,GAAG,cAAc,QAAQ,YAAY,CAAC;AAAA,IACrD;AAAA,EACF;AAEA,QAAM,QAAQ,QAAQ,SAAS,IAAI,IAAI,GAAG,OAAO,IAAI;AAErD,QAAM,OAAQ,MAAM,GACjB,OAAO,EACP,KAAK,aAAa,EAClB,MAAM,KAAK,EACX,QAAQ,KAAK,cAAc,SAAS,CAAC,EACrC,MAAM,KAAK,EACX,OAAO,MAAM;AAEhB,QAAM,CAAC,QAAQ,IAAK,MAAM,GACvB,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC,EACzB,KAAK,aAAa,EAClB,MAAM,KAAK;AACd,QAAM,YAAY,OAAO,UAAU,SAAS,CAAC;AAC7C,SAAO,EAAE,QAAQ,MAAM,UAAU;AACnC;","names":[]}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
npMediaRefs
|
|
3
|
+
} from "./chunk-M43PGOQY.js";
|
|
4
|
+
|
|
5
|
+
// src/media/refs.ts
|
|
6
|
+
import { and, eq } from "drizzle-orm";
|
|
7
|
+
function extractMediaIds(fields, data) {
|
|
8
|
+
const refs = [];
|
|
9
|
+
collectMediaIds(fields, data, refs, []);
|
|
10
|
+
return refs;
|
|
11
|
+
}
|
|
12
|
+
async function syncMediaRefs(tx, collection, documentId, refs) {
|
|
13
|
+
await tx.delete(npMediaRefs).where(
|
|
14
|
+
and(eq(npMediaRefs.collection, collection), eq(npMediaRefs.documentId, documentId))
|
|
15
|
+
);
|
|
16
|
+
const uniqueRefs = dedupeRefs(refs);
|
|
17
|
+
if (uniqueRefs.length === 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await tx.insert(npMediaRefs).values(
|
|
21
|
+
uniqueRefs.map((ref) => ({
|
|
22
|
+
mediaId: ref.mediaId,
|
|
23
|
+
collection,
|
|
24
|
+
documentId,
|
|
25
|
+
field: ref.field
|
|
26
|
+
}))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
function collectMediaIds(fields, data, refs, prefix) {
|
|
30
|
+
for (const field of fields) {
|
|
31
|
+
if (field.type === "row" || field.type === "collapsible") {
|
|
32
|
+
collectMediaIds(field.fields, data, refs, prefix);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const fieldPath = [...prefix, field.name];
|
|
36
|
+
const fieldKey = fieldPath.join(".");
|
|
37
|
+
const value = data[field.name];
|
|
38
|
+
if (field.type === "upload") {
|
|
39
|
+
const mediaId = getMediaId(value);
|
|
40
|
+
if (mediaId) {
|
|
41
|
+
refs.push({ mediaId, field: fieldKey });
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (field.type === "richText") {
|
|
46
|
+
collectRichTextMediaIds(value, fieldKey, refs);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (field.type === "array") {
|
|
50
|
+
if (!Array.isArray(value)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const item of value) {
|
|
54
|
+
const itemRecord = toOptionalRecord(item);
|
|
55
|
+
if (itemRecord) {
|
|
56
|
+
collectMediaIds(field.fields, itemRecord, refs, fieldPath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (field.type === "group") {
|
|
62
|
+
const groupRecord = toOptionalRecord(value);
|
|
63
|
+
if (groupRecord) {
|
|
64
|
+
collectMediaIds(field.fields, groupRecord, refs, fieldPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function collectRichTextMediaIds(value, field, refs) {
|
|
70
|
+
walkRichTextValue(value, (node) => {
|
|
71
|
+
if (node.type !== "image") {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const mediaId = getMediaId(node.mediaId) ?? getMediaId(node.value);
|
|
75
|
+
if (mediaId) {
|
|
76
|
+
refs.push({ mediaId, field });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function walkRichTextValue(value, visit) {
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
for (const item of value) {
|
|
83
|
+
walkRichTextValue(item, visit);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const record = toOptionalRecord(value);
|
|
88
|
+
if (!record) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
visit(record);
|
|
92
|
+
for (const child of Object.values(record)) {
|
|
93
|
+
walkRichTextValue(child, visit);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function getMediaId(value) {
|
|
97
|
+
if (typeof value === "string" && value.length > 0) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
const record = toOptionalRecord(value);
|
|
101
|
+
if (!record) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (typeof record.mediaId === "string" && record.mediaId.length > 0) {
|
|
105
|
+
return record.mediaId;
|
|
106
|
+
}
|
|
107
|
+
if (typeof record.id === "string" && record.id.length > 0) {
|
|
108
|
+
return record.id;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function dedupeRefs(refs) {
|
|
113
|
+
const seen = /* @__PURE__ */ new Set();
|
|
114
|
+
return refs.filter((ref) => {
|
|
115
|
+
const key = `${ref.mediaId}:${ref.field}`;
|
|
116
|
+
if (seen.has(key)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
seen.add(key);
|
|
120
|
+
return true;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function toOptionalRecord(value) {
|
|
124
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
extractMediaIds,
|
|
132
|
+
syncMediaRefs
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=chunk-S27S42QY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/media/refs.ts"],"sourcesContent":["import { and, eq } from \"drizzle-orm\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\n\nimport type { NpFieldConfig } from \"../config/types.js\";\nimport { npMediaRefs } from \"../db/schema/media.js\";\n\ninterface InsertValuesQuery extends Promise<unknown> {\n returning(): Promise<unknown[]>;\n}\n\ninterface SelectQuery extends Promise<unknown[]> {\n where(condition: ReturnType<typeof and>): SelectQuery;\n}\n\nexport interface DrizzleTransactionLike {\n insert(table: PgTable): {\n values(values: Record<string, unknown> | Record<string, unknown>[]): InsertValuesQuery;\n };\n delete(table: PgTable): {\n where(condition: ReturnType<typeof and>): Promise<unknown>;\n };\n select(selection?: Record<string, unknown>): {\n from(table: PgTable): SelectQuery;\n };\n}\n\nexport function extractMediaIds(\n fields: NpFieldConfig[],\n data: Record<string, unknown>,\n): Array<{ mediaId: string; field: string }> {\n const refs: Array<{ mediaId: string; field: string }> = [];\n\n collectMediaIds(fields, data, refs, []);\n\n return refs;\n}\n\nexport async function syncMediaRefs(\n tx: DrizzleTransactionLike,\n collection: string,\n documentId: string,\n refs: Array<{ mediaId: string; field: string }>,\n): Promise<void> {\n await tx.delete(npMediaRefs).where(\n and(eq(npMediaRefs.collection, collection), eq(npMediaRefs.documentId, documentId)),\n );\n\n const uniqueRefs = dedupeRefs(refs);\n\n if (uniqueRefs.length === 0) {\n return;\n }\n\n await tx.insert(npMediaRefs).values(\n uniqueRefs.map((ref) => ({\n mediaId: ref.mediaId,\n collection,\n documentId,\n field: ref.field,\n })),\n );\n}\n\nfunction collectMediaIds(\n fields: NpFieldConfig[],\n data: Record<string, unknown>,\n refs: Array<{ mediaId: string; field: string }>,\n prefix: string[],\n): void {\n for (const field of fields) {\n if (field.type === \"row\" || field.type === \"collapsible\") {\n collectMediaIds(field.fields, data, refs, prefix);\n continue;\n }\n\n const fieldPath = [...prefix, field.name];\n const fieldKey = fieldPath.join(\".\");\n const value = data[field.name];\n\n if (field.type === \"upload\") {\n const mediaId = getMediaId(value);\n\n if (mediaId) {\n refs.push({ mediaId, field: fieldKey });\n }\n\n continue;\n }\n\n if (field.type === \"richText\") {\n collectRichTextMediaIds(value, fieldKey, refs);\n continue;\n }\n\n if (field.type === \"array\") {\n if (!Array.isArray(value)) {\n continue;\n }\n\n for (const item of value) {\n const itemRecord = toOptionalRecord(item);\n\n if (itemRecord) {\n collectMediaIds(field.fields, itemRecord, refs, fieldPath);\n }\n }\n\n continue;\n }\n\n if (field.type === \"group\") {\n const groupRecord = toOptionalRecord(value);\n\n if (groupRecord) {\n collectMediaIds(field.fields, groupRecord, refs, fieldPath);\n }\n }\n }\n}\n\nfunction collectRichTextMediaIds(\n value: unknown,\n field: string,\n refs: Array<{ mediaId: string; field: string }>,\n): void {\n walkRichTextValue(value, (node) => {\n if (node.type !== \"image\") {\n return;\n }\n\n const mediaId = getMediaId(node.mediaId) ?? getMediaId(node.value);\n\n if (mediaId) {\n refs.push({ mediaId, field });\n }\n });\n}\n\nfunction walkRichTextValue(\n value: unknown,\n visit: (node: Record<string, unknown>) => void,\n): void {\n if (Array.isArray(value)) {\n for (const item of value) {\n walkRichTextValue(item, visit);\n }\n\n return;\n }\n\n const record = toOptionalRecord(value);\n\n if (!record) {\n return;\n }\n\n visit(record);\n\n for (const child of Object.values(record)) {\n walkRichTextValue(child, visit);\n }\n}\n\nfunction getMediaId(value: unknown): string | null {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n\n const record = toOptionalRecord(value);\n\n if (!record) {\n return null;\n }\n\n if (typeof record.mediaId === \"string\" && record.mediaId.length > 0) {\n return record.mediaId;\n }\n\n if (typeof record.id === \"string\" && record.id.length > 0) {\n return record.id;\n }\n\n return null;\n}\n\nfunction dedupeRefs(\n refs: Array<{ mediaId: string; field: string }>,\n): Array<{ mediaId: string; field: string }> {\n const seen = new Set<string>();\n\n return refs.filter((ref) => {\n const key = `${ref.mediaId}:${ref.field}`;\n\n if (seen.has(key)) {\n return false;\n }\n\n seen.add(key);\n return true;\n });\n}\n\nfunction toOptionalRecord(value: unknown): Record<string, unknown> | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n\n return value as Record<string, unknown>;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/sites/context.ts
|
|
2
|
+
var resolver = null;
|
|
3
|
+
function setCurrentSiteResolver(fn) {
|
|
4
|
+
resolver = fn;
|
|
5
|
+
}
|
|
6
|
+
function resetCurrentSiteResolver() {
|
|
7
|
+
resolver = null;
|
|
8
|
+
}
|
|
9
|
+
async function getCurrentSiteId() {
|
|
10
|
+
if (!resolver) return null;
|
|
11
|
+
return resolver();
|
|
12
|
+
}
|
|
13
|
+
async function requireSiteId() {
|
|
14
|
+
const id = await getCurrentSiteId();
|
|
15
|
+
if (!id) {
|
|
16
|
+
const { NpSiteContextMissingError } = await import("./errors-5OS3S2J3.js");
|
|
17
|
+
throw new NpSiteContextMissingError(
|
|
18
|
+
"site context required for this write but none is set \u2014 wrap the call in withCurrentSite() or stamp siteId on the job payload"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
async function withCurrentSite(siteId, fn) {
|
|
24
|
+
const previous = resolver;
|
|
25
|
+
resolver = () => siteId;
|
|
26
|
+
try {
|
|
27
|
+
return await fn();
|
|
28
|
+
} finally {
|
|
29
|
+
resolver = previous;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
setCurrentSiteResolver,
|
|
35
|
+
resetCurrentSiteResolver,
|
|
36
|
+
getCurrentSiteId,
|
|
37
|
+
requireSiteId,
|
|
38
|
+
withCurrentSite
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=chunk-SBCVAC2Z.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sites/context.ts"],"sourcesContent":["/**\n * Phase 15.1 — process-wide \"current site\" resolver hook.\n *\n * The pipeline doesn't know how to find the current request's\n * site on its own (the runtime layer does — it reads the\n * `x-np-site-id` header the middleware sets). This module\n * exposes a setter the runtime calls at boot:\n *\n * setCurrentSiteResolver(async () => {\n * const headerList = await headers();\n * return headerList.get(\"x-np-site-id\") ?? null;\n * });\n *\n * Pipeline / hooks call `getCurrentSiteId()` to read the\n * resolved id (or `null` when no resolver is wired, e.g.\n * background workers, scripts).\n *\n * This is intentionally async — the canonical Next.js\n * resolver awaits `headers()`. Sync paths (CLI, tests) can\n * register a sync resolver by returning the value directly.\n */\n\ntype Resolver = () => string | null | Promise<string | null>;\n\nlet resolver: Resolver | null = null;\n\nexport function setCurrentSiteResolver(fn: Resolver | null): void {\n resolver = fn;\n}\n\nexport function resetCurrentSiteResolver(): void {\n resolver = null;\n}\n\nexport async function getCurrentSiteId(): Promise<string | null> {\n if (!resolver) return null;\n return resolver();\n}\n\n/**\n * Like `getCurrentSiteId()` but throws when no site context is set.\n *\n * Use this on write paths that must NEVER silently fall through to\n * the default site — community moderation, ban/mute writes, report\n * creation, notification fan-out. Reading from the default site\n * when context is missing is usually fine; *writing* to it is how\n * cross-site data leaks happen.\n *\n * Background jobs / CLI scripts: stamp the originating `siteId`\n * onto the job payload at enqueue time and wrap the handler in\n * `withCurrentSite(siteId, fn)` so this helper resolves correctly.\n *\n * Throws `NpSiteContextMissingError` (code `SITE_CONTEXT_MISSING`,\n * status 500). The 500 is deliberate — this is a server-side\n * wiring bug, not user input fault, and the API layer surfaces\n * it through the standard NpError envelope.\n */\nexport async function requireSiteId(): Promise<string> {\n const id = await getCurrentSiteId();\n if (!id) {\n // Defer the import to keep this module's load graph thin —\n // `errors.js` doesn't currently reach back into sites/, but\n // the dynamic specifier costs nothing on the happy path\n // (resolver hit) and avoids a future cycle.\n const { NpSiteContextMissingError } = await import(\"../errors.js\");\n throw new NpSiteContextMissingError(\n \"site context required for this write but none is set — \" +\n \"wrap the call in withCurrentSite() or stamp siteId on the job payload\",\n );\n }\n return id;\n}\n\n/**\n * Tests / scripts that want to pin the current site id for the\n * duration of a block use the `withCurrentSite` helper — it swaps\n * in a constant resolver, runs `fn`, and restores the previous\n * resolver on exit.\n *\n * Contract — read this carefully (#320):\n *\n * `withCurrentSite` covers ONLY work that completes (synchronously\n * or via `await`) before `fn` returns. Any fire-and-forget async\n * work spawned inside `fn` runs AFTER the `finally` block has\n * already restored the previous resolver, so it sees the OUTER\n * site context — typically `null` for a CLI / job, or the wrong\n * site for a request that was acting on a different tenant.\n *\n * Concretely:\n * - `enqueueJob(...)` persists the row immediately but the\n * handler runs later in the worker. The worker has no\n * resolver wired, so `getCurrentSiteId()` returns `null`\n * and `requireSiteId()` throws — even though the enqueuer\n * was inside a `withCurrentSite` block.\n * - `void someAsyncFn()` patterns inside `fn` are similarly\n * exposed.\n *\n * How to do it safely:\n * - Stamp `siteId` explicitly onto every job payload at\n * enqueue time. The handler reads it back from the payload\n * and wraps its own work in `withCurrentSite(payload.siteId,\n * handlerBody)`.\n * - `await` everything that needs the site context inside\n * `fn`. Don't return from `fn` while a site-dependent\n * operation is still pending.\n *\n * This is a fundamental limit of plain module-scoped state. A\n * future refactor could switch the resolver to\n * `AsyncLocalStorage` so the site follows the async boundary\n * automatically — that's tracked under #320 but out of scope\n * for this helper today.\n */\nexport async function withCurrentSite<T>(\n siteId: string,\n fn: () => Promise<T>,\n): Promise<T> {\n const previous = resolver;\n resolver = () => siteId;\n try {\n return await fn();\n } finally {\n resolver = previous;\n }\n}\n"],"mappings":";AAwBA,IAAI,WAA4B;AAEzB,SAAS,uBAAuB,IAA2B;AAChE,aAAW;AACb;AAEO,SAAS,2BAAiC;AAC/C,aAAW;AACb;AAEA,eAAsB,mBAA2C;AAC/D,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS;AAClB;AAoBA,eAAsB,gBAAiC;AACrD,QAAM,KAAK,MAAM,iBAAiB;AAClC,MAAI,CAAC,IAAI;AAKP,UAAM,EAAE,0BAA0B,IAAI,MAAM,OAAO,sBAAc;AACjE,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO;AACT;AAyCA,eAAsB,gBACpB,QACA,IACY;AACZ,QAAM,WAAW;AACjB,aAAW,MAAM;AACjB,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA,eAAW;AAAA,EACb;AACF;","names":[]}
|