@cosmicdrift/kumiko-samples 0.392.2 → 0.394.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/package.json +1 -1
- package/packages/bundled-features/package.json +15 -5
- package/packages/bundled-features/src/account-security/README.md +1 -1
- package/packages/bundled-features/src/account-security/changes.json +6 -0
- package/packages/bundled-features/src/account-security/feature.ts +9 -1
- package/packages/bundled-features/src/audit/event-classification.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/changes.json +6 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +10 -52
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -4
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +3 -12
- package/packages/bundled-features/src/auth-email-password/web/index.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/login-failure.ts +54 -0
- package/packages/bundled-features/src/auth-email-password/web/login-url.ts +19 -0
- package/packages/bundled-features/src/auth-email-password/web/static-login.ts +183 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-client.ts +7 -63
- package/packages/bundled-features/src/billing-foundation/changes.json +30 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +8 -16
- package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/one-off-checkout-gate.ts +44 -0
- package/packages/bundled-features/src/billing-foundation/one-off-customer.ts +17 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +26 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +8 -2
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-recurring.ts +23 -0
- package/packages/bundled-features/src/billing-foundation/webhook-handler.ts +1 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +3 -3
- package/packages/bundled-features/src/cap-overview/types.ts +2 -0
- package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx +10 -1
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +12 -3
- package/packages/bundled-features/src/files/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-health.ts +146 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll-schedule.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-poll.ts +238 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-record.ts +2 -0
- package/packages/bundled-features/src/inbound-mail-foundation/account-wake.ts +21 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +155 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-callback-verify.ts +65 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-route-responses.ts +26 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +93 -62
- package/packages/bundled-features/src/inbound-mail-foundation/constants.ts +66 -3
- package/packages/bundled-features/src/inbound-mail-foundation/content-refetch.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/describe-error.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/entities.ts +45 -2
- package/packages/bundled-features/src/inbound-mail-foundation/events.ts +69 -2
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +133 -118
- package/packages/bundled-features/src/inbound-mail-foundation/file-storage-failure.ts +23 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/account-address.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/acknowledge-quarantine.write.ts +55 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/attach-message-content.write.ts +184 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +137 -84
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/disconnect-account.write.ts +65 -45
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-prepare.ts +177 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message-schema.ts +42 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +79 -160
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +93 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +46 -30
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-quarantine.query.ts +106 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/mark-credential-stored.write.ts +47 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.query.ts +99 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-content.ts +229 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/message-detail.query.ts +34 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/reactivate-account.write.ts +129 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/retry-quarantined-message.write.ts +49 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +48 -9
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/set-trusted-authserv-id.write.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +136 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/trusted-authserv-id.ts +4 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +82 -60
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +80 -1
- package/packages/bundled-features/src/inbound-mail-foundation/inbound-oauth-flow.ts +48 -0
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +94 -15
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-failures.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/ingest-payload.ts +46 -0
- package/packages/bundled-features/src/inbound-mail-foundation/mail-roles.ts +132 -0
- package/packages/bundled-features/src/inbound-mail-foundation/normalize-inbound-message.ts +119 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +26 -8
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachment-access.ts +28 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-attachments.ts +111 -0
- package/packages/bundled-features/src/inbound-mail-foundation/outbound-mail.ts +19 -1
- package/packages/bundled-features/src/inbound-mail-foundation/probe.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +33 -2
- package/packages/bundled-features/src/inbound-mail-foundation/provider-errors.ts +74 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-entities.ts +56 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-header-import.ts +63 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-retry.ts +137 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine-screens.ts +66 -0
- package/packages/bundled-features/src/inbound-mail-foundation/quarantine.ts +72 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +78 -0
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +43 -8
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +239 -79
- package/packages/bundled-features/src/inbound-mail-foundation/send-from-account.ts +24 -5
- package/packages/bundled-features/src/inbound-mail-foundation/sender-auth.ts +215 -0
- package/packages/bundled-features/src/inbound-mail-foundation/supervisor-ingest.ts +311 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-account-now.ts +61 -0
- package/packages/bundled-features/src/inbound-mail-foundation/sync-cursor-store.ts +79 -0
- package/packages/bundled-features/src/inbound-mail-foundation/syncable-accounts.ts +123 -0
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +76 -105
- package/packages/bundled-features/src/inbound-mail-foundation/wake-consumer.ts +33 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-catch-up.ts +68 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-lease.ts +124 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor-types.ts +87 -0
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +143 -433
- package/packages/bundled-features/src/inbound-mail-foundation/web/client-plugin.ts +73 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/constants.ts +7 -0
- package/packages/bundled-features/src/inbound-mail-foundation/web/index.ts +5 -0
- package/packages/bundled-features/src/inbound-provider-imap/changes.json +42 -0
- package/packages/bundled-features/src/inbound-provider-imap/connect-mailbox.ts +155 -63
- package/packages/bundled-features/src/inbound-provider-imap/connect-my-mailbox.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/constants.ts +14 -1
- package/packages/bundled-features/src/inbound-provider-imap/credential-document.ts +2 -1
- package/packages/bundled-features/src/inbound-provider-imap/download-batch.ts +38 -0
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +75 -44
- package/packages/bundled-features/src/inbound-provider-imap/fetch-content.ts +84 -0
- package/packages/bundled-features/src/inbound-provider-imap/i18n.ts +28 -2
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +35 -84
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +1 -2
- package/packages/bundled-features/src/inbound-provider-imap/mime-content.ts +85 -0
- package/packages/bundled-features/src/inbound-provider-imap/parse-message.ts +149 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-account.ts +45 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-mailbox.ts +103 -0
- package/packages/bundled-features/src/inbound-provider-imap/reconnect-prefill.query.ts +49 -0
- package/packages/bundled-features/src/inbound-provider-imap/screens.ts +209 -42
- package/packages/bundled-features/src/inbound-provider-imap/send-mail.ts +11 -2
- package/packages/bundled-features/src/inbound-provider-imap/sender-auth-headers.ts +36 -0
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +75 -3
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +6 -0
- package/packages/bundled-features/src/jobs/changes.json +18 -0
- package/packages/bundled-features/src/managed-pages/web/i18n.ts +1 -0
- package/packages/bundled-features/src/subscription-paddle/changes.json +15 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-config.query.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-methods.ts +107 -0
- package/packages/bundled-features/src/subscription-paddle/checkout-page.ts +154 -0
- package/packages/bundled-features/src/subscription-paddle/client.ts +78 -0
- package/packages/bundled-features/src/subscription-paddle/constants.ts +32 -0
- package/packages/bundled-features/src/subscription-paddle/feature.ts +265 -0
- package/packages/bundled-features/src/subscription-paddle/index.ts +17 -0
- package/packages/bundled-features/src/subscription-paddle/plan-switch-methods.ts +135 -0
- package/packages/bundled-features/src/subscription-paddle/price-methods.ts +148 -0
- package/packages/bundled-features/src/subscription-paddle/runtime.ts +203 -0
- package/packages/bundled-features/src/subscription-paddle/subscription-state.ts +73 -0
- package/packages/bundled-features/src/subscription-paddle/tenant-binding.ts +34 -0
- package/packages/bundled-features/src/subscription-paddle/verify-webhook.ts +61 -0
- package/packages/bundled-features/src/subscription-paddle/webhook-events.ts +172 -0
- package/packages/bundled-features/src/template-resolver/changes.json +12 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +6 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +25 -0
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +218 -117
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +1 -2
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +2 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +4 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +11 -5
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +89 -39
- package/samples/apps/use-all-bundled/kumiko/migrations/0056_inbound-mail-quarantine-retry.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/playwright.billing-docs.config.ts +15 -0
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +16 -2
- package/samples/apps/use-all-bundled/src/app/cap-overview-labels-feature.ts +2 -1
- package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +14 -2
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +7 -5
- package/samples/recipes/cross-feature-events/README.md +10 -0
- package/samples/recipes/cross-feature-events/src/feature.ts +14 -0
- package/samples/recipes/dashboard-stats/README.md +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +91 -1
- package/samples/recipes/env-composition/README.md +2 -2
- package/samples/recipes/env-composition/src/env.ts +2 -2
- package/samples/recipes/inbound-mail-triage/README.md +12 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +71 -48
- package/packages/bundled-features/src/subscription-dodo/changes.json +0 -14
- package/packages/bundled-features/src/subscription-dodo/client.ts +0 -45
- package/packages/bundled-features/src/subscription-dodo/constants.ts +0 -24
- package/packages/bundled-features/src/subscription-dodo/feature.ts +0 -207
- package/packages/bundled-features/src/subscription-dodo/index.ts +0 -16
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +0 -345
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +0 -126
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +0 -262
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
// Dodo Payments webhook: Standard Webhooks signature verification plus mapping
|
|
2
|
-
// of Dodo's event envelope onto the foundation's normalized events.
|
|
3
|
-
//
|
|
4
|
-
// Verification always runs against the raw body string; re-serialized JSON
|
|
5
|
-
// would change whitespace and break the HMAC. The signature check happens
|
|
6
|
-
// before any parsing, so no path bypasses it.
|
|
7
|
-
|
|
8
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
9
|
-
import {
|
|
10
|
-
BillingEventKinds,
|
|
11
|
-
type PaymentEvent,
|
|
12
|
-
parseProviderConsentId,
|
|
13
|
-
type SubscriptionEvent,
|
|
14
|
-
type SubscriptionEventType,
|
|
15
|
-
SubscriptionEventTypes,
|
|
16
|
-
type SubscriptionStatus,
|
|
17
|
-
SubscriptionStatuses,
|
|
18
|
-
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
19
|
-
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
20
|
-
import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
21
|
-
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
22
|
-
import * as z from "zod";
|
|
23
|
-
import { DODO_PROVIDER_NAME } from "./constants.ts";
|
|
24
|
-
import type { DodoWebhookRuntime } from "./runtime.ts";
|
|
25
|
-
|
|
26
|
-
export const WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
27
|
-
|
|
28
|
-
const SECRET_PREFIX = "whsec_";
|
|
29
|
-
const SIGNATURE_VERSION = "v1";
|
|
30
|
-
const TIMESTAMP_PATTERN = /^\d+$/;
|
|
31
|
-
|
|
32
|
-
export type StandardWebhookHeaders = Readonly<Record<string, string>>;
|
|
33
|
-
|
|
34
|
-
function signatureFor(secretKey: Buffer, signedContent: string): Buffer {
|
|
35
|
-
return createHmac("sha256", secretKey).update(signedContent).digest();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function matchesAnyV1Signature(header: string, expected: Buffer): boolean {
|
|
39
|
-
return header.split(" ").some((entry) => {
|
|
40
|
-
const [version, encoded] = entry.split(",");
|
|
41
|
-
if (version !== SIGNATURE_VERSION || !encoded) return false;
|
|
42
|
-
const candidate = Buffer.from(encoded, "base64");
|
|
43
|
-
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Throws unless the Standard Webhooks signature over `rawBody` is valid and
|
|
48
|
-
* the timestamp is within the tolerance in both directions. */
|
|
49
|
-
export function verifyStandardWebhook(
|
|
50
|
-
rawBody: string,
|
|
51
|
-
headers: StandardWebhookHeaders,
|
|
52
|
-
secret: string,
|
|
53
|
-
nowSeconds: number,
|
|
54
|
-
): void {
|
|
55
|
-
const id = headers["webhook-id"];
|
|
56
|
-
const timestamp = headers["webhook-timestamp"];
|
|
57
|
-
const signatureHeader = headers["webhook-signature"];
|
|
58
|
-
if (!id || !timestamp || !signatureHeader) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
"subscription-dodo: webhook-id, webhook-timestamp or webhook-signature missing",
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
if (!TIMESTAMP_PATTERN.test(timestamp)) {
|
|
64
|
-
throw new Error("subscription-dodo: webhook-timestamp is not a Unix timestamp in seconds");
|
|
65
|
-
}
|
|
66
|
-
if (Math.abs(nowSeconds - Number(timestamp)) > WEBHOOK_TOLERANCE_SECONDS) {
|
|
67
|
-
throw new Error("subscription-dodo: webhook timestamp outside the tolerance window");
|
|
68
|
-
}
|
|
69
|
-
const secretKey = Buffer.from(
|
|
70
|
-
secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret,
|
|
71
|
-
"base64",
|
|
72
|
-
);
|
|
73
|
-
const expected = signatureFor(secretKey, `${id}.${timestamp}.${rawBody}`);
|
|
74
|
-
if (!matchesAnyV1Signature(signatureHeader, expected)) {
|
|
75
|
-
throw new Error("subscription-dodo: webhook signature verify failed");
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// =============================================================================
|
|
80
|
-
// Event mapping
|
|
81
|
-
// =============================================================================
|
|
82
|
-
|
|
83
|
-
const DODO_SUBSCRIPTION_EVENT_TYPES: Readonly<Record<string, SubscriptionEventType>> = {
|
|
84
|
-
"subscription.active": SubscriptionEventTypes.created,
|
|
85
|
-
"subscription.updated": SubscriptionEventTypes.updated,
|
|
86
|
-
"subscription.plan_changed": SubscriptionEventTypes.updated,
|
|
87
|
-
"subscription.paused": SubscriptionEventTypes.updated,
|
|
88
|
-
"subscription.unpaused": SubscriptionEventTypes.updated,
|
|
89
|
-
"subscription.update_payment_method": SubscriptionEventTypes.updated,
|
|
90
|
-
"subscription.renewed": SubscriptionEventTypes.invoicePaid,
|
|
91
|
-
"subscription.past_due": SubscriptionEventTypes.invoicePaymentFailed,
|
|
92
|
-
"subscription.on_hold": SubscriptionEventTypes.invoicePaymentFailed,
|
|
93
|
-
"subscription.cancelled": SubscriptionEventTypes.canceled,
|
|
94
|
-
"subscription.expired": SubscriptionEventTypes.canceled,
|
|
95
|
-
"subscription.failed": SubscriptionEventTypes.canceled,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const DODO_SUBSCRIPTION_STATUSES: Readonly<Record<string, SubscriptionStatus>> = {
|
|
99
|
-
pending: SubscriptionStatuses.incomplete,
|
|
100
|
-
active: SubscriptionStatuses.active,
|
|
101
|
-
on_hold: SubscriptionStatuses.pastDue,
|
|
102
|
-
past_due: SubscriptionStatuses.pastDue,
|
|
103
|
-
paused: SubscriptionStatuses.pastDue,
|
|
104
|
-
cancelled: SubscriptionStatuses.canceled,
|
|
105
|
-
failed: SubscriptionStatuses.canceled,
|
|
106
|
-
expired: SubscriptionStatuses.canceled,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const PAYMENT_SUCCEEDED = "payment.succeeded";
|
|
110
|
-
|
|
111
|
-
const metadataSchema = z.record(z.string(), z.unknown()).nullish();
|
|
112
|
-
const customerSchema = z.object({ customer_id: z.string().min(1) });
|
|
113
|
-
|
|
114
|
-
const envelopeSchema = z.object({
|
|
115
|
-
type: z.string(),
|
|
116
|
-
timestamp: z.iso.datetime({ offset: true }),
|
|
117
|
-
data: z.unknown(),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
export const dodoSubscriptionSchema = z.object({
|
|
121
|
-
subscription_id: z.string().min(1),
|
|
122
|
-
product_id: z.string().min(1),
|
|
123
|
-
status: z.string(),
|
|
124
|
-
customer: customerSchema,
|
|
125
|
-
metadata: metadataSchema,
|
|
126
|
-
next_billing_date: z.iso.datetime({ offset: true }),
|
|
127
|
-
cancel_at_next_billing_date: z.boolean().nullish(),
|
|
128
|
-
});
|
|
129
|
-
export type DodoSubscription = z.infer<typeof dodoSubscriptionSchema>;
|
|
130
|
-
|
|
131
|
-
const dodoPaymentSchema = z.object({
|
|
132
|
-
subscription_id: z.string().nullish(),
|
|
133
|
-
checkout_session_id: z.string().nullish(),
|
|
134
|
-
customer: customerSchema,
|
|
135
|
-
metadata: metadataSchema,
|
|
136
|
-
product_cart: z.array(z.object({ product_id: z.string().min(1) })).nullish(),
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
function metadataString(metadata: DodoSubscription["metadata"], key: string): string | undefined {
|
|
140
|
-
const value = metadata?.[key];
|
|
141
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export type MappedDodoSubscriptionState = {
|
|
145
|
-
readonly status: SubscriptionStatus;
|
|
146
|
-
readonly tier: string;
|
|
147
|
-
readonly currentPeriodEnd: string;
|
|
148
|
-
readonly cancelAt: string | null;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
/** Shared by the webhook and retrieveSubscription so both map Dodo state
|
|
152
|
-
* identically. Null when the status is unknown or the product is not in
|
|
153
|
-
* `priceToTier` (SubscriptionEvent.tier is required). */
|
|
154
|
-
export function mapDodoSubscriptionState(
|
|
155
|
-
sub: DodoSubscription,
|
|
156
|
-
priceToTier: Readonly<Record<string, string>>,
|
|
157
|
-
): MappedDodoSubscriptionState | null {
|
|
158
|
-
const status = DODO_SUBSCRIPTION_STATUSES[sub.status];
|
|
159
|
-
const tier = priceToTier[sub.product_id];
|
|
160
|
-
if (!status || !tier) return null;
|
|
161
|
-
return {
|
|
162
|
-
status,
|
|
163
|
-
tier,
|
|
164
|
-
currentPeriodEnd: sub.next_billing_date,
|
|
165
|
-
cancelAt: sub.cancel_at_next_billing_date === true ? sub.next_billing_date : null,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export type DodoWebhookOptions = {
|
|
170
|
-
readonly priceToTier: Readonly<Record<string, string>>;
|
|
171
|
-
/** Test seam: Unix seconds. Defaults to the wall clock. */
|
|
172
|
-
readonly nowSeconds?: () => number;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
function currentUnixSeconds(): number {
|
|
176
|
-
return Math.floor(Temporal.Now.instant().epochMilliseconds / 1000);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function parsePaymentEvent(providerEventId: string, data: unknown): PaymentEvent | null {
|
|
180
|
-
const parsed = dodoPaymentSchema.safeParse(data);
|
|
181
|
-
if (!parsed.success) return null;
|
|
182
|
-
const payment = parsed.data;
|
|
183
|
-
// A subscription's own payments are covered by its subscription.* events.
|
|
184
|
-
if (payment.subscription_id) return null;
|
|
185
|
-
const tenantId = metadataString(payment.metadata, "tenantId");
|
|
186
|
-
const priceId = payment.product_cart?.[0]?.product_id;
|
|
187
|
-
if (!tenantId || !priceId) return null;
|
|
188
|
-
const consentId = parseProviderConsentId(payment.metadata?.["consentId"]);
|
|
189
|
-
return {
|
|
190
|
-
kind: BillingEventKinds.payment,
|
|
191
|
-
providerEventId,
|
|
192
|
-
providerName: DODO_PROVIDER_NAME,
|
|
193
|
-
tenantId,
|
|
194
|
-
...(consentId !== undefined && { consentId }),
|
|
195
|
-
...(payment.checkout_session_id && { providerCheckoutId: payment.checkout_session_id }),
|
|
196
|
-
providerCustomerId: payment.customer.customer_id,
|
|
197
|
-
priceId,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function parseSubscriptionEvent(
|
|
202
|
-
providerEventId: string,
|
|
203
|
-
type: SubscriptionEventType,
|
|
204
|
-
providerEventAt: string,
|
|
205
|
-
data: unknown,
|
|
206
|
-
priceToTier: Readonly<Record<string, string>>,
|
|
207
|
-
): SubscriptionEvent | null {
|
|
208
|
-
const parsed = dodoSubscriptionSchema.safeParse(data);
|
|
209
|
-
if (!parsed.success) return null;
|
|
210
|
-
const sub = parsed.data;
|
|
211
|
-
const tenantId = metadataString(sub.metadata, "tenantId");
|
|
212
|
-
const state = mapDodoSubscriptionState(sub, priceToTier);
|
|
213
|
-
if (!tenantId || !state) return null;
|
|
214
|
-
const consentId = parseProviderConsentId(sub.metadata?.["consentId"]);
|
|
215
|
-
return {
|
|
216
|
-
providerEventId,
|
|
217
|
-
providerName: DODO_PROVIDER_NAME,
|
|
218
|
-
type,
|
|
219
|
-
tenantId,
|
|
220
|
-
...(consentId !== undefined && { consentId }),
|
|
221
|
-
providerCustomerId: sub.customer.customer_id,
|
|
222
|
-
providerSubscriptionId: sub.subscription_id,
|
|
223
|
-
providerEventAt,
|
|
224
|
-
...state,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function verifyAndParseDodoWebhook(
|
|
229
|
-
runtime: DodoWebhookRuntime,
|
|
230
|
-
options: DodoWebhookOptions,
|
|
231
|
-
): (
|
|
232
|
-
rawBody: string,
|
|
233
|
-
headers: Record<string, string>,
|
|
234
|
-
systemSecrets?: SecretsContext,
|
|
235
|
-
) => Promise<SubscriptionEvent | PaymentEvent | null> {
|
|
236
|
-
const nowSeconds = options.nowSeconds ?? currentUnixSeconds;
|
|
237
|
-
return async (rawBody, headers, systemSecrets) => {
|
|
238
|
-
const secret = await runtime.resolveWebhookSecret(systemSecrets);
|
|
239
|
-
verifyStandardWebhook(rawBody, headers, secret, nowSeconds());
|
|
240
|
-
|
|
241
|
-
// Past verification the body is trusted to be Dodo's; a malformed shape is
|
|
242
|
-
// schema drift, so it is dropped (200 ignored) instead of answered with a 401.
|
|
243
|
-
const envelope = envelopeSchema.safeParse(
|
|
244
|
-
parseJsonOrThrow<unknown>(rawBody, "subscription-dodo webhook body"),
|
|
245
|
-
);
|
|
246
|
-
// verifyStandardWebhook has checked webhook-id is present.
|
|
247
|
-
const providerEventId = headers["webhook-id"];
|
|
248
|
-
if (!envelope.success || !providerEventId) return null;
|
|
249
|
-
|
|
250
|
-
const { type, timestamp, data } = envelope.data;
|
|
251
|
-
if (type === PAYMENT_SUCCEEDED) return parsePaymentEvent(providerEventId, data);
|
|
252
|
-
const normalizedType = DODO_SUBSCRIPTION_EVENT_TYPES[type];
|
|
253
|
-
if (!normalizedType) return null;
|
|
254
|
-
return parseSubscriptionEvent(
|
|
255
|
-
providerEventId,
|
|
256
|
-
normalizedType,
|
|
257
|
-
timestamp,
|
|
258
|
-
data,
|
|
259
|
-
options.priceToTier,
|
|
260
|
-
);
|
|
261
|
-
};
|
|
262
|
-
}
|