@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,345 +0,0 @@
|
|
|
1
|
-
// Dodo Payments plugin methods for the post-tenant phase. Option shapes are
|
|
2
|
-
// derived from the plugin contract so a contract change fails at compile time.
|
|
3
|
-
// Every response is validated with Zod; every non-2xx throws a DodoHttpError.
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
KNOWN_RECURRING_INTERVALS,
|
|
7
|
-
type PlanSwitchPreview,
|
|
8
|
-
type ProviderPrice,
|
|
9
|
-
type ProviderSubscriptionSnapshot,
|
|
10
|
-
SubscriptionCancelTimings,
|
|
11
|
-
type SubscriptionProviderPlugin,
|
|
12
|
-
} from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
|
|
13
|
-
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
-
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
15
|
-
import * as z from "zod";
|
|
16
|
-
import { DodoHttpError } from "./client.ts";
|
|
17
|
-
import type { DodoCtxRuntime } from "./runtime.ts";
|
|
18
|
-
import { dodoSubscriptionSchema, mapDodoSubscriptionState } from "./verify-webhook.ts";
|
|
19
|
-
|
|
20
|
-
type PluginMethod<TName extends keyof SubscriptionProviderPlugin> = NonNullable<
|
|
21
|
-
SubscriptionProviderPlugin[TName]
|
|
22
|
-
>;
|
|
23
|
-
|
|
24
|
-
const NOT_FOUND = 404;
|
|
25
|
-
|
|
26
|
-
function subscriptionPath(providerSubscriptionId: string): string {
|
|
27
|
-
return `/subscriptions/${encodeURIComponent(providerSubscriptionId)}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function forcedLanguage(locale: string): string {
|
|
31
|
-
return (locale.split(/[-_]/)[0] ?? locale).toLowerCase();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// =============================================================================
|
|
35
|
-
// Checkout + portal + cancel/resume
|
|
36
|
-
// =============================================================================
|
|
37
|
-
|
|
38
|
-
const checkoutResponseSchema = z.object({
|
|
39
|
-
session_id: z.string(),
|
|
40
|
-
checkout_url: z.string().nullable(),
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
export function createDodoCheckoutSession(
|
|
44
|
-
runtime: DodoCtxRuntime,
|
|
45
|
-
): PluginMethod<"createCheckoutSession"> {
|
|
46
|
-
return async (ctx, options) => {
|
|
47
|
-
// No Dodo session may be created while billing is not live.
|
|
48
|
-
await runtime.assertBillingLive(ctx);
|
|
49
|
-
const response = checkoutResponseSchema.parse(
|
|
50
|
-
await runtime.call(ctx, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
path: "/checkouts",
|
|
53
|
-
body: {
|
|
54
|
-
product_cart: [{ product_id: options.priceId, quantity: 1 }],
|
|
55
|
-
...(options.providerCustomerId && {
|
|
56
|
-
customer: { customer_id: options.providerCustomerId },
|
|
57
|
-
}),
|
|
58
|
-
return_url: options.successUrl,
|
|
59
|
-
cancel_url: options.cancelUrl,
|
|
60
|
-
metadata: {
|
|
61
|
-
tenantId: options.tenantId,
|
|
62
|
-
...(options.consentId && { consentId: options.consentId }),
|
|
63
|
-
},
|
|
64
|
-
...(options.locale && {
|
|
65
|
-
customization: { force_language: forcedLanguage(options.locale) },
|
|
66
|
-
}),
|
|
67
|
-
},
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
if (!response.checkout_url) {
|
|
71
|
-
throw new Error("subscription-dodo: checkout session was created without a checkout_url");
|
|
72
|
-
}
|
|
73
|
-
return { url: response.checkout_url };
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const portalResponseSchema = z.object({ link: z.string() });
|
|
78
|
-
|
|
79
|
-
export function createDodoPortalSession(
|
|
80
|
-
runtime: DodoCtxRuntime,
|
|
81
|
-
): PluginMethod<"createPortalSession"> {
|
|
82
|
-
return async (ctx, options) => {
|
|
83
|
-
const response = portalResponseSchema.parse(
|
|
84
|
-
await runtime.call(ctx, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
path: `/customers/${encodeURIComponent(options.providerCustomerId)}/customer-portal/session`,
|
|
87
|
-
query: { return_url: options.returnUrl },
|
|
88
|
-
}),
|
|
89
|
-
);
|
|
90
|
-
return { url: response.link };
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function createDodoCancelSubscription(
|
|
95
|
-
runtime: DodoCtxRuntime,
|
|
96
|
-
): PluginMethod<"cancelSubscription"> {
|
|
97
|
-
return async (ctx, options) => {
|
|
98
|
-
await runtime.call(ctx, {
|
|
99
|
-
method: "PATCH",
|
|
100
|
-
path: subscriptionPath(options.providerSubscriptionId),
|
|
101
|
-
body:
|
|
102
|
-
options.when === SubscriptionCancelTimings.periodEnd
|
|
103
|
-
? { cancel_at_next_billing_date: true }
|
|
104
|
-
: { status: "cancelled" },
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function createDodoResumeSubscription(
|
|
110
|
-
runtime: DodoCtxRuntime,
|
|
111
|
-
): PluginMethod<"resumeSubscription"> {
|
|
112
|
-
return async (ctx, options) => {
|
|
113
|
-
await runtime.call(ctx, {
|
|
114
|
-
method: "PATCH",
|
|
115
|
-
path: subscriptionPath(options.providerSubscriptionId),
|
|
116
|
-
body: { cancel_at_next_billing_date: false },
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// =============================================================================
|
|
122
|
-
// Prices
|
|
123
|
-
// =============================================================================
|
|
124
|
-
|
|
125
|
-
const productSchema = z.object({
|
|
126
|
-
product_id: z.string(),
|
|
127
|
-
price: z.discriminatedUnion("type", [
|
|
128
|
-
z.object({
|
|
129
|
-
type: z.literal("recurring_price"),
|
|
130
|
-
price: z.number(),
|
|
131
|
-
currency: z.string(),
|
|
132
|
-
payment_frequency_interval: z.string(),
|
|
133
|
-
payment_frequency_count: z.number(),
|
|
134
|
-
}),
|
|
135
|
-
z.object({
|
|
136
|
-
type: z.literal("one_time_price"),
|
|
137
|
-
price: z.number(),
|
|
138
|
-
currency: z.string(),
|
|
139
|
-
}),
|
|
140
|
-
z.object({ type: z.literal("usage_based_price") }),
|
|
141
|
-
]),
|
|
142
|
-
// nullish: an older product without metadata must not fail the whole price lookup.
|
|
143
|
-
metadata: z.record(z.string(), z.string()).nullish(),
|
|
144
|
-
});
|
|
145
|
-
type DodoProduct = z.infer<typeof productSchema>;
|
|
146
|
-
|
|
147
|
-
function mapDodoProduct(product: DodoProduct): ProviderPrice | null {
|
|
148
|
-
const { price } = product;
|
|
149
|
-
if (price.type === "usage_based_price") return null;
|
|
150
|
-
const base = {
|
|
151
|
-
priceId: product.product_id,
|
|
152
|
-
unitAmount: price.price,
|
|
153
|
-
currency: price.currency.toLowerCase(),
|
|
154
|
-
active: true,
|
|
155
|
-
metadata: product.metadata ?? {},
|
|
156
|
-
};
|
|
157
|
-
if (price.type === "one_time_price") return { ...base, interval: null, intervalCount: 1 };
|
|
158
|
-
const rawInterval = price.payment_frequency_interval.toLowerCase();
|
|
159
|
-
const interval = KNOWN_RECURRING_INTERVALS.find((known) => known === rawInterval) ?? null;
|
|
160
|
-
return {
|
|
161
|
-
...base,
|
|
162
|
-
interval,
|
|
163
|
-
intervalCount: interval ? price.payment_frequency_count : null,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export type DodoPriceCache = {
|
|
168
|
-
readonly get: (priceId: string) => ProviderPrice | null | undefined;
|
|
169
|
-
readonly set: (priceId: string, price: ProviderPrice | null) => void;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
export type DodoPriceCacheOptions = {
|
|
173
|
-
readonly ttlMs?: number;
|
|
174
|
-
readonly now?: () => number;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// Keyed by product id alone: ids are unique per account and a cache hit must not
|
|
178
|
-
// trigger an audited secret read on every billing-plans render. Failed lookups
|
|
179
|
-
// are never cached. `null` is a cached "skipped" product (usage-based).
|
|
180
|
-
export function createDodoPriceCache({
|
|
181
|
-
ttlMs = 600_000,
|
|
182
|
-
now = () => Temporal.Now.instant().epochMilliseconds,
|
|
183
|
-
}: DodoPriceCacheOptions = {}): DodoPriceCache {
|
|
184
|
-
const store = new Map<
|
|
185
|
-
string,
|
|
186
|
-
{ readonly price: ProviderPrice | null; readonly expiresAt: number }
|
|
187
|
-
>();
|
|
188
|
-
return {
|
|
189
|
-
get: (priceId) => {
|
|
190
|
-
const entry = store.get(priceId);
|
|
191
|
-
if (!entry) return undefined;
|
|
192
|
-
if (entry.expiresAt <= now()) {
|
|
193
|
-
store.delete(priceId);
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
return entry.price;
|
|
197
|
-
},
|
|
198
|
-
set: (priceId, price) => {
|
|
199
|
-
store.set(priceId, { price, expiresAt: now() + ttlMs });
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function fillPriceCache(
|
|
205
|
-
runtime: DodoCtxRuntime,
|
|
206
|
-
cache: DodoPriceCache,
|
|
207
|
-
ctx: HandlerContext,
|
|
208
|
-
priceIds: readonly string[],
|
|
209
|
-
): Promise<void> {
|
|
210
|
-
const misses = priceIds.filter((priceId) => cache.get(priceId) === undefined);
|
|
211
|
-
const settled = await Promise.allSettled(
|
|
212
|
-
misses.map(async (priceId) => ({
|
|
213
|
-
priceId,
|
|
214
|
-
product: productSchema.parse(
|
|
215
|
-
await runtime.call(ctx, {
|
|
216
|
-
method: "GET",
|
|
217
|
-
path: `/products/${encodeURIComponent(priceId)}`,
|
|
218
|
-
}),
|
|
219
|
-
),
|
|
220
|
-
})),
|
|
221
|
-
);
|
|
222
|
-
for (const outcome of settled) {
|
|
223
|
-
if (outcome.status === "fulfilled") {
|
|
224
|
-
cache.set(outcome.value.priceId, mapDodoProduct(outcome.value.product));
|
|
225
|
-
} else {
|
|
226
|
-
const message =
|
|
227
|
-
outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
228
|
-
ctx.obs.log?.warn("subscription-dodo: product lookup failed", { message });
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function createDodoRetrievePrices(
|
|
234
|
-
runtime: DodoCtxRuntime,
|
|
235
|
-
cache: DodoPriceCache,
|
|
236
|
-
): PluginMethod<"retrievePrices"> {
|
|
237
|
-
return async (ctx, priceIds) => {
|
|
238
|
-
await fillPriceCache(runtime, cache, ctx, priceIds);
|
|
239
|
-
return priceIds.flatMap((priceId) => cache.get(priceId) ?? []);
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// =============================================================================
|
|
244
|
-
// Plan switch (in-app: preview + apply)
|
|
245
|
-
// =============================================================================
|
|
246
|
-
|
|
247
|
-
const previewResponseSchema = z.object({
|
|
248
|
-
immediate_charge: z.object({
|
|
249
|
-
summary: z.object({
|
|
250
|
-
total_amount: z.number(),
|
|
251
|
-
tax: z.number().nullable(),
|
|
252
|
-
currency: z.string(),
|
|
253
|
-
customer_credits: z.number(),
|
|
254
|
-
}),
|
|
255
|
-
effective_at: z.string(),
|
|
256
|
-
}),
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
function changePlanBody(targetPriceId: string) {
|
|
260
|
-
return {
|
|
261
|
-
product_id: targetPriceId,
|
|
262
|
-
quantity: 1,
|
|
263
|
-
proration_billing_mode: "prorated_immediately",
|
|
264
|
-
effective_at: "immediately",
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function createDodoPreviewPlanSwitch(
|
|
269
|
-
runtime: DodoCtxRuntime,
|
|
270
|
-
): PluginMethod<"previewPlanSwitch"> {
|
|
271
|
-
return async (ctx, options): Promise<PlanSwitchPreview> => {
|
|
272
|
-
const { immediate_charge: charge } = previewResponseSchema.parse(
|
|
273
|
-
await runtime.call(ctx, {
|
|
274
|
-
method: "POST",
|
|
275
|
-
path: `${subscriptionPath(options.providerSubscriptionId)}/change-plan/preview`,
|
|
276
|
-
body: changePlanBody(options.targetPriceId),
|
|
277
|
-
}),
|
|
278
|
-
);
|
|
279
|
-
return {
|
|
280
|
-
amountDueNow: charge.summary.total_amount,
|
|
281
|
-
tax: charge.summary.tax,
|
|
282
|
-
currency: charge.summary.currency.toLowerCase(),
|
|
283
|
-
credit: charge.summary.customer_credits,
|
|
284
|
-
effectiveAt: charge.effective_at,
|
|
285
|
-
};
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const applyResponseSchema = z.looseObject({
|
|
290
|
-
payment_id: z.string().nullish(),
|
|
291
|
-
payment_link: z.string().nullish(),
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// The switch itself arrives later as a subscription.plan_changed webhook;
|
|
295
|
-
// prevent_change keeps the old plan if the immediate charge fails.
|
|
296
|
-
export function createDodoApplyPlanSwitch(
|
|
297
|
-
runtime: DodoCtxRuntime,
|
|
298
|
-
): PluginMethod<"applyPlanSwitch"> {
|
|
299
|
-
return async (ctx, options) => {
|
|
300
|
-
applyResponseSchema.parse(
|
|
301
|
-
await runtime.call(ctx, {
|
|
302
|
-
method: "POST",
|
|
303
|
-
path: `${subscriptionPath(options.providerSubscriptionId)}/change-plan`,
|
|
304
|
-
body: {
|
|
305
|
-
...changePlanBody(options.targetPriceId),
|
|
306
|
-
on_payment_failure: "prevent_change",
|
|
307
|
-
metadata: {
|
|
308
|
-
tenantId: options.tenantId,
|
|
309
|
-
...(options.consentId && { consentId: options.consentId }),
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
}),
|
|
313
|
-
);
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// =============================================================================
|
|
318
|
-
// retrieveSubscription
|
|
319
|
-
// =============================================================================
|
|
320
|
-
|
|
321
|
-
export function createDodoRetrieveSubscription(
|
|
322
|
-
runtime: DodoCtxRuntime,
|
|
323
|
-
options: { readonly priceToTier: Readonly<Record<string, string>> },
|
|
324
|
-
): PluginMethod<"retrieveSubscription"> {
|
|
325
|
-
return async (ctx, providerSubscriptionId): Promise<ProviderSubscriptionSnapshot | null> => {
|
|
326
|
-
let raw: unknown;
|
|
327
|
-
try {
|
|
328
|
-
raw = await runtime.call(ctx, {
|
|
329
|
-
method: "GET",
|
|
330
|
-
path: subscriptionPath(providerSubscriptionId),
|
|
331
|
-
});
|
|
332
|
-
} catch (error) {
|
|
333
|
-
if (error instanceof DodoHttpError && error.status === NOT_FOUND) return null;
|
|
334
|
-
throw error;
|
|
335
|
-
}
|
|
336
|
-
const sub = dodoSubscriptionSchema.parse(raw);
|
|
337
|
-
const state = mapDodoSubscriptionState(sub, options.priceToTier);
|
|
338
|
-
if (!state) return null;
|
|
339
|
-
return {
|
|
340
|
-
providerCustomerId: sub.customer.customer_id,
|
|
341
|
-
providerSubscriptionId: sub.subscription_id,
|
|
342
|
-
...state,
|
|
343
|
-
};
|
|
344
|
-
};
|
|
345
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// Call-time key/flag resolution for the subscription-dodo plugin, mirroring
|
|
2
|
-
// subscription-stripe/runtime.ts: credentials come from backing:"secrets"
|
|
3
|
-
// config keys under SYSTEM_TENANT_ID, so rotation and go-live need no redeploy.
|
|
4
|
-
// The factory options survive as env-bridge fallbacks.
|
|
5
|
-
//
|
|
6
|
-
// Two read paths: post-tenant handlers read the key audited through the
|
|
7
|
-
// HandlerContext; the pre-tenant webhook reads the secret un-audited from the
|
|
8
|
-
// system SecretsContext the foundation passes in.
|
|
9
|
-
|
|
10
|
-
import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
11
|
-
import {
|
|
12
|
-
type ConfigKeyHandle,
|
|
13
|
-
type HandlerContext,
|
|
14
|
-
SYSTEM_TENANT_ID,
|
|
15
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
16
|
-
import { FeatureDisabledError, UnconfiguredError } from "@cosmicdrift/kumiko-framework/errors";
|
|
17
|
-
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
18
|
-
import { parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
|
|
19
|
-
import { createDodoHttp, type DodoRequest, type DodoSend } from "./client.ts";
|
|
20
|
-
import { DODO_BASE_URLS, DodoEnvironments, SUBSCRIPTION_DODO_FEATURE } from "./constants.ts";
|
|
21
|
-
|
|
22
|
-
const API_KEY_HINT =
|
|
23
|
-
"Set the Dodo Payments API key via config:write:set on `subscription-dodo:config:api-key` (or seed it from DODO_PAYMENTS_API_KEY during the env bridge).";
|
|
24
|
-
|
|
25
|
-
export type DodoRuntimeDeps = {
|
|
26
|
-
readonly apiKeyHandle: ConfigKeyHandle<"text">;
|
|
27
|
-
readonly webhookSecretHandle: ConfigKeyHandle<"text">;
|
|
28
|
-
readonly billingLiveHandle: ConfigKeyHandle<"boolean">;
|
|
29
|
-
readonly environmentHandle: ConfigKeyHandle<"select">;
|
|
30
|
-
readonly send: DodoSend;
|
|
31
|
-
readonly fallback: { readonly apiKey?: string; readonly webhookSecret?: string };
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// backing:"secrets" values are stored JSON-serialized, so the store read has to
|
|
35
|
-
// be parsed back; the env fallback is a plain string and stays raw. The error
|
|
36
|
-
// must not carry the input: the JSON parser's own message would repeat the
|
|
37
|
-
// credential into logs and, for webhooks, into the 401 body.
|
|
38
|
-
function parseStoredSecret(raw: string | undefined): string | undefined {
|
|
39
|
-
if (raw === undefined || raw.length === 0) return undefined;
|
|
40
|
-
const parsed = parseJsonSafe<unknown>(raw, undefined);
|
|
41
|
-
if (parsed === undefined) {
|
|
42
|
-
throw new Error("subscription-dodo credential (backing:secrets) is not valid JSON");
|
|
43
|
-
}
|
|
44
|
-
return typeof parsed === "string" && parsed.length > 0 ? parsed : undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type DodoCtxRuntime = {
|
|
48
|
-
readonly call: (ctx: HandlerContext, request: DodoRequest) => Promise<unknown>;
|
|
49
|
-
readonly assertBillingLive: (ctx: HandlerContext, handlerName?: string) => Promise<void>;
|
|
50
|
-
readonly isBillingEnabled: (ctx: HandlerContext) => Promise<boolean>;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export type DodoWebhookRuntime = {
|
|
54
|
-
readonly resolveWebhookSecret: (systemSecrets?: SecretsContext) => Promise<string>;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export type DodoRuntimes = {
|
|
58
|
-
readonly ctx: DodoCtxRuntime;
|
|
59
|
-
readonly webhook: DodoWebhookRuntime;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export function createDodoRuntimes(deps: DodoRuntimeDeps): DodoRuntimes {
|
|
63
|
-
const http = createDodoHttp(deps.send);
|
|
64
|
-
|
|
65
|
-
async function ctxApiKey(ctx: HandlerContext): Promise<string> {
|
|
66
|
-
let key: string | undefined;
|
|
67
|
-
if (ctx.services.secrets) {
|
|
68
|
-
const got = await requireSecretsContext(
|
|
69
|
-
{ secrets: ctx.services.secrets, user: ctx.user },
|
|
70
|
-
SUBSCRIPTION_DODO_FEATURE,
|
|
71
|
-
).get(SYSTEM_TENANT_ID, deps.apiKeyHandle.name);
|
|
72
|
-
key = parseStoredSecret(got?.reveal());
|
|
73
|
-
}
|
|
74
|
-
if (!key && deps.fallback.apiKey && deps.fallback.apiKey.length > 0) {
|
|
75
|
-
key = deps.fallback.apiKey;
|
|
76
|
-
}
|
|
77
|
-
if (!key) {
|
|
78
|
-
throw new UnconfiguredError({
|
|
79
|
-
feature: SUBSCRIPTION_DODO_FEATURE,
|
|
80
|
-
key: deps.apiKeyHandle.name,
|
|
81
|
-
hint: API_KEY_HINT,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
return key;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function baseUrlFor(ctx: HandlerContext): Promise<string> {
|
|
88
|
-
const environment = ctx.config ? await ctx.config(deps.environmentHandle) : undefined;
|
|
89
|
-
return DODO_BASE_URLS[environment === DodoEnvironments.live ? "live" : "test"];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
ctx: {
|
|
94
|
-
call: async (ctx, request) =>
|
|
95
|
-
http({ baseUrl: await baseUrlFor(ctx), apiKey: await ctxApiKey(ctx) }, request),
|
|
96
|
-
assertBillingLive: async (ctx, handlerName = "create-checkout-session") => {
|
|
97
|
-
const live = ctx.config ? await ctx.config(deps.billingLiveHandle) : undefined;
|
|
98
|
-
if (live !== true) throw new FeatureDisabledError(SUBSCRIPTION_DODO_FEATURE, handlerName);
|
|
99
|
-
},
|
|
100
|
-
isBillingEnabled: async (ctx) => {
|
|
101
|
-
const live = ctx.config ? await ctx.config(deps.billingLiveHandle) : undefined;
|
|
102
|
-
if (live !== true) return false;
|
|
103
|
-
if (deps.fallback.apiKey !== undefined && deps.fallback.apiKey.length > 0) return true;
|
|
104
|
-
// Existence-only probe on every billing-plans render: must stay un-audited.
|
|
105
|
-
return ctx.services.secrets
|
|
106
|
-
? await ctx.services.secrets.has(SYSTEM_TENANT_ID, deps.apiKeyHandle.name)
|
|
107
|
-
: false;
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
webhook: {
|
|
111
|
-
resolveWebhookSecret: async (systemSecrets) => {
|
|
112
|
-
if (systemSecrets) {
|
|
113
|
-
// No auditCtx: every webhook would otherwise spam the audit trail.
|
|
114
|
-
const got = await systemSecrets.get(SYSTEM_TENANT_ID, deps.webhookSecretHandle.name);
|
|
115
|
-
const stored = parseStoredSecret(got?.reveal());
|
|
116
|
-
if (stored) return stored;
|
|
117
|
-
}
|
|
118
|
-
const fallback = deps.fallback.webhookSecret;
|
|
119
|
-
if (fallback && fallback.length > 0) return fallback;
|
|
120
|
-
throw new Error(
|
|
121
|
-
`subscription-dodo: '${deps.webhookSecretHandle.name}' unresolved — no system-secret set and no factory fallback. Webhook cannot verify until it is configured.`,
|
|
122
|
-
);
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
}
|