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:
@@ -47,6 +47,8 @@ const {
|
||||
formatHookFailureDigestExport,
|
||||
signHookFailureDigestCsv,
|
||||
verifyHookFailureDigestCsv,
|
||||
signAuditExportCsv,
|
||||
verifyAuditExportCsv,
|
||||
formatAutomationScheduleDigestExport,
|
||||
mergeHookFailureDigests,
|
||||
buildAutomationScheduleDashboard,
|
||||
@@ -1201,6 +1203,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
let archivePeerSeen = []
|
||||
let hookFailureDigest = null
|
||||
let hookFailureDigestCsvExport = null
|
||||
let auditExportCsvExport = null
|
||||
let archiveHealthPrefs = null
|
||||
await this._initDeliveryReceipts(guild.id)
|
||||
if (this.deliveryReceipts) {
|
||||
@@ -1215,6 +1218,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
archiveHealthPrefs = await this.deliveryReceipts.exportArchiveHealthPrefsSlice()
|
||||
hookFailureDigestCsvExport =
|
||||
await this.deliveryReceipts.exportHookFailureDigestCsvSlice()
|
||||
auditExportCsvExport = await this.deliveryReceipts.exportAuditExportCsvSlice()
|
||||
digestRelayHandoff = await this.deliveryReceipts.exportDigestRelayHandoffSlice()
|
||||
const digestSnap = await this.deliveryReceipts.getHookFailureDigestSnapshot()
|
||||
if (digestSnap) {
|
||||
@@ -1300,6 +1304,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
archivePeerSeen,
|
||||
hookFailureDigest,
|
||||
hookFailureDigestCsvExport,
|
||||
auditExportCsvExport,
|
||||
archiveHealthPrefs,
|
||||
modDigestGroupDmMeta,
|
||||
workerReleaseHint,
|
||||
@@ -1442,6 +1447,13 @@ class PearcordPlatform extends EventEmitter {
|
||||
payload.hookFailureDigestCsvExport
|
||||
)
|
||||
}
|
||||
if (payload.auditExportCsvExport) {
|
||||
await this._initDeliveryReceipts(payload.guildId)
|
||||
await this.deliveryReceipts.ingestAuditExportCsvSlice(
|
||||
payload.guildId,
|
||||
payload.auditExportCsvExport
|
||||
)
|
||||
}
|
||||
if (payload.archiveHealthPrefs) {
|
||||
await this._initDeliveryReceipts(payload.guildId)
|
||||
await this.deliveryReceipts.ingestArchiveHealthPrefsSlice(
|
||||
@@ -1762,6 +1774,9 @@ class PearcordPlatform extends EventEmitter {
|
||||
guildInstance.on('hook-failure-digest-sync', (payload) => {
|
||||
this._onHookFailureDigestGossip(payload).catch(() => {})
|
||||
})
|
||||
guildInstance.on('hook-failure-digest-csv-sync', (payload) => {
|
||||
this._onHookFailureDigestCsvGossip(payload).catch(() => {})
|
||||
})
|
||||
guildInstance.on('audit-export-archive', (payload) => {
|
||||
this._onAuditExportArchiveGossip(payload).catch(() => {})
|
||||
})
|
||||
@@ -2109,7 +2124,33 @@ class PearcordPlatform extends EventEmitter {
|
||||
const format = opts.format === 'csv' ? 'csv' : 'json'
|
||||
const limit = Math.min(500, Math.max(1, Number(opts.limit) || 200))
|
||||
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 entryRows = entries.map((e) => ({
|
||||
id: e.id,
|
||||
@@ -2145,11 +2186,26 @@ class PearcordPlatform extends EventEmitter {
|
||||
format,
|
||||
count: entries.length,
|
||||
body,
|
||||
guildId: this.guild.guild.id,
|
||||
exportedAt
|
||||
guildId,
|
||||
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) {
|
||||
if (!payload?.guildId || !Array.isArray(payload.entries)) return null
|
||||
if (this.guild?.guild?.id !== payload.guildId) return null
|
||||
@@ -2748,7 +2804,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
muteNotifications: false,
|
||||
digestInboxChannelId: null,
|
||||
digestRelayEnabled: false,
|
||||
digestRelayWebhookUrl: null
|
||||
digestRelayWebhookUrl: null,
|
||||
digestRelaySmtpHost: null,
|
||||
digestRelaySmtpPort: null,
|
||||
digestRelaySmtpFrom: null,
|
||||
digestRelaySmtpTo: null
|
||||
}
|
||||
}
|
||||
await this._initDeliveryReceipts(this.guild.guild.id)
|
||||
@@ -2765,7 +2825,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
digestInboxChannelId: guild.digestInboxChannelId || null,
|
||||
digestRelayEnabled: !!guild.digestRelayEnabled,
|
||||
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) {
|
||||
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)
|
||||
if (patch.digestRelayWebhookUrl !== undefined) {
|
||||
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)
|
||||
} else {
|
||||
if (partial.userMute != null || partial.muteNotifications != null) {
|
||||
@@ -2921,7 +3010,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
if (plugin) {
|
||||
handoff = applyRelayPluginToHandoff(handoff, plugin, {
|
||||
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)
|
||||
@@ -3971,7 +4064,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
const stamped = plugin
|
||||
? applyRelayPluginToHandoff(handoff, plugin, {
|
||||
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
|
||||
if (plugin?.transport === 'webhook-bridge') {
|
||||
@@ -5970,8 +6067,14 @@ class PearcordPlatform extends EventEmitter {
|
||||
})
|
||||
const plugin = await this.deliveryReceipts.getActiveDigestRelayPlugin()
|
||||
if (plugin) {
|
||||
const prefsEarly = await this.deliveryReceipts.getAutomationDigestNotifyPrefs()
|
||||
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 = {}) {
|
||||
if (!this.guild?.guild || !slice?.channelId) return null
|
||||
const roles = await this._memberRoles()
|
||||
@@ -6129,6 +6245,44 @@ class PearcordPlatform extends EventEmitter {
|
||||
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 () {
|
||||
if (!this.guild?.guild) {
|
||||
return { enabled: false, intervalHours: 24, filter: 'all', lastExportAt: 0 }
|
||||
|
||||
Reference in New Issue
Block a user