feat(platform): Phase 116 — audit CSV sign, CSV mesh push, SMTP prefs (v0.8.80)

- exportAuditLog signs CSV; getAuditExportCsvExport; GUILD_SYNC slice
- pushHookFailureDigestCsvToMesh + _onHookFailureDigestCsvGossip
- SMTP relay prefs in UI API; _syncDigestRelaySmtpConfigToPlugin
- relay handoff opts include smtp manifest fields

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-22 07:19:12 -04:00
co-authored by Cursor
parent 9e5390de96
commit 874472732c
+162 -8
View File
@@ -47,6 +47,8 @@ const {
formatHookFailureDigestExport, formatHookFailureDigestExport,
signHookFailureDigestCsv, signHookFailureDigestCsv,
verifyHookFailureDigestCsv, verifyHookFailureDigestCsv,
signAuditExportCsv,
verifyAuditExportCsv,
formatAutomationScheduleDigestExport, formatAutomationScheduleDigestExport,
mergeHookFailureDigests, mergeHookFailureDigests,
buildAutomationScheduleDashboard, buildAutomationScheduleDashboard,
@@ -1201,6 +1203,7 @@ class PearcordPlatform extends EventEmitter {
let archivePeerSeen = [] let archivePeerSeen = []
let hookFailureDigest = null let hookFailureDigest = null
let hookFailureDigestCsvExport = null let hookFailureDigestCsvExport = null
let auditExportCsvExport = null
let archiveHealthPrefs = null let archiveHealthPrefs = null
await this._initDeliveryReceipts(guild.id) await this._initDeliveryReceipts(guild.id)
if (this.deliveryReceipts) { if (this.deliveryReceipts) {
@@ -1215,6 +1218,7 @@ class PearcordPlatform extends EventEmitter {
archiveHealthPrefs = await this.deliveryReceipts.exportArchiveHealthPrefsSlice() archiveHealthPrefs = await this.deliveryReceipts.exportArchiveHealthPrefsSlice()
hookFailureDigestCsvExport = hookFailureDigestCsvExport =
await this.deliveryReceipts.exportHookFailureDigestCsvSlice() await this.deliveryReceipts.exportHookFailureDigestCsvSlice()
auditExportCsvExport = await this.deliveryReceipts.exportAuditExportCsvSlice()
digestRelayHandoff = await this.deliveryReceipts.exportDigestRelayHandoffSlice() digestRelayHandoff = await this.deliveryReceipts.exportDigestRelayHandoffSlice()
const digestSnap = await this.deliveryReceipts.getHookFailureDigestSnapshot() const digestSnap = await this.deliveryReceipts.getHookFailureDigestSnapshot()
if (digestSnap) { if (digestSnap) {
@@ -1300,6 +1304,7 @@ class PearcordPlatform extends EventEmitter {
archivePeerSeen, archivePeerSeen,
hookFailureDigest, hookFailureDigest,
hookFailureDigestCsvExport, hookFailureDigestCsvExport,
auditExportCsvExport,
archiveHealthPrefs, archiveHealthPrefs,
modDigestGroupDmMeta, modDigestGroupDmMeta,
workerReleaseHint, workerReleaseHint,
@@ -1442,6 +1447,13 @@ class PearcordPlatform extends EventEmitter {
payload.hookFailureDigestCsvExport payload.hookFailureDigestCsvExport
) )
} }
if (payload.auditExportCsvExport) {
await this._initDeliveryReceipts(payload.guildId)
await this.deliveryReceipts.ingestAuditExportCsvSlice(
payload.guildId,
payload.auditExportCsvExport
)
}
if (payload.archiveHealthPrefs) { if (payload.archiveHealthPrefs) {
await this._initDeliveryReceipts(payload.guildId) await this._initDeliveryReceipts(payload.guildId)
await this.deliveryReceipts.ingestArchiveHealthPrefsSlice( await this.deliveryReceipts.ingestArchiveHealthPrefsSlice(
@@ -1762,6 +1774,9 @@ class PearcordPlatform extends EventEmitter {
guildInstance.on('hook-failure-digest-sync', (payload) => { guildInstance.on('hook-failure-digest-sync', (payload) => {
this._onHookFailureDigestGossip(payload).catch(() => {}) this._onHookFailureDigestGossip(payload).catch(() => {})
}) })
guildInstance.on('hook-failure-digest-csv-sync', (payload) => {
this._onHookFailureDigestCsvGossip(payload).catch(() => {})
})
guildInstance.on('audit-export-archive', (payload) => { guildInstance.on('audit-export-archive', (payload) => {
this._onAuditExportArchiveGossip(payload).catch(() => {}) this._onAuditExportArchiveGossip(payload).catch(() => {})
}) })
@@ -2109,7 +2124,33 @@ class PearcordPlatform extends EventEmitter {
const format = opts.format === 'csv' ? 'csv' : 'json' const format = opts.format === 'csv' ? 'csv' : 'json'
const limit = Math.min(500, Math.max(1, Number(opts.limit) || 200)) const limit = Math.min(500, Math.max(1, Number(opts.limit) || 200))
const entries = await this.listAuditLog(limit, { filter: opts.filter }) const entries = await this.listAuditLog(limit, { filter: opts.filter })
const body = formatAuditExport(entries, format) const guildId = this.guild.guild.id
let body = formatAuditExport(entries, format)
let signature = null
let signatureAlg = null
let signatureValid = null
if (format === 'csv' && opts.sign !== false) {
const signed = signAuditExportCsv(body, { guildId, relaySecret: guildId })
signature = signed.signature
signatureAlg = signed.signatureAlg
const verified = verifyAuditExportCsv(signed.csv, signature, {
guildId,
relaySecret: guildId
})
signatureValid = verified.ok
body = signed.csv
await this._initDeliveryReceipts(guildId)
await this.deliveryReceipts.recordAuditExportCsvExport({
csv: body,
signature,
signatureAlg,
filter: opts.filter || 'all',
exportedAt: Date.now()
})
if (opts.recordMesh !== false) {
setTimeout(() => this._pushGuildSyncToMesh().catch(() => {}), 400)
}
}
const exportedAt = Date.now() const exportedAt = Date.now()
const entryRows = entries.map((e) => ({ const entryRows = entries.map((e) => ({
id: e.id, id: e.id,
@@ -2145,11 +2186,26 @@ class PearcordPlatform extends EventEmitter {
format, format,
count: entries.length, count: entries.length,
body, body,
guildId: this.guild.guild.id, guildId,
exportedAt exportedAt,
signature,
signatureAlg,
signatureValid
} }
} }
async getAuditExportCsvExport () {
if (!this.guild?.guild) return null
await this._initDeliveryReceipts(this.guild.guild.id)
const row = await this.deliveryReceipts.getAuditExportCsvExport()
if (!row?.signature) return row
const verified = verifyAuditExportCsv(row.csvBody, row.signature, {
guildId: this.guild.guild.id,
relaySecret: this.guild.guild.id
})
return { ...row, signatureValid: verified.ok }
}
async _onAuditExportSnapshotGossip (payload) { async _onAuditExportSnapshotGossip (payload) {
if (!payload?.guildId || !Array.isArray(payload.entries)) return null if (!payload?.guildId || !Array.isArray(payload.entries)) return null
if (this.guild?.guild?.id !== payload.guildId) return null if (this.guild?.guild?.id !== payload.guildId) return null
@@ -2748,7 +2804,11 @@ class PearcordPlatform extends EventEmitter {
muteNotifications: false, muteNotifications: false,
digestInboxChannelId: null, digestInboxChannelId: null,
digestRelayEnabled: false, digestRelayEnabled: false,
digestRelayWebhookUrl: null digestRelayWebhookUrl: null,
digestRelaySmtpHost: null,
digestRelaySmtpPort: null,
digestRelaySmtpFrom: null,
digestRelaySmtpTo: null
} }
} }
await this._initDeliveryReceipts(this.guild.guild.id) await this._initDeliveryReceipts(this.guild.guild.id)
@@ -2765,7 +2825,11 @@ class PearcordPlatform extends EventEmitter {
digestInboxChannelId: guild.digestInboxChannelId || null, digestInboxChannelId: guild.digestInboxChannelId || null,
digestRelayEnabled: !!guild.digestRelayEnabled, digestRelayEnabled: !!guild.digestRelayEnabled,
activeRelayPluginId: guild.activeRelayPluginId || null, activeRelayPluginId: guild.activeRelayPluginId || null,
digestRelayWebhookUrl: guild.digestRelayWebhookUrl || null digestRelayWebhookUrl: guild.digestRelayWebhookUrl || null,
digestRelaySmtpHost: guild.digestRelaySmtpHost || null,
digestRelaySmtpPort: guild.digestRelaySmtpPort ?? null,
digestRelaySmtpFrom: guild.digestRelaySmtpFrom || null,
digestRelaySmtpTo: guild.digestRelaySmtpTo || null
} }
} }
@@ -2820,10 +2884,35 @@ class PearcordPlatform extends EventEmitter {
if (partial.digestRelayWebhookUrl !== undefined) { if (partial.digestRelayWebhookUrl !== undefined) {
patch.digestRelayWebhookUrl = partial.digestRelayWebhookUrl || null patch.digestRelayWebhookUrl = partial.digestRelayWebhookUrl || null
} }
if (partial.digestRelaySmtpHost !== undefined) {
patch.digestRelaySmtpHost = partial.digestRelaySmtpHost || null
}
if (partial.digestRelaySmtpPort !== undefined) {
patch.digestRelaySmtpPort = partial.digestRelaySmtpPort ?? null
}
if (partial.digestRelaySmtpFrom !== undefined) {
patch.digestRelaySmtpFrom = partial.digestRelaySmtpFrom || null
}
if (partial.digestRelaySmtpTo !== undefined) {
patch.digestRelaySmtpTo = partial.digestRelaySmtpTo || null
}
await this.deliveryReceipts.setAutomationDigestNotifyPrefs(patch) await this.deliveryReceipts.setAutomationDigestNotifyPrefs(patch)
if (patch.digestRelayWebhookUrl !== undefined) { if (patch.digestRelayWebhookUrl !== undefined) {
await this._syncDigestRelayWebhookUrlToPlugin(patch.digestRelayWebhookUrl).catch(() => {}) await this._syncDigestRelayWebhookUrlToPlugin(patch.digestRelayWebhookUrl).catch(() => {})
} }
if (
patch.digestRelaySmtpHost !== undefined ||
patch.digestRelaySmtpPort !== undefined ||
patch.digestRelaySmtpFrom !== undefined ||
patch.digestRelaySmtpTo !== undefined
) {
await this._syncDigestRelaySmtpConfigToPlugin({
smtpHost: patch.digestRelaySmtpHost,
smtpPort: patch.digestRelaySmtpPort,
smtpFrom: patch.digestRelaySmtpFrom,
smtpTo: patch.digestRelaySmtpTo
}).catch(() => {})
}
setTimeout(() => this._pushGuildSyncToMesh().catch(() => {}), 400) setTimeout(() => this._pushGuildSyncToMesh().catch(() => {}), 400)
} else { } else {
if (partial.userMute != null || partial.muteNotifications != null) { if (partial.userMute != null || partial.muteNotifications != null) {
@@ -2921,7 +3010,11 @@ class PearcordPlatform extends EventEmitter {
if (plugin) { if (plugin) {
handoff = applyRelayPluginToHandoff(handoff, plugin, { handoff = applyRelayPluginToHandoff(handoff, plugin, {
relaySecret: this.guild.guild.id, relaySecret: this.guild.guild.id,
webhookUrl: prefs.digestRelayWebhookUrl || plugin.config?.webhookUrl || null webhookUrl: prefs.digestRelayWebhookUrl || plugin.config?.webhookUrl || null,
smtpHost: prefs.digestRelaySmtpHost || plugin.config?.smtpHost || null,
smtpPort: prefs.digestRelaySmtpPort ?? plugin.config?.smtpPort ?? null,
smtpFrom: prefs.digestRelaySmtpFrom || plugin.config?.smtpFrom || null,
smtpTo: prefs.digestRelaySmtpTo || plugin.config?.smtpTo || null
}) })
} }
await this.deliveryReceipts.appendDigestRelayHandoff(handoff) await this.deliveryReceipts.appendDigestRelayHandoff(handoff)
@@ -3971,7 +4064,11 @@ class PearcordPlatform extends EventEmitter {
const stamped = plugin const stamped = plugin
? applyRelayPluginToHandoff(handoff, plugin, { ? applyRelayPluginToHandoff(handoff, plugin, {
relaySecret: this.guild.guild.id, relaySecret: this.guild.guild.id,
webhookUrl: digPrefs.digestRelayWebhookUrl || null webhookUrl: digPrefs.digestRelayWebhookUrl || null,
smtpHost: digPrefs.digestRelaySmtpHost || null,
smtpPort: digPrefs.digestRelaySmtpPort ?? null,
smtpFrom: digPrefs.digestRelaySmtpFrom || null,
smtpTo: digPrefs.digestRelaySmtpTo || null
}) })
: handoff : handoff
if (plugin?.transport === 'webhook-bridge') { if (plugin?.transport === 'webhook-bridge') {
@@ -5970,8 +6067,14 @@ class PearcordPlatform extends EventEmitter {
}) })
const plugin = await this.deliveryReceipts.getActiveDigestRelayPlugin() const plugin = await this.deliveryReceipts.getActiveDigestRelayPlugin()
if (plugin) { if (plugin) {
const prefsEarly = await this.deliveryReceipts.getAutomationDigestNotifyPrefs()
handoff = applyRelayPluginToHandoff(handoff, plugin, { handoff = applyRelayPluginToHandoff(handoff, plugin, {
relaySecret: this.guild.guild.id relaySecret: this.guild.guild.id,
webhookUrl: prefsEarly.digestRelayWebhookUrl || null,
smtpHost: prefsEarly.digestRelaySmtpHost || null,
smtpPort: prefsEarly.digestRelaySmtpPort ?? null,
smtpFrom: prefsEarly.digestRelaySmtpFrom || null,
smtpTo: prefsEarly.digestRelaySmtpTo || null
}) })
} }
} }
@@ -5993,6 +6096,19 @@ class PearcordPlatform extends EventEmitter {
}) })
} }
async _syncDigestRelaySmtpConfigToPlugin (partial = {}) {
if (!this.guild?.guild) return null
const plugins = await this.deliveryReceipts.listDigestRelayPlugins()
const smtp = plugins.find((p) => p.pluginId === 'pearcord/digest-smtp-bridge')
if (!smtp) return null
const config = { ...(smtp.config || {}) }
if (partial.smtpHost !== undefined) config.smtpHost = partial.smtpHost || null
if (partial.smtpPort !== undefined) config.smtpPort = partial.smtpPort ?? null
if (partial.smtpFrom !== undefined) config.smtpFrom = partial.smtpFrom || null
if (partial.smtpTo !== undefined) config.smtpTo = partial.smtpTo || null
return this.deliveryReceipts.upsertDigestRelayPlugin({ ...smtp, config })
}
async syncModDigestGroupDmFromMesh (slice = {}) { async syncModDigestGroupDmFromMesh (slice = {}) {
if (!this.guild?.guild || !slice?.channelId) return null if (!this.guild?.guild || !slice?.channelId) return null
const roles = await this._memberRoles() const roles = await this._memberRoles()
@@ -6129,6 +6245,44 @@ class PearcordPlatform extends EventEmitter {
return row return row
} }
async pushHookFailureDigestCsvToMesh () {
if (!this.guild?.guild) throw new Error('no guild')
const roles = await this._memberRoles()
if (!roleHasPermission(roles, PERMISSION.MANAGE_GUILD)) {
throw new Error('no permission to sync hook failure digest CSV')
}
const exported = await this.exportHookFailureDigest({
format: 'csv',
recordMesh: false
})
if (!exported.signature) throw new Error('csv export not signed')
const payload = {
guildId: this.guild.guild.id,
exportedAt: exported.exportedAt || Date.now(),
exportedBy: this.identity.user?.id || null,
csvBody: exported.body,
signature: exported.signature,
signatureAlg: exported.signatureAlg
}
if (this.guild.gossipHookFailureDigestCsvSync) {
this.guild.gossipHookFailureDigestCsvSync(payload)
}
this.emit('hook-failure-digest-csv-sync', payload)
return exported
}
async _onHookFailureDigestCsvGossip (payload) {
if (!payload?.guildId || !payload?.csvBody || !payload?.signature) return null
if (this.guild?.guild?.id !== payload.guildId) return null
await this._initDeliveryReceipts(payload.guildId)
const row = await this.deliveryReceipts.ingestHookFailureDigestCsvSlice(
payload.guildId,
payload
)
if (row) this.emit('hook-failure-digest-csv-sync', payload)
return row
}
async getAuditExportSchedule () { async getAuditExportSchedule () {
if (!this.guild?.guild) { if (!this.guild?.guild) {
return { enabled: false, intervalHours: 24, filter: 'all', lastExportAt: 0 } return { enabled: false, intervalHours: 24, filter: 'all', lastExportAt: 0 }