@cosmicdrift/kumiko-samples 0.387.0 → 0.388.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 +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +6 -0
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +9 -9
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +5 -1
- package/packages/bundled-features/src/cap-counter/changes.json +18 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +12 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +10 -9
- package/packages/bundled-features/src/delivery/changes.json +30 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +6 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +14 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +4 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/sessions/feature.ts +7 -2
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/step-dispatcher/changes.json +6 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +48 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +15 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +32 -14
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +22 -4
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +5 -5
- package/packages/bundled-features/src/waitlist/import.ts +5 -0
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +22 -3
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +3 -0
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +120 -3
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -16,9 +16,12 @@ import {
|
|
|
16
16
|
createChannelEmailFeature,
|
|
17
17
|
createInMemoryTransport,
|
|
18
18
|
} from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
19
|
+
import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
|
|
19
20
|
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
20
21
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
22
|
import {
|
|
23
|
+
GALLERY_MOR_PRICE_IDS,
|
|
24
|
+
GALLERY_MOR_PROVIDER,
|
|
22
25
|
GALLERY_OPERATOR_EMAIL,
|
|
23
26
|
GALLERY_PLAN_PATH,
|
|
24
27
|
GALLERY_PRICE_IDS,
|
|
@@ -28,25 +31,44 @@ import {
|
|
|
28
31
|
|
|
29
32
|
export const galleryMailTransport = createInMemoryTransport();
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
// The merchant-of-record mock is only offered to tenants the spec opts in, so
|
|
35
|
+
// the single-provider flows keep their original screens.
|
|
36
|
+
const morEnabledTenants = new Set<string>();
|
|
37
|
+
|
|
38
|
+
export const GALLERY_MOR_ENABLE_PATH = "/gallery/mor/:tenantId";
|
|
39
|
+
|
|
40
|
+
export function createGalleryMorEnableRoute(): ExtraRouteDefinition {
|
|
41
|
+
return {
|
|
42
|
+
method: "POST",
|
|
43
|
+
path: GALLERY_MOR_ENABLE_PATH,
|
|
44
|
+
entry: "anonymous",
|
|
45
|
+
handler: (c) => {
|
|
46
|
+
morEnabledTenants.add(c.req.param("tenantId") ?? "");
|
|
47
|
+
return c.body(null, 204);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function monthlyPrice(priceId: string, unitAmount: number): ProviderPrice {
|
|
53
|
+
return {
|
|
54
|
+
priceId,
|
|
55
|
+
unitAmount,
|
|
44
56
|
currency: "eur",
|
|
45
57
|
interval: "month",
|
|
46
58
|
intervalCount: 1,
|
|
47
59
|
active: true,
|
|
48
60
|
metadata: {},
|
|
49
|
-
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const PRICES: readonly ProviderPrice[] = [
|
|
65
|
+
monthlyPrice(GALLERY_PRICE_IDS.starter, 900),
|
|
66
|
+
monthlyPrice(GALLERY_PRICE_IDS.pro, 1900),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const MOR_PRICES: readonly ProviderPrice[] = [
|
|
70
|
+
monthlyPrice(GALLERY_MOR_PRICE_IDS.starter, 990),
|
|
71
|
+
monthlyPrice(GALLERY_MOR_PRICE_IDS.pro, 1990),
|
|
50
72
|
];
|
|
51
73
|
|
|
52
74
|
async function resolveCurrentTier(db: TenantDb, tenantId: string): Promise<string> {
|
|
@@ -74,7 +96,6 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
|
|
|
74
96
|
viewRoles: ["TenantAdmin", "SystemAdmin"],
|
|
75
97
|
successPath: GALLERY_PLAN_PATH,
|
|
76
98
|
cancelPath: GALLERY_PLAN_PATH,
|
|
77
|
-
providerName: GALLERY_PROVIDER,
|
|
78
99
|
contact: {
|
|
79
100
|
labelKey: "gallery-screens:plan.enterprise.label",
|
|
80
101
|
benefits: [{ labelKey: "gallery-screens:plan.enterprise.benefit" }],
|
|
@@ -85,6 +106,24 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
|
|
|
85
106
|
consumerProtection: {
|
|
86
107
|
termsTextBlock: GALLERY_TERMS_SLUG,
|
|
87
108
|
vatNote: { de: "Preise inkl. USt.", en: "Prices include VAT." },
|
|
109
|
+
providerTexts: {
|
|
110
|
+
[GALLERY_PROVIDER]: {
|
|
111
|
+
checkoutNote: {
|
|
112
|
+
de: "Rechnung von Gallery Beispiel GmbH",
|
|
113
|
+
en: "Invoiced by Gallery Beispiel GmbH",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
[GALLERY_MOR_PROVIDER]: {
|
|
117
|
+
checkoutNote: {
|
|
118
|
+
de: "Verkäufer ist Dodo Payments · Rechnung und USt von Dodo Payments",
|
|
119
|
+
en: "Sold by Dodo Payments · invoice and VAT from Dodo Payments",
|
|
120
|
+
},
|
|
121
|
+
vatNote: {
|
|
122
|
+
de: "Rechnung und Umsatzsteuer stellt Dodo Payments als Merchant of Record.",
|
|
123
|
+
en: "Dodo Payments, as merchant of record, issues the invoice and charges the VAT.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
88
127
|
operatorEmail: GALLERY_OPERATOR_EMAIL,
|
|
89
128
|
terminationScope: "platform",
|
|
90
129
|
legalLinks: {
|
|
@@ -105,6 +144,7 @@ export function createGalleryEmailFeature(): FeatureDefinition {
|
|
|
105
144
|
// The page URL carries the consent id so the spec can echo it on the webhook,
|
|
106
145
|
// exactly as a real provider echoes the checkout metadata.
|
|
107
146
|
const galleryProviderPlugin: SubscriptionProviderPlugin = {
|
|
147
|
+
displayName: "Stripe",
|
|
108
148
|
verifyAndParseWebhook: async (rawBody) => JSON.parse(rawBody),
|
|
109
149
|
priceToTier: {
|
|
110
150
|
[GALLERY_PRICE_IDS.starter]: "starter",
|
|
@@ -119,12 +159,47 @@ const galleryProviderPlugin: SubscriptionProviderPlugin = {
|
|
|
119
159
|
cancelSubscription: async () => {},
|
|
120
160
|
};
|
|
121
161
|
|
|
162
|
+
// Merchant-of-record stand-in: switches plans in the app with fixed figures
|
|
163
|
+
// (Starter to Pro: 6.67 EUR due now, 1.06 EUR of it VAT). Applying without a
|
|
164
|
+
// consent id fails, so a successful switch in the spec proves the consent went
|
|
165
|
+
// through.
|
|
166
|
+
const galleryMorProviderPlugin: SubscriptionProviderPlugin = {
|
|
167
|
+
displayName: "Dodo Payments",
|
|
168
|
+
isBillingEnabled: async (ctx) => morEnabledTenants.has(ctx.user.tenantId),
|
|
169
|
+
verifyAndParseWebhook: async (rawBody) => JSON.parse(rawBody),
|
|
170
|
+
priceToTier: {
|
|
171
|
+
[GALLERY_MOR_PRICE_IDS.starter]: "starter",
|
|
172
|
+
[GALLERY_MOR_PRICE_IDS.pro]: "pro",
|
|
173
|
+
},
|
|
174
|
+
retrievePrices: async (_ctx, priceIds) => MOR_PRICES.filter((p) => priceIds.includes(p.priceId)),
|
|
175
|
+
createCheckoutSession: async (_ctx, options) => {
|
|
176
|
+
const url = new URL(options.successUrl);
|
|
177
|
+
url.searchParams.set("provider", "mor");
|
|
178
|
+
if (options.consentId !== undefined) url.searchParams.set("consentId", options.consentId);
|
|
179
|
+
return { url: url.toString() };
|
|
180
|
+
},
|
|
181
|
+
cancelSubscription: async () => {},
|
|
182
|
+
previewPlanSwitch: async () => ({
|
|
183
|
+
amountDueNow: 667,
|
|
184
|
+
tax: 106,
|
|
185
|
+
currency: "eur",
|
|
186
|
+
credit: 333,
|
|
187
|
+
effectiveAt: "2027-01-15T00:00:00Z",
|
|
188
|
+
}),
|
|
189
|
+
applyPlanSwitch: async (_ctx, options) => {
|
|
190
|
+
if (options.consentId === undefined) {
|
|
191
|
+
throw new Error("gallery MoR mock: plan switch applied without a consent id");
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
122
196
|
export const galleryProviderFeature: FeatureDefinition = defineFeature(
|
|
123
197
|
"gallery-mock-provider",
|
|
124
198
|
(r) => {
|
|
125
199
|
r.describe("E2E-only mock subscription provider for the consent/termination gallery.");
|
|
126
200
|
r.requires({ features: ["billing-foundation"] });
|
|
127
201
|
r.useExtension("subscriptionProvider", GALLERY_PROVIDER, galleryProviderPlugin);
|
|
202
|
+
r.useExtension("subscriptionProvider", GALLERY_MOR_PROVIDER, galleryMorProviderPlugin);
|
|
128
203
|
},
|
|
129
204
|
);
|
|
130
205
|
|
|
@@ -20,6 +20,7 @@ import { GALLERY_TENANT_HOST, GALLERY_TERMS_SLUG } from "./gallery-constants";
|
|
|
20
20
|
import {
|
|
21
21
|
createGalleryBillingFeature,
|
|
22
22
|
createGalleryEmailFeature,
|
|
23
|
+
createGalleryMorEnableRoute,
|
|
23
24
|
galleryMailTransport,
|
|
24
25
|
galleryProviderFeature,
|
|
25
26
|
galleryScreensFeature,
|
|
@@ -29,14 +30,22 @@ const port = Number.parseInt(process.env["PORT"] ?? "4196", 10);
|
|
|
29
30
|
|
|
30
31
|
const BILLING_FEATURE = "billing-foundation";
|
|
31
32
|
const EMAIL_FEATURE = "channel-email";
|
|
33
|
+
// The stock app mounts the real Mollie and Dodo providers; the gallery's
|
|
34
|
+
// provider choice is the two mocks, which never call out.
|
|
35
|
+
const REAL_PROVIDER_FEATURES: ReadonlySet<string> = new Set([
|
|
36
|
+
"subscription-mollie",
|
|
37
|
+
"subscription-dodo",
|
|
38
|
+
]);
|
|
32
39
|
|
|
33
40
|
const features = [
|
|
34
|
-
...devAppOptions.features
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
...devAppOptions.features
|
|
42
|
+
.filter((feature) => !REAL_PROVIDER_FEATURES.has(feature.name))
|
|
43
|
+
.map((feature) => {
|
|
44
|
+
if (feature.name === BILLING_FEATURE)
|
|
45
|
+
return createGalleryBillingFeature(`http://localhost:${port}`);
|
|
46
|
+
if (feature.name === EMAIL_FEATURE) return createGalleryEmailFeature();
|
|
47
|
+
return feature;
|
|
48
|
+
}),
|
|
40
49
|
galleryProviderFeature,
|
|
41
50
|
galleryScreensFeature,
|
|
42
51
|
];
|
|
@@ -77,6 +86,7 @@ await runDevApp({
|
|
|
77
86
|
extraRoutes: [
|
|
78
87
|
...createE2eSeedRoutes({ mailOutbox: galleryMailTransport }),
|
|
79
88
|
createSubscriptionWebhookRoute(),
|
|
89
|
+
createGalleryMorEnableRoute(),
|
|
80
90
|
...createContractTerminationRoutes(),
|
|
81
91
|
],
|
|
82
92
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "samples/apps/use-all-bundled APP_FEATURES (composeFeatures includeBundled + signup)",
|
|
3
|
-
"featureCount":
|
|
3
|
+
"featureCount": 88,
|
|
4
4
|
"features": [
|
|
5
5
|
{
|
|
6
6
|
"name": "account-security",
|
|
@@ -300,7 +300,7 @@
|
|
|
300
300
|
},
|
|
301
301
|
{
|
|
302
302
|
"name": "billing-foundation",
|
|
303
|
-
"description": "Plugin host for subscription billing
|
|
303
|
+
"description": "Plugin host for subscription billing. Manages the `read_subscriptions` projection table and exposes 5 domain events (subscription created/updated/canceled, invoice paid/failed) appended by the foundation's own `billing-foundation:write:process-event` write-handler after provider plugins verify and normalize each webhook. Also manages a separate `read_payments` projection table (one row per one-off-payment) fed by its own `payment-received` event and `billing-foundation:write:process-payment-event` write-handler. Also ships `billing-foundation:write:create-checkout-session` and `billing-foundation:write:create-portal-session` write-handlers, a `billing-foundation:query:subscription:list` query handler, and a `createSubscriptionWebhookRoute` factory for the `/api/subscription/webhook/:providerName` extraRoute. `createBillingFoundationFeature({ baseUrl, catalog })` additionally derives a `billing-foundation:query:billing-plans` query, `start-plan-checkout`/`switch-plan` write-handlers, a `plan-switch-preview` query and a dormant billing-plans dashboard screen/panel from the catalog. Several providers can be mounted at once: new checkouts are offered by every enabled provider (enabled means keys set and billingLive on, or simply mounted for plugins without `isBillingEnabled`), the customer picks one in the plan panel and `start-plan-checkout` takes a `providerName`. An existing subscription stays with the provider that stored it, so the plan list and every switch follow that provider. A plan switch is either hosted (`createPlanSwitchSession` answers a url) or in-app (`previewPlanSwitch` shows the proration, `applyPlanSwitch` runs after the customer confirms; `switch-plan` then answers `{ applied: true }` and records the consent). `consumerProtection.providerTexts` overrides the checkout note and the VAT note per provider. Also ships `billing-foundation:write:sync-subscription` (pulls a provider plugin's live subscription state via `retrieveSubscription` and appends drift as a `subscription.updated` event, which catches changes made on the provider's own dashboard that never reached us as a webhook) and the `sync-subscriptions` job (manual-trigger + runOnBoot, perTenant) that dispatches it. Low-level building block: use `subscription-stripe`, `subscription-mollie` or `subscription-dodo` unless you are writing a new payment provider.",
|
|
304
304
|
"toggleableDefault": null,
|
|
305
305
|
"requires": [
|
|
306
306
|
"tenant-lifecycle",
|
|
@@ -424,7 +424,7 @@
|
|
|
424
424
|
},
|
|
425
425
|
{
|
|
426
426
|
"name": "channel-email",
|
|
427
|
-
"description": "Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send the notifications declared with `tenantTransport: true` through the sending tenant's own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`; every other notification (password reset, sign-in) always goes through the app `transport`. `transport` may be left out when `tenantTransport` is set
|
|
427
|
+
"description": "Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send the notifications declared with `tenantTransport: true` through the sending tenant's own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`; every other notification (password reset, sign-in) always goes through the app `transport`. `transport` may be left out when `tenantTransport` is set, but then only notifications declared with `tenantTransport: true` are delivered: every other notification (password reset, sign-in, invitations) fails with `missing_credentials`, so an app without an app SMTP server must mark every notification that should go out or keep an app `transport`. Notifications declared with `unsubscribe: true` get a signed `data.unsubscribeUrl` from delivery, which turns into the `List-Unsubscribe` headers.",
|
|
428
428
|
"toggleableDefault": null,
|
|
429
429
|
"requires": [
|
|
430
430
|
"delivery"
|
|
@@ -2467,6 +2467,123 @@
|
|
|
2467
2467
|
"recommended": false
|
|
2468
2468
|
}
|
|
2469
2469
|
},
|
|
2470
|
+
{
|
|
2471
|
+
"name": "subscription-dodo",
|
|
2472
|
+
"description": "Dodo Payments provider plugin for `billing-foundation`. Dodo acts as merchant of record: it sells to the customer and issues invoice and VAT, so the consumer-protection texts for this provider come from `consumerProtection.providerTexts`. Reads its API key and webhook secret from system config keys with `backing:\"secrets\"` (envelope-encrypted in the secrets store under the system tenant), plus a `billingLive` master switch and an `environment` key (test | live). `environment` is its own key because Dodo serves test and live mode from different API hosts and a live key must never hit the test host or the other way round; `billingLive` only decides whether checkouts are offered at all. All four are runtime config, so keys rotate and prod goes live without a redeploy, and the `mask` on each derives the sysadmin settings screen and nav. Mount via `createSubscriptionDodoFeature({ priceToTier })`; the optional `apiKey`/`webhookSecret` options are env to secrets bridge fallbacks (`DODO_PAYMENTS_API_KEY`, `DODO_PAYMENTS_WEBHOOK_KEY`). The plugin always mounts; `createCheckoutSession` throws `feature_disabled` unless `billingLive` is true. Webhooks arrive at `/api/subscription/webhook/dodo` and are verified per the Standard Webhooks spec (HMAC-SHA256 over id, timestamp and the raw body, 5 minute tolerance); events for products outside `priceToTier` or without tenant metadata are dropped. Implements hosted checkout, customer portal, cancel and resume, `retrievePrices` (10-minute TTL cache), `retrieveSubscription` for the sync job, and an in-app plan switch: `previewPlanSwitch` shows the proration before the customer confirms, `applyPlanSwitch` changes the plan without a hosted page and waits for the `plan_changed` webhook to move the tier. A plan switch in Dodo requires all plan products to belong to one Product Collection.",
|
|
2473
|
+
"toggleableDefault": null,
|
|
2474
|
+
"requires": [
|
|
2475
|
+
"billing-foundation",
|
|
2476
|
+
"config",
|
|
2477
|
+
"secrets"
|
|
2478
|
+
],
|
|
2479
|
+
"optionalRequires": [],
|
|
2480
|
+
"configReads": [],
|
|
2481
|
+
"exposesApis": [],
|
|
2482
|
+
"usesApis": [],
|
|
2483
|
+
"extensionsUsed": [
|
|
2484
|
+
{
|
|
2485
|
+
"extensionName": "subscriptionProvider",
|
|
2486
|
+
"entityName": "dodo"
|
|
2487
|
+
}
|
|
2488
|
+
],
|
|
2489
|
+
"configKeys": [
|
|
2490
|
+
{
|
|
2491
|
+
"key": "api-key",
|
|
2492
|
+
"qualifiedName": "subscription-dodo:config:api-key",
|
|
2493
|
+
"type": "text",
|
|
2494
|
+
"scope": "system",
|
|
2495
|
+
"default": null,
|
|
2496
|
+
"encrypted": true,
|
|
2497
|
+
"computed": false,
|
|
2498
|
+
"validated": false,
|
|
2499
|
+
"options": null,
|
|
2500
|
+
"bounds": null,
|
|
2501
|
+
"pattern": null,
|
|
2502
|
+
"writeRoles": [
|
|
2503
|
+
"SystemAdmin"
|
|
2504
|
+
],
|
|
2505
|
+
"readRoles": [
|
|
2506
|
+
"TenantAdmin",
|
|
2507
|
+
"Admin",
|
|
2508
|
+
"SystemAdmin"
|
|
2509
|
+
]
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
"key": "billing-live",
|
|
2513
|
+
"qualifiedName": "subscription-dodo:config:billing-live",
|
|
2514
|
+
"type": "boolean",
|
|
2515
|
+
"scope": "system",
|
|
2516
|
+
"default": false,
|
|
2517
|
+
"encrypted": false,
|
|
2518
|
+
"computed": false,
|
|
2519
|
+
"validated": false,
|
|
2520
|
+
"options": null,
|
|
2521
|
+
"bounds": null,
|
|
2522
|
+
"pattern": null,
|
|
2523
|
+
"writeRoles": [
|
|
2524
|
+
"system",
|
|
2525
|
+
"SystemAdmin"
|
|
2526
|
+
],
|
|
2527
|
+
"readRoles": [
|
|
2528
|
+
"TenantAdmin",
|
|
2529
|
+
"Admin",
|
|
2530
|
+
"SystemAdmin"
|
|
2531
|
+
]
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
"key": "environment",
|
|
2535
|
+
"qualifiedName": "subscription-dodo:config:environment",
|
|
2536
|
+
"type": "select",
|
|
2537
|
+
"scope": "system",
|
|
2538
|
+
"default": "test",
|
|
2539
|
+
"encrypted": false,
|
|
2540
|
+
"computed": false,
|
|
2541
|
+
"validated": false,
|
|
2542
|
+
"options": [
|
|
2543
|
+
"test",
|
|
2544
|
+
"live"
|
|
2545
|
+
],
|
|
2546
|
+
"bounds": null,
|
|
2547
|
+
"pattern": null,
|
|
2548
|
+
"writeRoles": [
|
|
2549
|
+
"SystemAdmin"
|
|
2550
|
+
],
|
|
2551
|
+
"readRoles": [
|
|
2552
|
+
"TenantAdmin",
|
|
2553
|
+
"Admin",
|
|
2554
|
+
"SystemAdmin"
|
|
2555
|
+
]
|
|
2556
|
+
},
|
|
2557
|
+
{
|
|
2558
|
+
"key": "webhook-secret",
|
|
2559
|
+
"qualifiedName": "subscription-dodo:config:webhook-secret",
|
|
2560
|
+
"type": "text",
|
|
2561
|
+
"scope": "system",
|
|
2562
|
+
"default": null,
|
|
2563
|
+
"encrypted": true,
|
|
2564
|
+
"computed": false,
|
|
2565
|
+
"validated": false,
|
|
2566
|
+
"options": null,
|
|
2567
|
+
"bounds": null,
|
|
2568
|
+
"pattern": null,
|
|
2569
|
+
"writeRoles": [
|
|
2570
|
+
"SystemAdmin"
|
|
2571
|
+
],
|
|
2572
|
+
"readRoles": [
|
|
2573
|
+
"TenantAdmin",
|
|
2574
|
+
"Admin",
|
|
2575
|
+
"SystemAdmin"
|
|
2576
|
+
]
|
|
2577
|
+
}
|
|
2578
|
+
],
|
|
2579
|
+
"secrets": [],
|
|
2580
|
+
"writeHandlers": [],
|
|
2581
|
+
"uiHints": {
|
|
2582
|
+
"displayLabel": "Billing · Dodo Payments",
|
|
2583
|
+
"category": "billing",
|
|
2584
|
+
"recommended": false
|
|
2585
|
+
}
|
|
2586
|
+
},
|
|
2470
2587
|
{
|
|
2471
2588
|
"name": "subscription-mollie",
|
|
2472
2589
|
"description": "Mollie payment provider plugin for `billing-foundation`, covering the DACH/EU mid-market use case. Mount via `createSubscriptionMollieFeature({ apiKey, webhookUrl, priceToTier, priceToConfig })` — the factory validates that `priceToTier` and `priceToConfig` keys are identical at boot time. Implements `verifyAndParseWebhook` (lazy Mollie-API fetch + heuristic event-type mapping) and `createCheckoutSession` (customer + first-payment with `sequenceType=\"first\"`); `createPortalSession` and `cancelSubscription` are not available because Mollie has no customer portal and requires a `customerId` that the plugin contract does not carry.",
|
|
@@ -75,6 +75,7 @@ import { createRendererPdfFeature } from "@cosmicdrift/kumiko-bundled-features/r
|
|
|
75
75
|
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
76
76
|
import { createSeoFeature } from "@cosmicdrift/kumiko-bundled-features/seo";
|
|
77
77
|
import { createStepDispatcherFeature } from "@cosmicdrift/kumiko-bundled-features/step-dispatcher";
|
|
78
|
+
import { createSubscriptionDodoFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
|
|
78
79
|
import { createSubscriptionMollieFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-mollie";
|
|
79
80
|
import { createSubscriptionStripeFeature } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
80
81
|
import { createTagsFeature } from "@cosmicdrift/kumiko-bundled-features/tags";
|
|
@@ -209,6 +210,11 @@ export const APP_FEATURES = [
|
|
|
209
210
|
webhookSecret: "whsec_smoke",
|
|
210
211
|
priceToTier: { price_smoke: "free" },
|
|
211
212
|
}),
|
|
213
|
+
createSubscriptionDodoFeature({
|
|
214
|
+
apiKey: "dodo_smoke",
|
|
215
|
+
webhookSecret: "whsec_smoke",
|
|
216
|
+
priceToTier: { pdt_smoke: "free" },
|
|
217
|
+
}),
|
|
212
218
|
createSubscriptionMollieFeature({
|
|
213
219
|
apiKey: "test_smoke_key",
|
|
214
220
|
webhookUrl: "https://smoke.example/webhook",
|
|
@@ -416,12 +422,12 @@ export const BOOT_FEATURES = APP_FEATURES.filter(
|
|
|
416
422
|
(feature) => feature.name !== FEATURE_TOGGLES_FEATURE,
|
|
417
423
|
);
|
|
418
424
|
|
|
419
|
-
// Smoke signup — enables createAuthSelfRegistrationToggleFeature via
|
|
420
|
-
// composeFeatures when passed as authOptions (#1521 Option A). Same shape
|
|
421
|
-
// as run{Dev,Prod}App's auth.signup; appUrl is a stub (no real mail in boot).
|
|
422
425
|
// locale-de is mounted, so every translation key must exist in German and English.
|
|
423
426
|
export const VALIDATE_BOOT_OPTIONS = { requiredLocales: ["de", "en"] } as const;
|
|
424
427
|
|
|
428
|
+
// Smoke signup — enables createAuthSelfRegistrationToggleFeature via
|
|
429
|
+
// composeFeatures when passed as authOptions (#1521 Option A). Same shape
|
|
430
|
+
// as run{Dev,Prod}App's auth.signup; appUrl is a stub (no real mail in boot).
|
|
425
431
|
export const AUTH_COMPOSE_OPTIONS = {
|
|
426
432
|
signup: {
|
|
427
433
|
appUrl: "http://localhost:4186/signup",
|
|
@@ -75,8 +75,13 @@ features: [..., createSessionsFeature(), mfaFeature, createAccountSecurityFeatur
|
|
|
75
75
|
5. **MFA state changes revoke every OTHER live session** (stolen-session defense) — never the session that made the change.
|
|
76
76
|
6. **Enforcement is opt-in.** Without a tenant enabling `auth-mfa:config:required`, users choose for themselves whether to enroll.
|
|
77
77
|
|
|
78
|
+
## Confirmation code input
|
|
79
|
+
|
|
80
|
+
`src/web/confirmation-code-step.tsx` shows the `Input kind="code"` renderer primitive outside the MFA screen, for example an e-mail or invite confirmation code. Pass `length` (here 6), `autoFocus` and `onComplete`. The input accepts digits only, so pasting "123 456" yields `123456`, and `onComplete` fires once when the value reaches `length`. `autoComplete` defaults to `one-time-code`, so mobile keyboards offer the code from the SMS or mail. After a wrong code the step clears the value and bumps a `key`, which remounts the input and returns focus to it.
|
|
81
|
+
|
|
78
82
|
## Tests
|
|
79
83
|
|
|
80
84
|
- **Baseline:** a user without MFA enrolled still gets a JWT straight from `/auth/login`.
|
|
81
85
|
- **End-to-end:** enroll via `enable-start`/`enable-confirm`, log in (gets a challenge, no JWT), complete via `/auth/mfa/verify` (gets a JWT) — copies the `buildServer({ auth: ... })` block from the sample comment verbatim.
|
|
82
86
|
- **Account security:** the composed screen boot-validates next to `auth-mfa` and `sessions` and shows a sidebar entry once `nav` is set.
|
|
87
|
+
- **Confirmation code step:** typing or pasting six digits calls `onConfirm` once; a rejected code shows the error, clears the field and refocuses it.
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@cosmicdrift/kumiko-bundled-features": "workspace:*",
|
|
7
|
-
"@cosmicdrift/kumiko-framework": "workspace:*"
|
|
7
|
+
"@cosmicdrift/kumiko-framework": "workspace:*",
|
|
8
|
+
"@cosmicdrift/kumiko-renderer": "workspace:*",
|
|
9
|
+
"react": "^19.2.6"
|
|
8
10
|
},
|
|
9
11
|
"devDependencies": {
|
|
10
|
-
"@cosmicdrift/kumiko-testing": "workspace:*"
|
|
12
|
+
"@cosmicdrift/kumiko-testing": "workspace:*",
|
|
13
|
+
"@types/react": "^19.2.14"
|
|
11
14
|
}
|
|
12
15
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import { type ReactNode, useState } from "react";
|
|
3
|
+
|
|
4
|
+
const CONFIRMATION_CODE_LENGTH = 6;
|
|
5
|
+
|
|
6
|
+
export type ConfirmationCodeStepProps = {
|
|
7
|
+
/** Resolves true when the server accepted the code. */
|
|
8
|
+
readonly onConfirm: (code: string) => Promise<boolean>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function ConfirmationCodeStep({ onConfirm }: ConfirmationCodeStepProps): ReactNode {
|
|
12
|
+
const { Input, Banner } = usePrimitives();
|
|
13
|
+
const [code, setCode] = useState("");
|
|
14
|
+
const [attempt, setAttempt] = useState(0);
|
|
15
|
+
const [pending, setPending] = useState(false);
|
|
16
|
+
const [rejected, setRejected] = useState(false);
|
|
17
|
+
|
|
18
|
+
const confirm = async (submittedCode: string): Promise<void> => {
|
|
19
|
+
if (pending) return;
|
|
20
|
+
setPending(true);
|
|
21
|
+
setRejected(false);
|
|
22
|
+
const accepted = await onConfirm(submittedCode).finally(() => setPending(false));
|
|
23
|
+
if (accepted) return;
|
|
24
|
+
setRejected(true);
|
|
25
|
+
setCode("");
|
|
26
|
+
// The input was disabled while pending and lost focus; remounting restores autoFocus.
|
|
27
|
+
setAttempt((n) => n + 1);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<Input
|
|
33
|
+
key={attempt}
|
|
34
|
+
kind="code"
|
|
35
|
+
length={CONFIRMATION_CODE_LENGTH}
|
|
36
|
+
id="confirmation-code"
|
|
37
|
+
name="confirmation-code"
|
|
38
|
+
testId="confirmation-code"
|
|
39
|
+
value={code}
|
|
40
|
+
onChange={setCode}
|
|
41
|
+
onComplete={(next) => void confirm(next)}
|
|
42
|
+
disabled={pending}
|
|
43
|
+
hasError={rejected}
|
|
44
|
+
autoFocus
|
|
45
|
+
/>
|
|
46
|
+
{rejected ? <Banner variant="error">That code is not valid. Try again.</Banner> : null}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -10,7 +10,8 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
10
10
|
`kmsEnvSchema` and `s3EnvSchema`. `composeEnvSchema` collects them and
|
|
11
11
|
records which feature declared each variable.
|
|
12
12
|
- **App-level blocks in `extend`**: `buildInfoEnvSchema`, `demoAdminEnvSchema`,
|
|
13
|
-
`stripeEnvSchema({ prices })
|
|
13
|
+
`stripeEnvSchema({ prices })`, `dodoEnvSchema({ prices })` and
|
|
14
|
+
`createS3EnvSchema({ prefix })` for a
|
|
14
15
|
second bucket. These belong to no mounted feature, so the app adds them.
|
|
15
16
|
- **Typed result**: `parseEnv(composed.schema, …)` returns the merged type,
|
|
16
17
|
so `parsed.S3_BUCKET` is `string | undefined` and `parsed.APP_PUBLIC_URL` is
|
|
@@ -26,6 +27,7 @@ instead of keeping a hand-written copy of every feature's variables.
|
|
|
26
27
|
| `smtpEnvSchema` | `channel-email` | `createChannelEmailFeature()` |
|
|
27
28
|
| `s3EnvSchema`, `createS3EnvSchema({ prefix })` | `file-provider-s3` | `createFileProviderS3Feature({ fromEnv: true })` |
|
|
28
29
|
| `stripeEnvSchema({ prices })` | `subscription-stripe` | app (`extend`); the feature registers the bridge keys |
|
|
30
|
+
| `dodoEnvSchema({ prices })` | `subscription-dodo` | app (`extend`); the feature registers the bridge keys |
|
|
29
31
|
| `mfaTokenSecretsEnvSchema` | `auth-mfa` | `createAuthMfaFeature()` |
|
|
30
32
|
| `buildInfoEnvSchema`, `demoAdminEnvSchema` | `foundation-shared` | app (`extend`) |
|
|
31
33
|
| `prometheusMetricsEnvSchema` | `framework/observability` | app (`extend`) |
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
demoAdminEnvSchema,
|
|
16
16
|
} from "@cosmicdrift/kumiko-bundled-features/foundation-shared";
|
|
17
17
|
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
18
|
+
import { dodoEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-dodo";
|
|
18
19
|
import { stripeEnvSchema } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
|
|
19
20
|
import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";
|
|
20
21
|
import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
|
|
@@ -36,6 +37,7 @@ const appEnvSchema = z.object({
|
|
|
36
37
|
...buildInfoEnvSchema.shape,
|
|
37
38
|
...demoAdminEnvSchema.shape,
|
|
38
39
|
...stripeEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
40
|
+
...dodoEnvSchema({ prices: ["PRO", "TEAM"] }).shape,
|
|
39
41
|
...createS3EnvSchema({ prefix: "RELEASES_S3_" }).shape,
|
|
40
42
|
APP_PUBLIC_URL: z.url().describe("Public origin of the app, used in mail links."),
|
|
41
43
|
});
|
|
@@ -72,7 +72,7 @@ await runProdApp({
|
|
|
72
72
|
// Required (1): routes run anonymous, need tenant resolution
|
|
73
73
|
anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
|
|
74
74
|
// Required (2): boot check + internal lookup use ctx.templateResolver
|
|
75
|
-
extraContext: ({ db }) => ({
|
|
75
|
+
extraContext: ({ db, registry }) => ({
|
|
76
76
|
templateResolver: createTemplateResolverApi(db, registry),
|
|
77
77
|
}),
|
|
78
78
|
});
|
|
@@ -28,6 +28,24 @@ exhausted) and no bucket is charged. Each `per` may appear once; an empty list f
|
|
|
28
28
|
An anonymous handler needs every bucket to be ip-keyed, and `{ disabled: true, reason }`
|
|
29
29
|
stays a single declaration.
|
|
30
30
|
|
|
31
|
+
## Cost per payload element
|
|
32
|
+
|
|
33
|
+
A bulk handler books one token per item instead of one per request. Declare `rateLimitCost`
|
|
34
|
+
next to `rateLimit`:
|
|
35
|
+
|
|
36
|
+
```ts illustration
|
|
37
|
+
r.writeHandler({
|
|
38
|
+
// ...
|
|
39
|
+
rateLimit: { per: "tenant+handler", limit: 20, windowSeconds: 600 },
|
|
40
|
+
rateLimitCost: (payload) => payload.files.length,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The cost is computed from the validated payload and booked on every declared bucket, so an
|
|
45
|
+
invalid payload costs nothing and a rejected request consumes nothing. Results below 1 count
|
|
46
|
+
as 1. Because it books after the parse, boot rejects `rateLimitCost` on anonymous or ip-limited
|
|
47
|
+
handlers.
|
|
48
|
+
|
|
31
49
|
## Per-payload limits
|
|
32
50
|
|
|
33
51
|
A public write handler that mails an address typed by the caller is also limited per
|
|
@@ -33,5 +33,17 @@ export function createRateLimitShowcaseFeature(): FeatureDefinition {
|
|
|
33
33
|
{ per: "tenant+handler", limit: 3, windowSeconds: 3600 },
|
|
34
34
|
],
|
|
35
35
|
});
|
|
36
|
+
|
|
37
|
+
// Bulk upload: a request books one token per item instead of one per request. The cost
|
|
38
|
+
// comes from the validated payload, so a schema-invalid request costs nothing; a request
|
|
39
|
+
// that would exceed the remaining budget is rejected and consumes nothing.
|
|
40
|
+
r.writeHandler({
|
|
41
|
+
name: "bulk-upload",
|
|
42
|
+
schema: z.object({ files: z.array(z.string()).min(1).max(20) }),
|
|
43
|
+
handler: async () => ({ isSuccess: true as const, data: {} }),
|
|
44
|
+
access: { roles: ["Admin", "User"] },
|
|
45
|
+
rateLimit: { per: "tenant+handler", limit: 20, windowSeconds: 600 },
|
|
46
|
+
rateLimitCost: (payload) => payload.files.length,
|
|
47
|
+
});
|
|
36
48
|
});
|
|
37
49
|
}
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
|
-
import { decryptStoredPii } from "../../shared/index.ts";
|
|
3
|
-
|
|
4
|
-
// findForAuth returns the stored column, which is ciphertext under an active KMS;
|
|
5
|
-
// channel-email refuses to mail a ciphertext address. Null for an erased subject.
|
|
6
|
-
export async function openStoredUserEmail(
|
|
7
|
-
stored: string,
|
|
8
|
-
fallbackRequestId: string,
|
|
9
|
-
): Promise<string | null> {
|
|
10
|
-
const email = await decryptStoredPii(stored, "email", fallbackRequestId);
|
|
11
|
-
return email === PII_ERASED_SENTINEL ? null : email;
|
|
12
|
-
}
|