@openlup/platform 0.16.0 → 0.17.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/CHANGELOG.md +84 -0
- package/README.md +285 -5
- package/dist/communications/accountingDocumentDelivery.d.ts +35 -0
- package/dist/communications/accountingDocumentDelivery.js +33 -0
- package/dist/communications/authEmailError.d.ts +5 -0
- package/dist/communications/authEmailError.js +8 -0
- package/dist/communications/authEmailPorts.d.ts +2 -0
- package/dist/communications/authEmailPorts.js +1 -0
- package/dist/communications/authSendEmailHook.d.ts +35 -0
- package/dist/communications/authSendEmailHook.js +250 -0
- package/dist/communications/capturedTransactionalDelivery.d.ts +13 -0
- package/dist/communications/capturedTransactionalDelivery.js +14 -0
- package/dist/communications/communicationSync.d.ts +11 -0
- package/dist/communications/communicationSync.js +79 -0
- package/dist/communications/communicationSyncWorker.d.ts +68 -0
- package/dist/communications/communicationSyncWorker.js +218 -0
- package/dist/communications/consentPolicy.d.ts +19 -0
- package/dist/communications/consentPolicy.js +43 -0
- package/dist/communications/consentPurposeRegistry.d.ts +4 -0
- package/dist/communications/consentPurposeRegistry.js +24 -0
- package/dist/communications/contracts/abandonedCart.d.ts +27 -0
- package/dist/communications/contracts/abandonedCart.js +25 -0
- package/dist/communications/contracts/authEmail.d.ts +62 -0
- package/dist/communications/contracts/authEmail.js +1 -0
- package/dist/communications/contracts/backInStock.d.ts +28 -0
- package/dist/communications/contracts/backInStock.js +28 -0
- package/dist/communications/contracts/blocks.d.ts +24 -0
- package/dist/communications/contracts/blocks.js +23 -0
- package/dist/communications/contracts/checkoutExpired.d.ts +22 -0
- package/dist/communications/contracts/checkoutExpired.js +25 -0
- package/dist/communications/contracts/checkoutRecovery.d.ts +26 -0
- package/dist/communications/contracts/checkoutRecovery.js +37 -0
- package/dist/communications/contracts/commerceEmailContent.d.ts +176 -0
- package/dist/communications/contracts/commerceEmailContent.js +1 -0
- package/dist/communications/contracts/commerceEngagementEmailContent.d.ts +95 -0
- package/dist/communications/contracts/commerceEngagementEmailContent.js +1 -0
- package/dist/communications/contracts/consent.d.ts +12 -0
- package/dist/communications/contracts/consent.js +1 -0
- package/dist/communications/contracts/email.d.ts +182 -0
- package/dist/communications/contracts/email.js +1 -0
- package/dist/communications/contracts/guideCard.d.ts +3 -0
- package/dist/communications/contracts/guideCard.js +26 -0
- package/dist/communications/contracts/index.d.ts +51 -0
- package/dist/communications/contracts/index.js +42 -0
- package/dist/communications/contracts/inquiryContent.d.ts +32 -0
- package/dist/communications/contracts/inquiryContent.js +33 -0
- package/dist/communications/contracts/invoiceDocumentContent.d.ts +26 -0
- package/dist/communications/contracts/invoiceDocumentContent.js +36 -0
- package/dist/communications/contracts/marketingAutomation.d.ts +52 -0
- package/dist/communications/contracts/marketingAutomation.js +1 -0
- package/dist/communications/contracts/newsletter.d.ts +63 -0
- package/dist/communications/contracts/newsletter.js +1 -0
- package/dist/communications/contracts/newsletterWebhook.d.ts +26 -0
- package/dist/communications/contracts/newsletterWebhook.js +1 -0
- package/dist/communications/contracts/optOutContent.d.ts +11 -0
- package/dist/communications/contracts/optOutContent.js +17 -0
- package/dist/communications/contracts/orderCanceled.d.ts +25 -0
- package/dist/communications/contracts/orderCanceled.js +27 -0
- package/dist/communications/contracts/orderDraft.d.ts +46 -0
- package/dist/communications/contracts/orderDraft.js +46 -0
- package/dist/communications/contracts/orderPaid.d.ts +53 -0
- package/dist/communications/contracts/orderPaid.js +71 -0
- package/dist/communications/contracts/orderRefunded.d.ts +24 -0
- package/dist/communications/contracts/orderRefunded.js +27 -0
- package/dist/communications/contracts/paymentFailed.d.ts +24 -0
- package/dist/communications/contracts/paymentFailed.js +24 -0
- package/dist/communications/contracts/publicIntake.d.ts +49 -0
- package/dist/communications/contracts/publicIntake.js +1 -0
- package/dist/communications/contracts/render.d.ts +2 -0
- package/dist/communications/contracts/render.js +229 -0
- package/dist/communications/contracts/reorderReminder.d.ts +24 -0
- package/dist/communications/contracts/reorderReminder.js +24 -0
- package/dist/communications/contracts/returnEmails.d.ts +25 -0
- package/dist/communications/contracts/returnEmails.js +62 -0
- package/dist/communications/contracts/reviewEffects.d.ts +26 -0
- package/dist/communications/contracts/reviewEffects.js +25 -0
- package/dist/communications/contracts/reviewRequest.d.ts +26 -0
- package/dist/communications/contracts/reviewRequest.js +24 -0
- package/dist/communications/contracts/roleNotificationContent.d.ts +25 -0
- package/dist/communications/contracts/roleNotificationContent.js +46 -0
- package/dist/communications/contracts/shipmentDelivered.d.ts +23 -0
- package/dist/communications/contracts/shipmentDelivered.js +24 -0
- package/dist/communications/contracts/shipmentDispatched.d.ts +27 -0
- package/dist/communications/contracts/shipmentDispatched.js +35 -0
- package/dist/communications/contracts/shipmentException.d.ts +21 -0
- package/dist/communications/contracts/shipmentException.js +26 -0
- package/dist/communications/contracts/subscriptionActivationActionRequired.d.ts +17 -0
- package/dist/communications/contracts/subscriptionActivationActionRequired.js +22 -0
- package/dist/communications/contracts/subscriptionAddressChanged.d.ts +19 -0
- package/dist/communications/contracts/subscriptionAddressChanged.js +25 -0
- package/dist/communications/contracts/subscriptionCancelled.d.ts +19 -0
- package/dist/communications/contracts/subscriptionCancelled.js +25 -0
- package/dist/communications/contracts/subscriptionCycleSkipped.d.ts +25 -0
- package/dist/communications/contracts/subscriptionCycleSkipped.js +40 -0
- package/dist/communications/contracts/subscriptionDeliveryRescheduled.d.ts +25 -0
- package/dist/communications/contracts/subscriptionDeliveryRescheduled.js +39 -0
- package/dist/communications/contracts/subscriptionEmailContent.d.ts +145 -0
- package/dist/communications/contracts/subscriptionEmailContent.js +1 -0
- package/dist/communications/contracts/subscriptionPackageChanged.d.ts +20 -0
- package/dist/communications/contracts/subscriptionPackageChanged.js +26 -0
- package/dist/communications/contracts/subscriptionPauseReminder.d.ts +23 -0
- package/dist/communications/contracts/subscriptionPauseReminder.js +29 -0
- package/dist/communications/contracts/subscriptionPaused.d.ts +21 -0
- package/dist/communications/contracts/subscriptionPaused.js +25 -0
- package/dist/communications/contracts/subscriptionPaymentExpired.d.ts +34 -0
- package/dist/communications/contracts/subscriptionPaymentExpired.js +37 -0
- package/dist/communications/contracts/subscriptionPaymentFailed.d.ts +49 -0
- package/dist/communications/contracts/subscriptionPaymentFailed.js +56 -0
- package/dist/communications/contracts/subscriptionPaymentRecovered.d.ts +19 -0
- package/dist/communications/contracts/subscriptionPaymentRecovered.js +22 -0
- package/dist/communications/contracts/subscriptionRenewalAtRisk.d.ts +36 -0
- package/dist/communications/contracts/subscriptionRenewalAtRisk.js +36 -0
- package/dist/communications/contracts/subscriptionRenewalUpcoming.d.ts +49 -0
- package/dist/communications/contracts/subscriptionRenewalUpcoming.js +43 -0
- package/dist/communications/contracts/subscriptionResumed.d.ts +23 -0
- package/dist/communications/contracts/subscriptionResumed.js +25 -0
- package/dist/communications/contracts/subscriptionWelcome.d.ts +44 -0
- package/dist/communications/contracts/subscriptionWelcome.js +45 -0
- package/dist/communications/contracts/subscriptionWinback.d.ts +21 -0
- package/dist/communications/contracts/subscriptionWinback.js +28 -0
- package/dist/communications/contracts/transactionalDelivery.d.ts +42 -0
- package/dist/communications/contracts/transactionalDelivery.js +1 -0
- package/dist/communications/contracts/types.d.ts +46 -0
- package/dist/communications/contracts/types.js +1 -0
- package/dist/communications/contribution.d.ts +10 -0
- package/dist/communications/contribution.js +24 -0
- package/dist/communications/deliveryReconciliation.d.ts +64 -0
- package/dist/communications/deliveryReconciliation.js +60 -0
- package/dist/communications/emailEgress.d.ts +38 -0
- package/dist/communications/emailEgress.js +100 -0
- package/dist/communications/emailHealthAssessment.d.ts +39 -0
- package/dist/communications/emailHealthAssessment.js +59 -0
- package/dist/communications/emailHealthMetricsPort.d.ts +17 -0
- package/dist/communications/emailHealthMetricsPort.js +1 -0
- package/dist/communications/emailNotificationControl.d.ts +27 -0
- package/dist/communications/emailNotificationControl.js +10 -0
- package/dist/communications/emailObservation.d.ts +28 -0
- package/dist/communications/emailObservation.js +23 -0
- package/dist/communications/emailTransport.d.ts +82 -0
- package/dist/communications/emailTransport.js +95 -0
- package/dist/communications/errors.d.ts +15 -0
- package/dist/communications/errors.js +30 -0
- package/dist/communications/index.d.ts +46 -0
- package/dist/communications/index.js +38 -0
- package/dist/communications/integrationEventSignature.d.ts +15 -0
- package/dist/communications/integrationEventSignature.js +37 -0
- package/dist/communications/managedTransactionalDeliveryStore.d.ts +12 -0
- package/dist/communications/managedTransactionalDeliveryStore.js +49 -0
- package/dist/communications/nativeOperations.d.ts +79 -0
- package/dist/communications/nativeOperations.js +3 -0
- package/dist/communications/newsletterIntegrationContracts.d.ts +130 -0
- package/dist/communications/newsletterIntegrationContracts.js +128 -0
- package/dist/communications/newsletterWebhookPort.d.ts +11 -0
- package/dist/communications/newsletterWebhookPort.js +84 -0
- package/dist/communications/newsletterWebhookProcessing.d.ts +17 -0
- package/dist/communications/newsletterWebhookProcessing.js +71 -0
- package/dist/communications/originPolicy.d.ts +40 -0
- package/dist/communications/originPolicy.js +100 -0
- package/dist/communications/postgresCommunicationSync.d.ts +79 -0
- package/dist/communications/postgresCommunicationSync.js +111 -0
- package/dist/communications/postgresConsentEvaluator.d.ts +19 -0
- package/dist/communications/postgresConsentEvaluator.js +43 -0
- package/dist/communications/postgresDeliveryReconciliation.d.ts +3 -0
- package/dist/communications/postgresDeliveryReconciliation.js +113 -0
- package/dist/communications/postgresNativeOperations.d.ts +13 -0
- package/dist/communications/postgresNativeOperations.js +63 -0
- package/dist/communications/postgresNotificationRecipients.d.ts +37 -0
- package/dist/communications/postgresNotificationRecipients.js +60 -0
- package/dist/communications/postgresTransactionalDeliveryStore.d.ts +7 -0
- package/dist/communications/postgresTransactionalDeliveryStore.js +55 -0
- package/dist/communications/preferencesToken.d.ts +10 -0
- package/dist/communications/preferencesToken.js +40 -0
- package/dist/communications/publicPreferenceToken.d.ts +24 -0
- package/dist/communications/publicPreferenceToken.js +53 -0
- package/dist/communications/shipmentNotifications.d.ts +34 -0
- package/dist/communications/shipmentNotifications.js +61 -0
- package/dist/communications/sql.d.ts +26 -0
- package/dist/communications/sql.js +7 -0
- package/dist/communications/standardWebhooks.d.ts +14 -0
- package/dist/communications/standardWebhooks.js +73 -0
- package/dist/communications/status.d.ts +11 -0
- package/dist/communications/status.js +36 -0
- package/dist/communications/transactionalDelivery.d.ts +19 -0
- package/dist/communications/transactionalDelivery.js +96 -0
- package/dist/communications/transactionalDeliveryIdentifier.d.ts +3 -0
- package/dist/communications/transactionalDeliveryIdentifier.js +6 -0
- package/dist/communications/unsubscribeToken.d.ts +77 -0
- package/dist/communications/unsubscribeToken.js +150 -0
- package/dist/communications/validation.d.ts +2 -0
- package/dist/communications/validation.js +3 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/webhookSignature.d.ts +42 -0
- package/dist/http/webhookSignature.js +86 -0
- package/package.json +12 -2
- package/release-gates.json +41 -8
- package/sql/20260807190000_comms_outbox_rail.sql +329 -0
- package/sql/20260810120001_transactional_delivery_receipts.sql +209 -0
- package/sql/20260813120000_communications_control_plane.sql +568 -0
- package/sql/20261019200000_communications_native_operations.sql +68 -0
- package/src/communications/accountingDocumentDelivery.ts +63 -0
- package/src/communications/authEmailError.ts +10 -0
- package/src/communications/authEmailPorts.ts +2 -0
- package/src/communications/authSendEmailHook.ts +310 -0
- package/src/communications/capturedTransactionalDelivery.ts +29 -0
- package/src/communications/communicationSync.ts +96 -0
- package/src/communications/communicationSyncWorker.ts +302 -0
- package/src/communications/consentPolicy.ts +82 -0
- package/src/communications/consentPurposeRegistry.ts +31 -0
- package/src/communications/contracts/abandonedCart.ts +64 -0
- package/src/communications/contracts/authEmail.ts +73 -0
- package/src/communications/contracts/backInStock.ts +69 -0
- package/src/communications/contracts/blocks.ts +47 -0
- package/src/communications/contracts/checkoutExpired.ts +62 -0
- package/src/communications/contracts/checkoutRecovery.ts +68 -0
- package/src/communications/contracts/commerceEmailContent.ts +189 -0
- package/src/communications/contracts/commerceEngagementEmailContent.ts +106 -0
- package/src/communications/contracts/consent.ts +13 -0
- package/src/communications/contracts/email.ts +210 -0
- package/src/communications/contracts/guideCard.ts +34 -0
- package/src/communications/contracts/index.ts +51 -0
- package/src/communications/contracts/inquiryContent.ts +53 -0
- package/src/communications/contracts/invoiceDocumentContent.ts +57 -0
- package/src/communications/contracts/marketingAutomation.ts +45 -0
- package/src/communications/contracts/newsletter.ts +78 -0
- package/src/communications/contracts/newsletterWebhook.ts +23 -0
- package/src/communications/contracts/optOutContent.ts +23 -0
- package/src/communications/contracts/orderCanceled.ts +64 -0
- package/src/communications/contracts/orderDraft.ts +112 -0
- package/src/communications/contracts/orderPaid.ts +147 -0
- package/src/communications/contracts/orderRefunded.ts +63 -0
- package/src/communications/contracts/paymentFailed.ts +59 -0
- package/src/communications/contracts/publicIntake.ts +53 -0
- package/src/communications/contracts/render.ts +271 -0
- package/src/communications/contracts/reorderReminder.ts +59 -0
- package/src/communications/contracts/returnEmails.ts +113 -0
- package/src/communications/contracts/reviewEffects.ts +60 -0
- package/src/communications/contracts/reviewRequest.ts +59 -0
- package/src/communications/contracts/roleNotificationContent.ts +65 -0
- package/src/communications/contracts/shipmentDelivered.ts +59 -0
- package/src/communications/contracts/shipmentDispatched.ts +77 -0
- package/src/communications/contracts/shipmentException.ts +62 -0
- package/src/communications/contracts/subscriptionActivationActionRequired.ts +37 -0
- package/src/communications/contracts/subscriptionAddressChanged.ts +56 -0
- package/src/communications/contracts/subscriptionCancelled.ts +57 -0
- package/src/communications/contracts/subscriptionCycleSkipped.ts +80 -0
- package/src/communications/contracts/subscriptionDeliveryRescheduled.ts +79 -0
- package/src/communications/contracts/subscriptionEmailContent.ts +163 -0
- package/src/communications/contracts/subscriptionPackageChanged.ts +59 -0
- package/src/communications/contracts/subscriptionPauseReminder.ts +63 -0
- package/src/communications/contracts/subscriptionPaused.ts +58 -0
- package/src/communications/contracts/subscriptionPaymentExpired.ts +83 -0
- package/src/communications/contracts/subscriptionPaymentFailed.ts +117 -0
- package/src/communications/contracts/subscriptionPaymentRecovered.ts +50 -0
- package/src/communications/contracts/subscriptionRenewalAtRisk.ts +85 -0
- package/src/communications/contracts/subscriptionRenewalUpcoming.ts +103 -0
- package/src/communications/contracts/subscriptionResumed.ts +60 -0
- package/src/communications/contracts/subscriptionWelcome.ts +102 -0
- package/src/communications/contracts/subscriptionWinback.ts +62 -0
- package/src/communications/contracts/transactionalDelivery.ts +43 -0
- package/src/communications/contracts/types.ts +58 -0
- package/src/communications/contribution.ts +33 -0
- package/src/communications/deliveryReconciliation.ts +108 -0
- package/src/communications/emailEgress.ts +111 -0
- package/src/communications/emailHealthAssessment.ts +106 -0
- package/src/communications/emailHealthMetricsPort.ts +17 -0
- package/src/communications/emailNotificationControl.ts +28 -0
- package/src/communications/emailObservation.ts +43 -0
- package/src/communications/emailTransport.ts +167 -0
- package/src/communications/errors.ts +34 -0
- package/src/communications/index.ts +46 -0
- package/src/communications/integrationEventSignature.ts +49 -0
- package/src/communications/managedTransactionalDeliveryStore.ts +59 -0
- package/src/communications/nativeOperations.ts +40 -0
- package/src/communications/newsletterIntegrationContracts.ts +154 -0
- package/src/communications/newsletterWebhookPort.ts +104 -0
- package/src/communications/newsletterWebhookProcessing.ts +102 -0
- package/src/communications/originPolicy.ts +151 -0
- package/src/communications/postgresCommunicationSync.ts +127 -0
- package/src/communications/postgresConsentEvaluator.ts +49 -0
- package/src/communications/postgresDeliveryReconciliation.ts +101 -0
- package/src/communications/postgresNativeOperations.ts +55 -0
- package/src/communications/postgresNotificationRecipients.ts +57 -0
- package/src/communications/postgresTransactionalDeliveryStore.ts +79 -0
- package/src/communications/preferencesToken.ts +62 -0
- package/src/communications/publicPreferenceToken.ts +68 -0
- package/src/communications/shipmentNotifications.ts +78 -0
- package/src/communications/sql.ts +7 -0
- package/src/communications/standardWebhooks.ts +84 -0
- package/src/communications/status.ts +51 -0
- package/src/communications/transactionalDelivery.ts +114 -0
- package/src/communications/transactionalDeliveryIdentifier.ts +9 -0
- package/src/communications/unsubscribeToken.ts +186 -0
- package/src/communications/validation.ts +3 -0
- package/src/http/index.ts +1 -0
- package/src/http/webhookSignature.ts +96 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The sealed transport boundary for handing an accounting document to its recipient.
|
|
5
|
+
*
|
|
6
|
+
* The kernel exercises the whole capability up to this line: a document is owed, registered with
|
|
7
|
+
* whatever authority the market has, queued, claimed and settled against a receipt. What crosses
|
|
8
|
+
* the line is one opaque hand-off, and this adapter is the deliberately inert implementation of
|
|
9
|
+
* it -- deterministic, and leaving the installation by no route at all.
|
|
10
|
+
*
|
|
11
|
+
* It is FAIL CLOSED, and that is the whole design. An installation that has declared no delivery
|
|
12
|
+
* channel gets a refusal, never a silent success: a boundary whose default answer is "fine" is
|
|
13
|
+
* how a deployment discovers months later that nothing was ever handed to anyone.
|
|
14
|
+
*/
|
|
15
|
+
export type CapturedDocumentDeliveryCommand = {
|
|
16
|
+
readonly deliveryId: string;
|
|
17
|
+
readonly recipientReference: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CapturedDocumentDeliveryPort = {
|
|
21
|
+
deliver(command: CapturedDocumentDeliveryCommand): Promise<{ readonly receiptReference: string }>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type CapturedDocumentDeliveryResolution =
|
|
25
|
+
| { readonly port: CapturedDocumentDeliveryPort; readonly refused?: undefined }
|
|
26
|
+
| { readonly port?: undefined; readonly refused: string };
|
|
27
|
+
|
|
28
|
+
/** The one marker that opens the boundary. Anything else, including its absence, is a refusal. */
|
|
29
|
+
export const CAPTURED_DOCUMENT_DELIVERY_CHANNEL = "captured";
|
|
30
|
+
const CHANNEL_KEY = "PLATFORM_DOCUMENT_DELIVERY_CHANNEL";
|
|
31
|
+
|
|
32
|
+
const reference = (command: CapturedDocumentDeliveryCommand): string =>
|
|
33
|
+
`captured:${createHash("sha256")
|
|
34
|
+
.update(`${command.deliveryId}::${command.recipientReference}`)
|
|
35
|
+
.digest("hex")
|
|
36
|
+
.slice(0, 32)}`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the boundary for one installation. The environment is the only input, and every path
|
|
40
|
+
* that is not an explicit declaration of the captured channel ends in a named refusal.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveCapturedDocumentDelivery(
|
|
43
|
+
env: Record<string, string | undefined>,
|
|
44
|
+
): CapturedDocumentDeliveryResolution {
|
|
45
|
+
const declared = env[CHANNEL_KEY]?.trim().toLowerCase() ?? "";
|
|
46
|
+
if (declared === "") return { refused: "document_delivery_channel_undeclared" };
|
|
47
|
+
if (declared !== CAPTURED_DOCUMENT_DELIVERY_CHANNEL) {
|
|
48
|
+
return { refused: "document_delivery_channel_unsupported" };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
port: {
|
|
52
|
+
async deliver(command) {
|
|
53
|
+
if (typeof command.deliveryId !== "string" || command.deliveryId.trim() === "") {
|
|
54
|
+
throw new Error("document_delivery_command_invalid");
|
|
55
|
+
}
|
|
56
|
+
if (typeof command.recipientReference !== "string" || command.recipientReference.trim() === "") {
|
|
57
|
+
throw new Error("document_delivery_recipient_missing");
|
|
58
|
+
}
|
|
59
|
+
return { receiptReference: reference(command) };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Typed refusal used by public/default composition when product copy is absent. */
|
|
2
|
+
export class AuthEmailContentUnavailableError extends Error {
|
|
3
|
+
readonly code = "auth_email_content_unavailable";
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
super("Auth email content unavailable");
|
|
7
|
+
this.name = "AuthEmailContentUnavailableError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Auth Send Email hook. Live, not a candidate.
|
|
3
|
+
*
|
|
4
|
+
* Supabase Auth targets `/api/auth-send-email` in staging and production - the
|
|
5
|
+
* hooks were repointed here on 2026-09-04 and the hosted Edge Functions were
|
|
6
|
+
* undeployed the same day, so this owns the raw-stream, signature, render/send
|
|
7
|
+
* and ledger contract for every magic link, recovery and confirmation the
|
|
8
|
+
* product sends. Rotating its signing secret is a human-only control-plane act.
|
|
9
|
+
*/
|
|
10
|
+
import type { HttpRequest as VercelRequest, HttpResponse as VercelResponse } from "../http/index.js";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import { readEmailOriginConfiguration, resolveEmailOrigin, type EmailOriginPolicyError } from "./originPolicy.js";
|
|
14
|
+
import type { EmailBrand, EmailRenderer } from "./contracts/email.js";
|
|
15
|
+
import { buildAuthVerifyUrl } from "../identity/index.js";
|
|
16
|
+
import {
|
|
17
|
+
AuthEmailContentUnavailableError,
|
|
18
|
+
type AuthEmailContentPort,
|
|
19
|
+
type AuthEmailPersistencePort,
|
|
20
|
+
type AuthEmailTransportPort,
|
|
21
|
+
type AuthEmailTransportResult,
|
|
22
|
+
} from "./authEmailPorts.js";
|
|
23
|
+
import { guardAuthEmailTransport } from "./emailEgress.js";
|
|
24
|
+
import { verifyStandardWebhookSignature } from "./standardWebhooks.js";
|
|
25
|
+
|
|
26
|
+
const CORS_HEADERS = {
|
|
27
|
+
"access-control-allow-origin": "*",
|
|
28
|
+
"access-control-allow-headers": "authorization, x-client-info, apikey, content-type",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const KNOWN_ACTION_TYPES = new Set([
|
|
32
|
+
"magiclink", "signup", "recovery", "invite", "email_change",
|
|
33
|
+
"email_change_current", "email_change_new", "email",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
interface SendEmailHookPayload {
|
|
37
|
+
user?: { id?: string; email?: string; user_metadata?: Record<string, unknown> | null };
|
|
38
|
+
email_data?: {
|
|
39
|
+
token?: string;
|
|
40
|
+
token_hash?: string;
|
|
41
|
+
redirect_to?: string;
|
|
42
|
+
email_action_type?: string;
|
|
43
|
+
site_url?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AuthSendEmailHookDeps {
|
|
48
|
+
transport: AuthEmailTransportPort;
|
|
49
|
+
persistence: AuthEmailPersistencePort;
|
|
50
|
+
content: AuthEmailContentPort;
|
|
51
|
+
fromEmail: string;
|
|
52
|
+
hookSecret: string;
|
|
53
|
+
authServiceUrl: string;
|
|
54
|
+
/** App-layer defaults injected at composition; no brand/env ownership lives here. */
|
|
55
|
+
defaultOrigin: string;
|
|
56
|
+
productionEmailHosts: string[];
|
|
57
|
+
legacyBaseUrl?: string | null;
|
|
58
|
+
emailBrandForOrigin: (origin: string) => EmailBrand;
|
|
59
|
+
defaultSandboxRecipient: string;
|
|
60
|
+
defaultRedirectTo?: string;
|
|
61
|
+
env: Record<string, string | undefined>;
|
|
62
|
+
now?: () => number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AuthEmailHookHost {
|
|
66
|
+
renderEmail: EmailRenderer;
|
|
67
|
+
readObservedEnvironment: (env: Record<string, string | undefined>) => string;
|
|
68
|
+
readHiddenPreviewEnabled: (env: Record<string, string | undefined>) => boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type AuthEmailDeliveryMode = "standard" | "sandbox_recipient" | "production_like_real";
|
|
72
|
+
function json(res: VercelResponse, status: number, body: unknown): void {
|
|
73
|
+
for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
|
|
74
|
+
res.status(status).json(body);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hookError(res: VercelResponse, httpCode: number, message: string): void {
|
|
78
|
+
json(res, httpCode, { error: { http_code: httpCode, message } });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readRawBody(req: VercelRequest): Promise<string> {
|
|
82
|
+
const chunks: Buffer[] = [];
|
|
83
|
+
for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
84
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveRedirectTarget(
|
|
88
|
+
data: { redirect_to?: string; site_url?: string } | undefined,
|
|
89
|
+
defaultRedirectTo: string | undefined,
|
|
90
|
+
): { redirectTo: string; source: "redirect_to" | "site_url" | "default_redirect" } {
|
|
91
|
+
if (data?.redirect_to?.trim()) return { redirectTo: data.redirect_to, source: "redirect_to" };
|
|
92
|
+
if (data?.site_url?.trim()) return { redirectTo: data.site_url, source: "site_url" };
|
|
93
|
+
return { redirectTo: defaultRedirectTo ?? "", source: "default_redirect" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* An email carries no base URL, so a relative action link is not a degraded
|
|
98
|
+
* link - it is an unclickable one. The Edge runtime supplied this origin for
|
|
99
|
+
* free; a Node deployment must be configured with it, and one that is not must
|
|
100
|
+
* refuse rather than send mail whose only working path is the OTP fallback.
|
|
101
|
+
*/
|
|
102
|
+
function absoluteAuthServiceUrl(value: string): boolean {
|
|
103
|
+
try { const url = new URL(value); return url.protocol === "https:" || url.protocol === "http:"; }
|
|
104
|
+
catch { return false; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function uuidOrNull(value: string | undefined): string | null {
|
|
108
|
+
return value && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
|
109
|
+
? value
|
|
110
|
+
: null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function deliveryMode(
|
|
114
|
+
result: AuthEmailTransportResult,
|
|
115
|
+
env: Record<string, string | undefined>,
|
|
116
|
+
recipient: string,
|
|
117
|
+
defaultSandboxRecipient: string,
|
|
118
|
+
): AuthEmailDeliveryMode {
|
|
119
|
+
// Preserve the existing Auth-hook audit vocabulary even though the Node
|
|
120
|
+
// transport's lower-level egress seam uses a richer set of route names.
|
|
121
|
+
if (result.outcome.egressMode === "production") return "standard";
|
|
122
|
+
if ((env.EMAIL_DELIVERY_PROFILE ?? "").trim().toLowerCase() !== "production_like") return "standard";
|
|
123
|
+
const sandboxRecipients = parseEmailList(
|
|
124
|
+
env.EMAIL_SANDBOX_RECIPIENTS ?? env.COMMS_SANDBOX_SINK_ADDRESS ?? defaultSandboxRecipient,
|
|
125
|
+
);
|
|
126
|
+
return result.outcome.egressMode === "sandbox_sink"
|
|
127
|
+
|| (result.outcome.adminDisabled === true && sandboxRecipients.has(recipient.trim().toLowerCase()))
|
|
128
|
+
? "sandbox_recipient"
|
|
129
|
+
: "production_like_real";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseEmailList(value: string | undefined): Set<string> {
|
|
133
|
+
return new Set((value ?? "")
|
|
134
|
+
.split(/[,\s;]+/)
|
|
135
|
+
.map((item) => item.trim().toLowerCase())
|
|
136
|
+
.filter(Boolean));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function selectFromEmail(
|
|
140
|
+
deps: AuthSendEmailHookDeps,
|
|
141
|
+
recipient: string,
|
|
142
|
+
): string {
|
|
143
|
+
const env = deps.env;
|
|
144
|
+
if ((env.EMAIL_DELIVERY_PROFILE ?? "").trim().toLowerCase() !== "production_like") return deps.fromEmail;
|
|
145
|
+
const sandboxRecipients = parseEmailList(
|
|
146
|
+
env.EMAIL_SANDBOX_RECIPIENTS ?? env.COMMS_SANDBOX_SINK_ADDRESS ?? deps.defaultSandboxRecipient,
|
|
147
|
+
);
|
|
148
|
+
if (sandboxRecipients.has(recipient.trim().toLowerCase())) return deps.fromEmail;
|
|
149
|
+
return env.EMAIL_REAL_DELIVERY_FROM_EMAIL?.trim()
|
|
150
|
+
|| env.AUTH_EMAIL_REAL_DELIVERY_FROM_EMAIL?.trim()
|
|
151
|
+
|| deps.fromEmail;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validateAuthEmailRedirectOrigin(
|
|
155
|
+
deps: AuthSendEmailHookDeps,
|
|
156
|
+
redirectTo: string,
|
|
157
|
+
host: AuthEmailHookHost,
|
|
158
|
+
): { ok: true; environment: string; origin: string; production: boolean }
|
|
159
|
+
| { ok: false; error: EmailOriginPolicyError } {
|
|
160
|
+
const env = deps.env;
|
|
161
|
+
const originConfiguration = readEmailOriginConfiguration(env, {
|
|
162
|
+
defaultOrigin: deps.defaultOrigin,
|
|
163
|
+
productionEmailHosts: deps.productionEmailHosts,
|
|
164
|
+
});
|
|
165
|
+
const environment = env.EMAIL_ENVIRONMENT ?? host.readObservedEnvironment(env);
|
|
166
|
+
const configured = resolveEmailOrigin({
|
|
167
|
+
...originConfiguration,
|
|
168
|
+
explicitBaseUrl: env.APP_BASE_URL?.trim() || deps.legacyBaseUrl,
|
|
169
|
+
customerAuthRedirectOrigin: env.CUSTOMER_AUTH_REDIRECT_ORIGIN,
|
|
170
|
+
siteUrl: env.SITE_URL,
|
|
171
|
+
hiddenPreviewEnabled: host.readHiddenPreviewEnabled(env),
|
|
172
|
+
environment,
|
|
173
|
+
});
|
|
174
|
+
if (configured.ok === false) return { ok: false, error: configured.error };
|
|
175
|
+
const redirect = resolveEmailOrigin({
|
|
176
|
+
...originConfiguration,
|
|
177
|
+
explicitBaseUrl: redirectTo,
|
|
178
|
+
hiddenPreviewEnabled: host.readHiddenPreviewEnabled(env),
|
|
179
|
+
environment,
|
|
180
|
+
});
|
|
181
|
+
if (redirect.ok === false) return { ok: false, error: redirect.error };
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
environment: configured.resolved.environment,
|
|
185
|
+
origin: redirect.origin,
|
|
186
|
+
production: configured.resolved.production,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function persistenceOutcome(result: AuthEmailTransportResult) {
|
|
191
|
+
return {
|
|
192
|
+
ok: result.outcome.ok,
|
|
193
|
+
messageId: result.outcome.messageId,
|
|
194
|
+
providerError: result.outcome.providerError,
|
|
195
|
+
aborted: result.outcome.aborted,
|
|
196
|
+
skipReason: result.outcome.adminDisabled === true
|
|
197
|
+
? "admin_disabled" as const
|
|
198
|
+
: result.outcome.suppressed === "drop" ? "egress_suppressed" as const : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function createAuthSendEmailHookCandidate(deps: AuthSendEmailHookDeps, host: AuthEmailHookHost) {
|
|
203
|
+
return async function authSendEmailHookCandidate(req: VercelRequest, res: VercelResponse): Promise<void> {
|
|
204
|
+
if (req.method === "OPTIONS") {
|
|
205
|
+
for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
|
|
206
|
+
res.status(200).send("ok");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (req.method !== "POST") return hookError(res, 405, "Method not allowed");
|
|
210
|
+
if (!deps.hookSecret) {
|
|
211
|
+
console.error("[auth-send-email] SEND_EMAIL_HOOK_SECRET not configured");
|
|
212
|
+
return hookError(res, 500, "Hook secret not configured");
|
|
213
|
+
}
|
|
214
|
+
if (!absoluteAuthServiceUrl(deps.authServiceUrl)) {
|
|
215
|
+
console.error("[auth-send-email] auth service URL not configured");
|
|
216
|
+
return hookError(res, 500, "Auth service URL not configured");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let rawBody: string;
|
|
220
|
+
try { rawBody = await readRawBody(req); }
|
|
221
|
+
catch { return hookError(res, 400, "Invalid JSON payload"); }
|
|
222
|
+
const valid = await verifyStandardWebhookSignature({
|
|
223
|
+
headers: {
|
|
224
|
+
id: typeof req.headers["webhook-id"] === "string" ? req.headers["webhook-id"] : null,
|
|
225
|
+
timestamp: typeof req.headers["webhook-timestamp"] === "string" ? req.headers["webhook-timestamp"] : null,
|
|
226
|
+
signature: typeof req.headers["webhook-signature"] === "string" ? req.headers["webhook-signature"] : null,
|
|
227
|
+
},
|
|
228
|
+
rawBody,
|
|
229
|
+
secret: deps.hookSecret,
|
|
230
|
+
now: deps.now,
|
|
231
|
+
});
|
|
232
|
+
if (!valid) return hookError(res, 401, "Invalid signature");
|
|
233
|
+
|
|
234
|
+
let payload: SendEmailHookPayload;
|
|
235
|
+
try { payload = JSON.parse(rawBody) as SendEmailHookPayload; }
|
|
236
|
+
catch { return hookError(res, 400, "Invalid JSON payload"); }
|
|
237
|
+
|
|
238
|
+
const email = payload.user?.email;
|
|
239
|
+
const data = payload.email_data;
|
|
240
|
+
const actionType = data?.email_action_type;
|
|
241
|
+
const tokenHash = data?.token_hash;
|
|
242
|
+
const redirect = resolveRedirectTarget(data, deps.defaultRedirectTo);
|
|
243
|
+
if (!email || !actionType || !tokenHash) return hookError(res, 400, "Missing user email or email_data");
|
|
244
|
+
const redirectPolicy = validateAuthEmailRedirectOrigin(deps, redirect.redirectTo, host);
|
|
245
|
+
if (redirectPolicy.ok === false) return hookError(res, 500, `Email origin policy failed: ${redirectPolicy.error}`);
|
|
246
|
+
if (!KNOWN_ACTION_TYPES.has(actionType)) console.warn("[auth-send-email] unknown email_action_type:", actionType);
|
|
247
|
+
|
|
248
|
+
let content: ReturnType<AuthEmailContentPort["buildAuthEmailContent"]>;
|
|
249
|
+
try { content = deps.content.buildAuthEmailContent({
|
|
250
|
+
actionType, userMetadata: payload.user?.user_metadata, redirectTo: redirect.redirectTo,
|
|
251
|
+
actionLink: buildAuthVerifyUrl(deps.authServiceUrl, actionType, tokenHash, redirect.redirectTo),
|
|
252
|
+
otp: data?.token ?? null,
|
|
253
|
+
});
|
|
254
|
+
} catch (error) { if (error instanceof AuthEmailContentUnavailableError) return hookError(res, 503, error.message);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
const rendered = host.renderEmail({
|
|
258
|
+
brand: deps.emailBrandForOrigin(redirectPolicy.origin), locale: content.locale,
|
|
259
|
+
subject: content.subject, preheader: content.preheader, blocks: content.blocks,
|
|
260
|
+
});
|
|
261
|
+
const sendAttemptId = crypto.randomUUID();
|
|
262
|
+
const authUserId = uuidOrNull(payload.user?.id);
|
|
263
|
+
const aggregateId = authUserId ?? crypto.randomUUID();
|
|
264
|
+
// Match the hosted hook's ordering: persistence/control failure is a
|
|
265
|
+
// retryable refusal before any email side effect, never a post-send false 5xx.
|
|
266
|
+
const templateSlug = `auth-${actionType}`;
|
|
267
|
+
const enabled = await deps.persistence.isTemplateEnabled(templateSlug, new AbortController().signal);
|
|
268
|
+
const sendResult = enabled
|
|
269
|
+
? await guardAuthEmailTransport(deps.transport).send({
|
|
270
|
+
sender: selectFromEmail(deps, email), recipient: email,
|
|
271
|
+
subject: rendered.subject, html: rendered.html, text: rendered.text,
|
|
272
|
+
})
|
|
273
|
+
: {
|
|
274
|
+
outcome: { ok: true, messageId: null, providerError: null, aborted: false, adminDisabled: true },
|
|
275
|
+
providerResponse: { skipped: "admin_disabled" },
|
|
276
|
+
};
|
|
277
|
+
const mode = deliveryMode(sendResult, deps.env, email, deps.defaultSandboxRecipient);
|
|
278
|
+
const originMetadata = {
|
|
279
|
+
emailBaseUrl: redirectPolicy.origin,
|
|
280
|
+
emailOriginSource: redirect.source,
|
|
281
|
+
emailEnvironment: redirectPolicy.environment,
|
|
282
|
+
};
|
|
283
|
+
await deps.persistence.recordSend({
|
|
284
|
+
templateSlug,
|
|
285
|
+
recipientEmail: email,
|
|
286
|
+
authUserId,
|
|
287
|
+
aggregateId,
|
|
288
|
+
dedupeKey: `auth-send-email:${actionType}:${sendAttemptId}`,
|
|
289
|
+
providerKind: deps.transport.providerKind,
|
|
290
|
+
outcome: persistenceOutcome(sendResult),
|
|
291
|
+
providerResponse: sendResult.providerResponse,
|
|
292
|
+
metadata: {
|
|
293
|
+
...originMetadata,
|
|
294
|
+
authEmailDeliveryMode: mode,
|
|
295
|
+
deliveryMode: mode,
|
|
296
|
+
triggerSource: "auth-send-email",
|
|
297
|
+
triggerReason: actionType,
|
|
298
|
+
providerKind: deps.transport.providerKind,
|
|
299
|
+
sendAttemptId,
|
|
300
|
+
aggregateType: "auth_user",
|
|
301
|
+
aggregateId,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
if (!sendResult.outcome.ok) {
|
|
305
|
+
console.error("[auth-send-email] transport send failed:", sendResult.outcome.providerError);
|
|
306
|
+
return hookError(res, 502, "Email delivery failed");
|
|
307
|
+
}
|
|
308
|
+
json(res, 200, {});
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CapturedTransactionalDeliveryPort,
|
|
3
|
+
TransactionalDeliveryCommand,
|
|
4
|
+
} from "./contracts/transactionalDelivery.js";
|
|
5
|
+
|
|
6
|
+
export type CapturedTransactionalDeliveryOutcome =
|
|
7
|
+
| { readonly state: "accepted"; readonly deliveryReference?: string }
|
|
8
|
+
| { readonly state: "failed" };
|
|
9
|
+
|
|
10
|
+
/** Injected only by tests and the parity proof to demonstrate durable recovery. */
|
|
11
|
+
export type CapturedTransactionalDeliveryOutcomeSource = (
|
|
12
|
+
command: TransactionalDeliveryCommand,
|
|
13
|
+
) => Promise<CapturedTransactionalDeliveryOutcome> | CapturedTransactionalDeliveryOutcome;
|
|
14
|
+
|
|
15
|
+
/** Deterministic no-egress delivery adapter for the portable direct action. */
|
|
16
|
+
export function createCapturedTransactionalDelivery(
|
|
17
|
+
options: { outcome?: CapturedTransactionalDeliveryOutcomeSource } = {},
|
|
18
|
+
): CapturedTransactionalDeliveryPort {
|
|
19
|
+
return {
|
|
20
|
+
async deliver(command) {
|
|
21
|
+
const outcome = await (options.outcome?.(command) ?? { state: "accepted" as const });
|
|
22
|
+
if (outcome.state === "failed") throw new Error("captured_delivery_failed");
|
|
23
|
+
const deliveryReference = outcome.deliveryReference
|
|
24
|
+
?? `captured:${createHash("sha256").update(command.idempotencyKey).digest("hex")}`;
|
|
25
|
+
return { deliveryReference };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { NewsletterSyncEvent } from "./contracts/newsletter.js";
|
|
2
|
+
import type { CommunicationSyncStore } from "./communicationSyncWorker.js";
|
|
3
|
+
|
|
4
|
+
export interface CommunicationSyncRpcClient {
|
|
5
|
+
rpc(
|
|
6
|
+
functionName: string,
|
|
7
|
+
args: Record<string, unknown>,
|
|
8
|
+
): PromiseLike<{ data: unknown; error: { code?: string; message?: string } | null }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function rpcFailure(rpcName: string, error: { code?: string; message?: string }): Error {
|
|
12
|
+
return new Error(`${rpcName}_failed: ${error.message ?? error.code ?? "unknown"}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createSupabaseCommunicationSyncStore(
|
|
16
|
+
client: CommunicationSyncRpcClient,
|
|
17
|
+
): CommunicationSyncStore {
|
|
18
|
+
return {
|
|
19
|
+
async claimBatch(input): Promise<NewsletterSyncEvent[]> {
|
|
20
|
+
const { data, error } = await client.rpc("communication_claim_sync_outbox", {
|
|
21
|
+
p_provider_kind: input.providerKind,
|
|
22
|
+
p_batch_size: input.batchSize,
|
|
23
|
+
p_visibility_seconds: input.visibilitySeconds,
|
|
24
|
+
p_max_attempts: input.maxAttempts,
|
|
25
|
+
});
|
|
26
|
+
if (error) throw rpcFailure("communication_claim_sync_outbox", error);
|
|
27
|
+
return Array.isArray(data) ? data.map(mapClaimedEvent).filter(isEvent) : [];
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async markSent(input): Promise<boolean> {
|
|
31
|
+
const { data, error } = await client.rpc("communication_mark_sync_outbox_sent", {
|
|
32
|
+
p_outbox_id: input.eventId,
|
|
33
|
+
p_claim_token: input.claimToken,
|
|
34
|
+
p_provider_kind: input.providerKind,
|
|
35
|
+
p_remote_profile_id: input.remoteProfileId ?? null,
|
|
36
|
+
p_response_summary: input.responseSummary ?? {},
|
|
37
|
+
});
|
|
38
|
+
if (error) throw rpcFailure("communication_mark_sync_outbox_sent", error);
|
|
39
|
+
return data === true;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async markFailed(input): Promise<boolean> {
|
|
43
|
+
const { data, error } = await client.rpc("communication_mark_sync_outbox_failed", {
|
|
44
|
+
p_outbox_id: input.eventId,
|
|
45
|
+
p_claim_token: input.claimToken,
|
|
46
|
+
p_provider_kind: input.providerKind,
|
|
47
|
+
p_error: input.error,
|
|
48
|
+
p_retry: input.retry,
|
|
49
|
+
p_backoff_seconds: input.backoffSeconds,
|
|
50
|
+
});
|
|
51
|
+
if (error) throw rpcFailure("communication_mark_sync_outbox_failed", error);
|
|
52
|
+
return data === true;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mapClaimedEvent(row: unknown): NewsletterSyncEvent | null {
|
|
58
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return null;
|
|
59
|
+
const record = row as Record<string, unknown>;
|
|
60
|
+
if (
|
|
61
|
+
typeof record.id !== "string" ||
|
|
62
|
+
typeof record.claim_token !== "string" ||
|
|
63
|
+
typeof record.contact_id !== "string" ||
|
|
64
|
+
typeof record.normalized_email !== "string" ||
|
|
65
|
+
typeof record.event_type !== "string" ||
|
|
66
|
+
typeof record.purpose !== "string"
|
|
67
|
+
) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
id: record.id,
|
|
72
|
+
claimToken: record.claim_token,
|
|
73
|
+
contactId: record.contact_id,
|
|
74
|
+
normalizedEmail: record.normalized_email,
|
|
75
|
+
eventType: record.event_type as NewsletterSyncEvent["eventType"],
|
|
76
|
+
purpose: record.purpose as NewsletterSyncEvent["purpose"],
|
|
77
|
+
payload: isRecord(record.payload) ? record.payload : {},
|
|
78
|
+
attemptCount: typeof record.attempt_count === "number" ? record.attempt_count : 0,
|
|
79
|
+
originProviderKind: readNullableString(record.origin_provider_kind),
|
|
80
|
+
originProviderEventId: readNullableString(record.origin_provider_event_id),
|
|
81
|
+
remoteProfileId: readNullableString(record.remote_profile_id),
|
|
82
|
+
remoteListId: readNullableString(record.remote_list_id),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isEvent(event: NewsletterSyncEvent | null): event is NewsletterSyncEvent {
|
|
87
|
+
return Boolean(event);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
91
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readNullableString(value: unknown): string | null {
|
|
95
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
96
|
+
}
|