ReviewOS

also looking at this

stacks/commshq

Add BunPress guide and pre-E2E quality gates

#6
Merged glennmichael123 wants to merge docs/bunpress-product-guide into main
318 files +2174 -70006

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

app/Actions/Public/ConfirmSubscriptionAction.tsmodified+6-2
Changes to app/Actions/Public/ConfirmSubscriptionAction.ts
@@ -3,16 +3,20 @@ import { Action } from '@stacksjs/actions'
33import { config } from '@stacksjs/config'
44import { response } from '@stacksjs/router'
55import Contact from '../../Models/Contact'
6import { appendConsentOnce, consentIdempotencyKey, findConsentByKey } from './consent-ledger'
67import { verifyPublicToken } from './signed-token'
78
89export default new Action({
910 name: 'Confirm Subscription', description: 'Confirms a double opt-in token', method: 'GET',
1011 async handle(request: RequestInstance) {
11 const payload = verifyPublicToken(String(request.getParam('token') || ''), String(config.app.key))
12 const rawToken = String(request.getParam('token') || '')
13 const payload = verifyPublicToken(rawToken, String(config.app.key))
1214 if (!payload || payload.purpose !== 'confirm') return response.json({ error: 'Confirmation link is invalid or expired' }, 400)
1315 const contact = await Contact.where('id', payload.contactId).where('team_id', payload.teamId).first()
1416 if (!contact) return response.json({ error: 'Confirmation link is invalid or expired' }, 400)
1517 const recipient = String(payload.channel === 'sms' ? contact.phone : contact.email).trim().toLowerCase()
18 const idempotencyKey = consentIdempotencyKey({ scope: 'confirm', token: rawToken, channel: payload.channel, action: 'confirmed' })
19 if (await findConsentByKey(payload.teamId, idempotencyKey)) return response.redirect('/subscription-confirmed')
1620 const suppression = await CommunicationSuppression
1721 .where('team_id', payload.teamId)
1822 .where('channel', payload.channel)
@@ -25,7 +29,7 @@ export default new Action({
2529 })
2630 }
2731 await contact.update({ status: 'active' })
28 await ConsentEvent.create({ team_id: payload.teamId, recipient, channel: payload.channel, action: 'confirmed', purpose: 'marketing', source: 'double_opt_in', policyVersion: '1.0', proof: JSON.stringify({ tokenPurpose: payload.purpose }), occurredAt: new Date().toISOString() })
32 await appendConsentOnce({ teamId: payload.teamId, recipient, channel: payload.channel, action: 'confirmed', source: 'double_opt_in', idempotencyKey, proof: JSON.stringify({ tokenPurpose: payload.purpose }) })
2933 return response.redirect('/subscription-confirmed')
3034 },
3135})
app/Actions/Public/SubscribeAction.tsmodified+18-2
Changes to app/Actions/Public/SubscribeAction.ts
@@ -9,6 +9,7 @@ import Contact from '../../Models/Contact'
99import FormDefinition from '../../Models/FormDefinition'
1010import FormSubmission from '../../Models/FormSubmission'
1111import { buildConfirmationMessage } from '../../Mail/ConfirmSubscription'
12import { appendConsentOnce, consentIdempotencyKey } from './consent-ledger'
1213import { createPublicToken } from './signed-token'
1314
1415function clientIp(request: RequestInstance): string {
@@ -40,12 +41,27 @@ export default new Action({
4041 const existing = await FormSubmission.where('form_definition_id', form.id).where('dedupeKey', dedupeKey).first()
4142 if (!existing) await FormSubmission.forceCreate({ team_id: teamId, form_definition_id: form.id, contact_id: contact.id, payload: JSON.stringify({ email, firstName: request.get('firstName'), lastName: request.get('lastName') }), dedupeKey, sourceUrl: request.headers.get('referer') || undefined, ipHash: createHash('sha256').update(ip).digest('hex'), status: form.doubleOptIn ? 'accepted' : 'confirmed' })
4243
43 await ConsentEvent.create({ team_id: teamId, recipient: email, channel: 'email', action: form.doubleOptIn ? 'requested' : 'confirmed', purpose: 'marketing', source: `form:${form.uuid}`, jurisdiction: String(request.get('jurisdiction') || ''), policyVersion: '1.0', proof: JSON.stringify({ formId: form.id, userAgent: request.headers.get('user-agent') }), ipAddress: ip, occurredAt: new Date().toISOString() })
44 const action = form.doubleOptIn ? 'requested' : 'confirmed'
45 const consentKey = consentIdempotencyKey({ scope: `form:${form.id}:${dedupeKey}`, token: email, channel: 'email', action })
46 await appendConsentOnce({
47 teamId,
48 recipient: email,
49 channel: 'email',
50 action,
51 source: `form:${form.uuid}`,
52 idempotencyKey: consentKey,
53 jurisdiction: String(request.get('jurisdiction') || ''),
54 proof: JSON.stringify({ formId: form.id, userAgent: request.headers.get('user-agent') }),
55 ipAddress: ip,
56 })
4457
4558 if (form.doubleOptIn) {
4659 const token = createPublicToken({ teamId, contactId: contact.id, channel: 'email', purpose: 'confirm', expiresAt: Date.now() + 48 * 60 * 60 * 1000 }, String(config.app.key))
4760 const confirmationUrl = `${String(config.app.url).replace(/\/$/, '')}/confirm/${token}`
48 await job('SendEmail', { message: buildConfirmationMessage(email, confirmationUrl), driver: config.email.default }).onQueue('emails').dispatch()
61 await job('SendEmail', { message: buildConfirmationMessage(email, confirmationUrl), driver: config.email.default })
62 .withIdempotencyKey(`confirmation:${consentKey}`)
63 .onQueue('emails')
64 .dispatch()
4965 }
5066
5167 return response.json({ accepted: true, confirmationRequired: !!form.doubleOptIn }, 202)
app/Actions/Public/UnsubscribeAction.tsmodified+21-4
Changes to app/Actions/Public/UnsubscribeAction.ts
@@ -3,19 +3,36 @@ import { Action } from '@stacksjs/actions'
33import { config } from '@stacksjs/config'
44import { response } from '@stacksjs/router'
55import Contact from '../../Models/Contact'
6import { appendConsentOnce, consentIdempotencyKey, consentStateMatches, ensureSuppressed, findActiveSuppression, findLatestConsent } from './consent-ledger'
67import { verifyPublicToken } from './signed-token'
78
89export default new Action({
910 name: 'Public Unsubscribe', description: 'Applies one-click channel suppression', method: 'POST',
1011 async handle(request: RequestInstance) {
11 const payload = verifyPublicToken(String(request.getParam('token') || ''), String(config.app.key))
12 const rawToken = String(request.getParam('token') || '')
13 const payload = verifyPublicToken(rawToken, String(config.app.key))
1214 if (!payload || !['unsubscribe', 'preferences'].includes(payload.purpose)) return response.json({ error: 'Unsubscribe link is invalid or expired' }, 400)
1315 const contact = await Contact.where('id', payload.contactId).where('team_id', payload.teamId).first()
1416 if (!contact) return response.json({ error: 'Unsubscribe link is invalid or expired' }, 400)
1517 const recipient = String(payload.channel === 'sms' ? contact.phone : contact.email).trim().toLowerCase()
16 const existing = await CommunicationSuppression.where('team_id', payload.teamId).where('channel', payload.channel).where('recipient', recipient).first()
17 if (!existing) await CommunicationSuppression.create({ team_id: payload.teamId, recipient, channel: payload.channel, reason: 'unsubscribe', source: 'one_click', suppressedAt: new Date().toISOString() })
18 await ConsentEvent.create({ team_id: payload.teamId, recipient, channel: payload.channel, action: 'revoked', purpose: 'marketing', source: 'one_click', policyVersion: '1.0', proof: JSON.stringify({ tokenPurpose: payload.purpose }), occurredAt: new Date().toISOString() })
18 const [activeSuppression, latestConsent] = await Promise.all([
19 findActiveSuppression(payload.teamId, payload.channel, recipient),
20 findLatestConsent(payload.teamId, payload.channel, recipient),
21 ])
22 const alreadyApplied = !!activeSuppression && consentStateMatches(latestConsent, 'revoked', 'one_click')
23
24 await ensureSuppressed({ teamId: payload.teamId, recipient, channel: payload.channel, source: 'one_click' })
25 if (!alreadyApplied) {
26 await appendConsentOnce({
27 teamId: payload.teamId,
28 recipient,
29 channel: payload.channel,
30 action: 'revoked',
31 source: 'one_click',
32 idempotencyKey: consentIdempotencyKey({ scope: 'unsubscribe', token: rawToken, channel: payload.channel, action: 'revoked', predecessorId: latestConsent?.id }),
33 proof: JSON.stringify({ tokenPurpose: payload.purpose }),
34 })
35 }
1936 if (payload.channel === 'email') await contact.update({ status: 'unsubscribed' })
2037 return response.json({ unsubscribed: true })
2138 },
app/Actions/Public/UpdatePreferencesAction.tsmodified+42-37
Changes to app/Actions/Public/UpdatePreferencesAction.ts
@@ -5,37 +5,28 @@ import { job } from '@stacksjs/queue'
55import { response } from '@stacksjs/router'
66import Contact from '../../Models/Contact'
77import { buildConfirmationMessage } from '../../Mail/ConfirmSubscription'
8import { appendConsentOnce, consentIdempotencyKey, consentStateMatches, ensureSuppressed, findActiveSuppression, findLatestConsent } from './consent-ledger'
89import { parsePreferenceChoice } from './preference-policy'
910import { createPublicToken, verifyPublicToken } from './signed-token'
1011
11async function suppress(teamId: number, channel: 'email' | 'sms', recipient: string): Promise<void> {
12async function suppress(teamId: number, channel: 'email' | 'sms', recipient: string, token: string): Promise<void> {
1213 if (!recipient) return
13 const existing = await CommunicationSuppression
14 .where('team_id', teamId)
15 .where('channel', channel)
16 .where('recipient', recipient)
17 .whereNull('liftedAt')
18 .first()
19 if (!existing) {
20 await CommunicationSuppression.create({
21 team_id: teamId,
22 recipient,
23 channel,
24 reason: 'unsubscribe',
25 source: 'preference_center',
26 suppressedAt: new Date().toISOString(),
27 })
28 }
29 await ConsentEvent.create({
30 team_id: teamId,
14 const [activeSuppression, latestConsent] = await Promise.all([
15 findActiveSuppression(teamId, channel, recipient),
16 findLatestConsent(teamId, channel, recipient),
17 ])
18 const alreadyApplied = !!activeSuppression && consentStateMatches(latestConsent, 'revoked', 'preference_center')
19
20 await ensureSuppressed({ teamId, recipient, channel, source: 'preference_center' })
21 if (alreadyApplied) return
22 await appendConsentOnce({
23 teamId,
3124 recipient,
3225 channel,
3326 action: 'revoked',
34 purpose: 'marketing',
3527 source: 'preference_center',
36 policyVersion: '1.0',
28 idempotencyKey: consentIdempotencyKey({ scope: 'preferences', token, channel, action: 'revoked', predecessorId: latestConsent?.id }),
3729 proof: JSON.stringify({ signedPreferenceToken: true }),
38 occurredAt: new Date().toISOString(),
3930 })
4031}
4132
@@ -44,7 +35,8 @@ export default new Action({
4435 description: 'Applies opt-outs and starts verified channel opt-ins',
4536 method: 'POST',
4637 async handle(request: RequestInstance) {
47 const payload = verifyPublicToken(String(request.getParam('token') || ''), String(config.app.key))
38 const rawToken = String(request.getParam('token') || '')
39 const payload = verifyPublicToken(rawToken, String(config.app.key))
4840 if (!payload || payload.purpose !== 'preferences')
4941 return response.json({ error: 'Preference link is invalid or expired' }, 400)
5042
@@ -58,31 +50,44 @@ export default new Action({
5850 const result = { emailConfirmationRequired: false, smsStartRequired: false }
5951
6052 if (wantsEmail === false) {
61 await suppress(payload.teamId, 'email', email)
53 await suppress(payload.teamId, 'email', email, rawToken)
6254 await contact.update({ status: 'unsubscribed' })
6355 }
6456 else if (wantsEmail === true && email) {
65 await ConsentEvent.create({
66 team_id: payload.teamId,
57 const [activeSuppression, latestConsent] = await Promise.all([
58 findActiveSuppression(payload.teamId, 'email', email),
59 findLatestConsent(payload.teamId, 'email', email),
60 ])
61 const alreadyRequested = consentStateMatches(latestConsent, 'requested', 'preference_center')
62 const alreadyEnabled = !activeSuppression && !!latestConsent && ['granted', 'confirmed'].includes(String(latestConsent.action))
63 const transitionKey = consentIdempotencyKey({ scope: 'preferences', token: rawToken, channel: 'email', action: 'requested', predecessorId: latestConsent?.id })
64 const requestKey = alreadyRequested
65 ? String(latestConsent.idempotencyKey || latestConsent.idempotency_key || transitionKey)
66 : transitionKey
67 const created = alreadyRequested || alreadyEnabled ? false : await appendConsentOnce({
68 teamId: payload.teamId,
6769 recipient: email,
6870 channel: 'email',
6971 action: 'requested',
70 purpose: 'marketing',
7172 source: 'preference_center',
72 policyVersion: '1.0',
73 idempotencyKey: requestKey,
7374 proof: JSON.stringify({ signedPreferenceToken: true }),
74 occurredAt: new Date().toISOString(),
7575 })
76 const token = createPublicToken({ teamId: payload.teamId, contactId: contact.id, channel: 'email', purpose: 'confirm', expiresAt: Date.now() + 48 * 60 * 60 * 1000 }, String(config.app.key))
77 const confirmationUrl = `${String(config.app.url).replace(/\/$/, '')}/confirm/${token}`
78 await job('SendEmail', {
79 message: buildConfirmationMessage(email, confirmationUrl),
80 driver: config.email.default,
81 }).onQueue('emails').dispatch()
82 result.emailConfirmationRequired = true
76 if (created || alreadyRequested) {
77 result.emailConfirmationRequired = true
78 const token = createPublicToken({ teamId: payload.teamId, contactId: contact.id, channel: 'email', purpose: 'confirm', expiresAt: Date.now() + 48 * 60 * 60 * 1000 }, String(config.app.key))
79 const confirmationUrl = `${String(config.app.url).replace(/\/$/, '')}/confirm/${token}`
80 await job('SendEmail', {
81 message: buildConfirmationMessage(email, confirmationUrl),
82 driver: config.email.default,
83 })
84 .withIdempotencyKey(`confirmation:${requestKey}`)
85 .onQueue('emails')
86 .dispatch()
87 }
8388 }
8489
85 if (wantsSms === false) await suppress(payload.teamId, 'sms', sms)
90 if (wantsSms === false) await suppress(payload.teamId, 'sms', sms, rawToken)
8691 else if (wantsSms === true && sms) result.smsStartRequired = true
8792
8893 if (request.headers.get('accept')?.includes('text/html'))
app/Actions/Public/consent-ledger.tsadded+132-0
Changes to app/Actions/Public/consent-ledger.ts
@@ -0,0 +1,132 @@
1import { createHash } from 'node:crypto'
2
3export type ConsentChannel = 'email' | 'sms'
4export type ConsentAction = 'requested' | 'granted' | 'revoked' | 'confirmed' | 'suppressed'
5
6interface ConsentKeyInput {
7 scope: string
8 token: string
9 channel: ConsentChannel
10 action: ConsentAction
11 predecessorId?: number | string | null
12}
13
14interface ConsentEventInput {
15 teamId: number
16 recipient: string
17 channel: ConsentChannel
18 action: ConsentAction
19 source: string
20 idempotencyKey: string
21 purpose?: string
22 policyVersion?: string
23 proof?: string
24 jurisdiction?: string
25 ipAddress?: string
26}
27
28interface SuppressionInput {
29 teamId: number
30 recipient: string
31 channel: ConsentChannel
32 source: string
33 reason?: 'unsubscribe' | 'bounce' | 'complaint' | 'carrier' | 'manual' | 'legal'
34}
35
36export function consentIdempotencyKey(input: ConsentKeyInput): string {
37 const tokenDigest = createHash('sha256').update(input.token).digest('hex')
38 const predecessor = input.predecessorId == null ? 'origin' : String(input.predecessorId)
39 return `public:${input.scope}:${input.channel}:${input.action}:${predecessor}:${tokenDigest}`
40}
41
42export function consentStateMatches(event: any, action: ConsentAction, source: string): boolean {
43 return !!event && String(event.action) === action && String(event.source) === source
44}
45
46export async function findLatestConsent(teamId: number, channel: ConsentChannel, recipient: string): Promise<any | null> {
47 return ConsentEvent
48 .where('team_id', teamId)
49 .where('channel', channel)
50 .where('recipient', recipient)
51 .orderByDesc('occurredAt')
52 .first()
53}
54
55export async function findConsentByKey(teamId: number, idempotencyKey: string): Promise<any | null> {
56 return ConsentEvent
57 .where('team_id', teamId)
58 .where('idempotency_key', idempotencyKey)
59 .first()
60}
61
62export async function appendConsentOnce(input: ConsentEventInput): Promise<boolean> {
63 const findExisting = () => findConsentByKey(input.teamId, input.idempotencyKey)
64 if (await findExisting()) return false
65
66 try {
67 await ConsentEvent.create({
68 team_id: input.teamId,
69 recipient: input.recipient,
70 channel: input.channel,
71 action: input.action,
72 purpose: input.purpose ?? 'marketing',
73 source: input.source,
74 jurisdiction: input.jurisdiction,
75 policyVersion: input.policyVersion ?? '1.0',
76 idempotencyKey: input.idempotencyKey,
77 proof: input.proof,
78 ipAddress: input.ipAddress,
79 occurredAt: new Date().toISOString(),
80 })
81 return true
82 }
83 catch (error) {
84 if (await findExisting()) return false
85 throw error
86 }
87}
88
89export async function findActiveSuppression(teamId: number, channel: ConsentChannel, recipient: string): Promise<any | null> {
90 if (!recipient) return null
91 return CommunicationSuppression
92 .where('team_id', teamId)
93 .where('channel', channel)
94 .where('recipient', recipient)
95 .whereNull('liftedAt')
96 .first()
97}
98
99export async function ensureSuppressed(input: SuppressionInput): Promise<void> {
100 if (!input.recipient || await findActiveSuppression(input.teamId, input.channel, input.recipient)) return
101
102 const existing = await CommunicationSuppression
103 .where('team_id', input.teamId)
104 .where('channel', input.channel)
105 .where('recipient', input.recipient)
106 .first()
107
108 if (existing) {
109 await CommunicationSuppression.forceUpdate(existing.id, {
110 reason: input.reason ?? 'unsubscribe',
111 source: input.source,
112 suppressedAt: new Date().toISOString(),
113 liftedAt: null,
114 })
115 return
116 }
117
118 try {
119 await CommunicationSuppression.create({
120 team_id: input.teamId,
121 recipient: input.recipient,
122 channel: input.channel,
123 reason: input.reason ?? 'unsubscribe',
124 source: input.source,
125 suppressedAt: new Date().toISOString(),
126 })
127 }
128 catch (error) {
129 if (await findActiveSuppression(input.teamId, input.channel, input.recipient)) return
130 throw error
131 }
132}
app/Jobs/ProcessWebhookEvent.tsmodified+11-10
Changes to app/Jobs/ProcessWebhookEvent.ts
@@ -4,6 +4,7 @@ import { Job } from '@stacksjs/queue'
44import CommerceConnection from '../Models/CommerceConnection'
55import CommerceEvent from '../Models/CommerceEvent'
66import WebhookEvent from '../Models/WebhookEvent'
7import { appendConsentOnce, ensureSuppressed } from '../Actions/Public/consent-ledger'
78import { twilioConsentIdempotencyKey } from '../Actions/Webhooks/twilio-compliance'
89import { commerceEventType, objectPayload, webhookRecipient } from '../Actions/Webhooks/payload'
910
@@ -32,25 +33,25 @@ export default new Job({
3233 if (event.provider === 'twilio' && !payload.MessageStatus) {
3334 const { intent, keyword } = classifySmsIntent(String(payload.Body || ''))
3435 const idempotencyKey = twilioConsentIdempotencyKey(String(event.providerEventId))
35 const existingConsent = await ConsentEvent
36 .where('team_id', input.teamId)
37 .where('idempotency_key', idempotencyKey)
38 .first()
3936 if (intent === 'opt-out') {
40 const existing = await CommunicationSuppression.where('team_id', input.teamId).where('channel', 'sms').where('recipient', recipient).first()
41 if (!existing) await CommunicationSuppression.create({ team_id: input.teamId, channel: 'sms', recipient, reason: 'unsubscribe', source: `twilio:${keyword}`, suppressedAt: new Date().toISOString() })
42 if (!existingConsent) await ConsentEvent.create({ team_id: input.teamId, recipient, channel: 'sms', action: 'revoked', purpose: 'marketing', source: 'twilio_inbound', policyVersion: '1.0', idempotencyKey, proof: JSON.stringify({ eventId: event.providerEventId, keyword }), occurredAt: new Date().toISOString() })
37 await ensureSuppressed({ teamId: input.teamId, channel: 'sms', recipient, source: `twilio:${keyword}` })
38 await appendConsentOnce({ teamId: input.teamId, recipient, channel: 'sms', action: 'revoked', source: 'twilio_inbound', idempotencyKey, proof: JSON.stringify({ eventId: event.providerEventId, keyword }) })
4339 }
4440 else if (intent === 'opt-in') {
4541 const existing = await CommunicationSuppression.where('team_id', input.teamId).where('channel', 'sms').where('recipient', recipient).first()
4642 if (existing && !existing.liftedAt) await CommunicationSuppression.forceUpdate(existing.id, { liftedAt: new Date().toISOString() })
47 if (!existingConsent) await ConsentEvent.create({ team_id: input.teamId, recipient, channel: 'sms', action: 'granted', purpose: 'marketing', source: 'twilio_inbound', policyVersion: '1.0', idempotencyKey, proof: JSON.stringify({ eventId: event.providerEventId, keyword }), occurredAt: new Date().toISOString() })
43 await appendConsentOnce({ teamId: input.teamId, recipient, channel: 'sms', action: 'granted', source: 'twilio_inbound', idempotencyKey, proof: JSON.stringify({ eventId: event.providerEventId, keyword }) })
4844 }
4945 }
5046
5147 if (event.provider === 'mail' && ['bounce', 'complaint'].some(kind => String(event.type).includes(kind))) {
52 const existing = await CommunicationSuppression.where('team_id', input.teamId).where('channel', 'email').where('recipient', recipient).first()
53 if (!existing) await CommunicationSuppression.create({ team_id: input.teamId, channel: 'email', recipient, reason: String(event.type).includes('complaint') ? 'complaint' : 'bounce', source: 'owned_mail', suppressedAt: new Date().toISOString() })
48 await ensureSuppressed({
49 teamId: input.teamId,
50 channel: 'email',
51 recipient,
52 reason: String(event.type).includes('complaint') ? 'complaint' : 'bounce',
53 source: 'owned_mail',
54 })
5455 }
5556
5657 if (['shopify', 'woocommerce'].includes(String(event.provider))) {
tests/Unit/ConsentIdempotency.test.tsadded+107-0
Changes to tests/Unit/ConsentIdempotency.test.ts
@@ -0,0 +1,107 @@
1import { afterEach, describe, expect, it } from 'bun:test'
2import { readFileSync } from 'node:fs'
3import { resolve } from 'node:path'
4import { appendConsentOnce, consentIdempotencyKey, consentStateMatches, ensureSuppressed } from '../../app/Actions/Public/consent-ledger'
5
6const originalConsentModel = (globalThis as any).ConsentEvent
7const originalSuppressionModel = (globalThis as any).CommunicationSuppression
8
9afterEach(() => {
10 ;(globalThis as any).ConsentEvent = originalConsentModel
11 ;(globalThis as any).CommunicationSuppression = originalSuppressionModel
12})
13
14describe('public consent idempotency', () => {
15 it('generates a stable key without exposing the signed token', () => {
16 const input = { scope: 'unsubscribe', token: 'signed.secret.token', channel: 'email' as const, action: 'revoked' as const, predecessorId: 42 }
17 const first = consentIdempotencyKey(input)
18
19 expect(consentIdempotencyKey(input)).toBe(first)
20 expect(first).not.toContain(input.token)
21 expect(first.length).toBeLessThanOrEqual(255)
22 })
23
24 it('allows a later state transition while deduplicating the same transition', () => {
25 const base = { scope: 'preferences', token: 'signed-token', channel: 'email' as const, action: 'revoked' as const }
26
27 expect(consentIdempotencyKey({ ...base, predecessorId: 10 })).toBe(consentIdempotencyKey({ ...base, predecessorId: 10 }))
28 expect(consentIdempotencyKey({ ...base, predecessorId: 10 })).not.toBe(consentIdempotencyKey({ ...base, predecessorId: 11 }))
29 })
30
31 it('matches both the consent action and its evidence source', () => {
32 expect(consentStateMatches({ action: 'revoked', source: 'one_click' }, 'revoked', 'one_click')).toBe(true)
33 expect(consentStateMatches({ action: 'revoked', source: 'webhook' }, 'revoked', 'one_click')).toBe(false)
34 expect(consentStateMatches(null, 'revoked', 'one_click')).toBe(false)
35 })
36
37 it('guards confirmation replay before lifting a newer suppression', () => {
38 const action = readFileSync(resolve(import.meta.dir, '../../app/Actions/Public/ConfirmSubscriptionAction.ts'), 'utf8')
39 const replayGuard = action.indexOf('if (await findConsentByKey')
40 const suppressionLookup = action.indexOf('CommunicationSuppression')
41
42 expect(replayGuard).toBeGreaterThan(-1)
43 expect(replayGuard).toBeLessThan(suppressionLookup)
44 })
45
46 it('reactivates a previously lifted suppression', async () => {
47 let update: Record<string, unknown> | undefined
48 const existing = { id: 7, liftedAt: '2026-09-01T00:00:00.000Z' }
49 const query: any = {
50 first: async () => existing,
51 where: () => query,
52 whereNull: () => ({ first: async () => null }),
53 }
54 ;(globalThis as any).CommunicationSuppression = {
55 forceUpdate: async (_id: number, values: Record<string, unknown>) => { update = values },
56 where: () => query,
57 }
58
59 await ensureSuppressed({ teamId: 1, channel: 'email', recipient: 'person@example.com', source: 'one_click' })
60
61 expect(update).toMatchObject({ liftedAt: null, reason: 'unsubscribe', source: 'one_click' })
62 })
63
64 it('treats a concurrent unique-key winner as an idempotent replay', async () => {
65 let reads = 0
66 const query: any = {
67 first: async () => ++reads === 1 ? null : { id: 9 },
68 where: () => query,
69 }
70 ;(globalThis as any).ConsentEvent = {
71 create: async () => { throw new Error('unique constraint') },
72 where: () => query,
73 }
74
75 const created = await appendConsentOnce({
76 teamId: 1,
77 recipient: 'person@example.com',
78 channel: 'email',
79 action: 'revoked',
80 source: 'one_click',
81 idempotencyKey: 'public:unsubscribe:email:revoked:origin:digest',
82 })
83
84 expect(created).toBe(false)
85 expect(reads).toBe(2)
86 })
87
88 it('routes every public consent writer through the idempotent ledger', () => {
89 const files = [
90 'SubscribeAction.ts',
91 'UnsubscribeAction.ts',
92 'ConfirmSubscriptionAction.ts',
93 'UpdatePreferencesAction.ts',
94 ]
95
96 for (const file of files) {
97 const action = readFileSync(resolve(import.meta.dir, `../../app/Actions/Public/${file}`), 'utf8')
98 expect(action).toContain('appendConsentOnce')
99 expect(action).not.toContain('ConsentEvent.create')
100 }
101
102 const subscribe = readFileSync(resolve(import.meta.dir, '../../app/Actions/Public/SubscribeAction.ts'), 'utf8')
103 const preferences = readFileSync(resolve(import.meta.dir, '../../app/Actions/Public/UpdatePreferencesAction.ts'), 'utf8')
104 expect(subscribe).toContain('.withIdempotencyKey(')
105 expect(preferences).toContain('.withIdempotencyKey(')
106 })
107})