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,
|
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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user