@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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
// kumiko-lint-ignore cross-feature-import client-only types, the feature's server barrel has no web/ re-export
|
|
10
10
|
import {
|
|
11
|
+
failureFromErrorField,
|
|
11
12
|
type LoginFailure,
|
|
12
13
|
type LoginResponse,
|
|
13
14
|
toLoginResponse,
|
|
@@ -36,13 +37,7 @@ export async function verifyMfaChallenge(
|
|
|
36
37
|
token?: string;
|
|
37
38
|
user?: LoginResponse["user"];
|
|
38
39
|
landingPath?: string;
|
|
39
|
-
error?:
|
|
40
|
-
| {
|
|
41
|
-
code?: string;
|
|
42
|
-
message?: string;
|
|
43
|
-
details?: { reason?: string; retryAfterSeconds?: number };
|
|
44
|
-
}
|
|
45
|
-
| string;
|
|
40
|
+
error?: unknown;
|
|
46
41
|
};
|
|
47
42
|
if (body.isSuccess === true && body.token !== undefined && body.user !== undefined) {
|
|
48
43
|
return {
|
|
@@ -50,20 +45,7 @@ export async function verifyMfaChallenge(
|
|
|
50
45
|
data: toLoginResponse(body.token, body.user, body.landingPath),
|
|
51
46
|
};
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
if (typeof err === "string") {
|
|
55
|
-
return { kind: "failure", error: { reason: err } };
|
|
56
|
-
}
|
|
57
|
-
const reason = err?.details?.reason ?? err?.code ?? "mfa_verify_failed";
|
|
58
|
-
const retry = err?.details?.retryAfterSeconds;
|
|
59
|
-
return {
|
|
60
|
-
kind: "failure",
|
|
61
|
-
error: {
|
|
62
|
-
reason,
|
|
63
|
-
...(err?.message !== undefined && { message: err.message }),
|
|
64
|
-
...(retry !== undefined && { retryAfterSeconds: retry }),
|
|
65
|
-
},
|
|
66
|
-
};
|
|
48
|
+
return { kind: "failure", error: failureFromErrorField(body.error, "mfa_verify_failed") };
|
|
67
49
|
}
|
|
68
50
|
|
|
69
51
|
export type MfaSetupPreauthStart = {
|
|
@@ -101,13 +83,7 @@ export async function startMfaSetupPreauth(
|
|
|
101
83
|
setupToken?: string;
|
|
102
84
|
otpauthUri?: string;
|
|
103
85
|
recoveryCodes?: readonly string[];
|
|
104
|
-
error?:
|
|
105
|
-
| {
|
|
106
|
-
code?: string;
|
|
107
|
-
message?: string;
|
|
108
|
-
details?: { reason?: string; retryAfterSeconds?: number };
|
|
109
|
-
}
|
|
110
|
-
| string;
|
|
86
|
+
error?: unknown;
|
|
111
87
|
};
|
|
112
88
|
if (
|
|
113
89
|
body.isSuccess === true &&
|
|
@@ -124,20 +100,7 @@ export async function startMfaSetupPreauth(
|
|
|
124
100
|
},
|
|
125
101
|
};
|
|
126
102
|
}
|
|
127
|
-
|
|
128
|
-
if (typeof err === "string") {
|
|
129
|
-
return { kind: "failure", error: { reason: err } };
|
|
130
|
-
}
|
|
131
|
-
const reason = err?.details?.reason ?? err?.code ?? "setup_failed";
|
|
132
|
-
const retry = err?.details?.retryAfterSeconds;
|
|
133
|
-
return {
|
|
134
|
-
kind: "failure",
|
|
135
|
-
error: {
|
|
136
|
-
reason,
|
|
137
|
-
...(err?.message !== undefined && { message: err.message }),
|
|
138
|
-
...(retry !== undefined && { retryAfterSeconds: retry }),
|
|
139
|
-
},
|
|
140
|
-
};
|
|
103
|
+
return { kind: "failure", error: failureFromErrorField(body.error, "setup_failed") };
|
|
141
104
|
}
|
|
142
105
|
|
|
143
106
|
// Browser side of POST /api/auth/mfa/preauth-confirm — completes both the
|
|
@@ -165,13 +128,7 @@ export async function confirmMfaSetupPreauth(
|
|
|
165
128
|
token?: string;
|
|
166
129
|
user?: LoginResponse["user"];
|
|
167
130
|
landingPath?: string;
|
|
168
|
-
error?:
|
|
169
|
-
| {
|
|
170
|
-
code?: string;
|
|
171
|
-
message?: string;
|
|
172
|
-
details?: { reason?: string; retryAfterSeconds?: number };
|
|
173
|
-
}
|
|
174
|
-
| string;
|
|
131
|
+
error?: unknown;
|
|
175
132
|
};
|
|
176
133
|
if (body.isSuccess === true && body.token !== undefined && body.user !== undefined) {
|
|
177
134
|
return {
|
|
@@ -179,18 +136,5 @@ export async function confirmMfaSetupPreauth(
|
|
|
179
136
|
data: toLoginResponse(body.token, body.user, body.landingPath),
|
|
180
137
|
};
|
|
181
138
|
}
|
|
182
|
-
|
|
183
|
-
if (typeof err === "string") {
|
|
184
|
-
return { kind: "failure", error: { reason: err } };
|
|
185
|
-
}
|
|
186
|
-
const reason = err?.details?.reason ?? err?.code ?? "mfa_setup_confirm_failed";
|
|
187
|
-
const retry = err?.details?.retryAfterSeconds;
|
|
188
|
-
return {
|
|
189
|
-
kind: "failure",
|
|
190
|
-
error: {
|
|
191
|
-
reason,
|
|
192
|
-
...(err?.message !== undefined && { message: err.message }),
|
|
193
|
-
...(retry !== undefined && { retryAfterSeconds: retry }),
|
|
194
|
-
},
|
|
195
|
-
};
|
|
139
|
+
return { kind: "failure", error: failureFromErrorField(body.error, "mfa_setup_confirm_failed") };
|
|
196
140
|
}
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.394.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "authorizeOneOffCheckout hook gates one-off checkouts per tenant",
|
|
6
|
+
"detail": "BillingFoundationOptions.authorizeOneOffCheckout runs before every one-off (mode payment) checkout, after the oneOffPriceIds gate and before any provider call. A rejection becomes an UnprocessableError with the hook's reason as code. Without the hook the behavior is unchanged."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.394.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "One-off checkouts reuse the tenant's provider customer",
|
|
12
|
+
"detail": "create-checkout-session with mode payment and no providerCustomerId now passes the customer id of the tenant's own subscription at the same provider, so Stripe also receives the customer on one-off checkouts. The foreign-customer guard still applies."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.394.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "PaymentEvent carries the bought quantity",
|
|
18
|
+
"detail": "PaymentEvent.quantity (absent means 1) reaches the stored payment-received event payload, which stays readable for events stored before. Apps that grant credits per pack have to multiply by it. No read-model column."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.394.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "Plan-switch preview shows the recurring price gross",
|
|
24
|
+
"detail": "PlanSwitchPreview.recurringAmount and recurringTax carry the provider's gross recurring price; the confirm dialog shows them with the tax when set and falls back to the catalog price otherwise. subscription-paddle fills them from recurring_transaction_details."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.394.0",
|
|
28
|
+
"type": "improvement",
|
|
29
|
+
"title": "Plan cards can show a note under the price",
|
|
30
|
+
"detail": "`BillingPlanCatalog.priceNoteKey` (an app i18n key) renders a small note under the price of every plan card, e.g. \"plus VAT\" when catalog prices are net. `PlanCardPrice` gets an optional `note`."
|
|
31
|
+
},
|
|
2
32
|
{
|
|
3
33
|
"version": "0.390.0",
|
|
4
34
|
"type": "fix",
|
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
UnprocessableError,
|
|
13
13
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
14
14
|
import { getSubscriptionForTenant } from "./get-subscription-for-tenant.ts";
|
|
15
|
-
import
|
|
15
|
+
import { assertOneOffAuthorized, assertOneOffPriceAllowed } from "./one-off-checkout-gate.ts";
|
|
16
|
+
import type {
|
|
17
|
+
BillingFoundationOptions,
|
|
18
|
+
BillingPlanCatalog,
|
|
19
|
+
SubscriptionProviderPlugin,
|
|
20
|
+
} from "./types.ts";
|
|
16
21
|
import {
|
|
17
22
|
BILLING_FOUNDATION_FEATURE,
|
|
18
23
|
isSubscriptionBlockingCheckout,
|
|
@@ -264,6 +269,7 @@ export type OpenCheckoutOptions = {
|
|
|
264
269
|
readonly baseUrl?: string;
|
|
265
270
|
readonly catalog?: BillingPlanCatalog;
|
|
266
271
|
readonly now: () => Temporal.Instant;
|
|
272
|
+
readonly authorizeOneOffCheckout?: BillingFoundationOptions["authorizeOneOffCheckout"];
|
|
267
273
|
/** The caller already ran `assertBillingEnabled` under its own handler name
|
|
268
274
|
* (before a costly provider lookup), so the gate is not repeated here. */
|
|
269
275
|
readonly billingEnabledChecked?: boolean;
|
|
@@ -311,21 +317,6 @@ function assertSubscriptionPriceAllowed(
|
|
|
311
317
|
}
|
|
312
318
|
}
|
|
313
319
|
|
|
314
|
-
// Missing/empty oneOffPriceIds rejects every payment checkout: same
|
|
315
|
-
// "no gate configured yet = closed" default as the priceToTier gate.
|
|
316
|
-
function assertOneOffPriceAllowed(
|
|
317
|
-
plugin: SubscriptionProviderPlugin,
|
|
318
|
-
input: OpenCheckoutInput,
|
|
319
|
-
): void {
|
|
320
|
-
const oneOffPriceIds = plugin.oneOffPriceIds ?? [];
|
|
321
|
-
if (!oneOffPriceIds.includes(input.priceId)) {
|
|
322
|
-
throw new UnprocessableError("unknown_price", {
|
|
323
|
-
i18nKey: "billing-foundation.errors.unknownPrice",
|
|
324
|
-
message: `subscription-foundation: priceId "${input.priceId}" is not in provider "${input.providerName}"'s oneOffPriceIds`,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
320
|
type CheckoutCapableProvider = SubscriptionProviderPlugin & {
|
|
330
321
|
readonly createCheckoutSession: NonNullable<SubscriptionProviderPlugin["createCheckoutSession"]>;
|
|
331
322
|
};
|
|
@@ -373,6 +364,7 @@ export async function assertCheckoutAllowed(
|
|
|
373
364
|
: await assertNoActiveSubscription(ctx, options.now());
|
|
374
365
|
} else {
|
|
375
366
|
assertOneOffPriceAllowed(plugin, input);
|
|
367
|
+
await assertOneOffAuthorized(ctx, options.authorizeOneOffCheckout, input);
|
|
376
368
|
}
|
|
377
369
|
|
|
378
370
|
// Rejects a foreign tenant's provider-customer id — otherwise the checkout
|
|
@@ -105,6 +105,8 @@ export const paymentEventPayloadSchema = z.object({
|
|
|
105
105
|
// subscriptionEventPayloadSchema's same rationale.
|
|
106
106
|
providerCustomerId: z.string().min(1).max(1000),
|
|
107
107
|
priceId: z.string().min(1).max(200),
|
|
108
|
+
// Optional so payloads stored before quantity existed still parse; absent = 1.
|
|
109
|
+
quantity: z.number().int().positive().optional(),
|
|
108
110
|
consentId: z.string().min(1).max(100).optional(),
|
|
109
111
|
});
|
|
110
112
|
export type PaymentEventPayload = z.infer<typeof paymentEventPayloadSchema>;
|
|
@@ -138,7 +138,7 @@ export function createBillingFoundationFeature<TTier extends string = string>(
|
|
|
138
138
|
BILLING_FOUNDATION_FEATURE,
|
|
139
139
|
(r) => {
|
|
140
140
|
r.describe(
|
|
141
|
-
"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-
|
|
141
|
+
"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-paddle` unless you are writing a new payment provider.",
|
|
142
142
|
);
|
|
143
143
|
r.uiHints({
|
|
144
144
|
displayLabel: "Billing · Foundation",
|
package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
prepareConsent,
|
|
40
40
|
recordConsent,
|
|
41
41
|
} from "../consumer-protection/consent-gate.ts";
|
|
42
|
+
import { withOwnProviderCustomer } from "../one-off-customer.ts";
|
|
42
43
|
import type {
|
|
43
44
|
ProviderPrice,
|
|
44
45
|
ResolvedBillingFoundationOptions,
|
|
@@ -86,8 +87,13 @@ export function createCheckoutSessionHandler(
|
|
|
86
87
|
// @cast-boundary engine-payload — dispatcher-zod-validated payload
|
|
87
88
|
const payload = event.payload as CreateCheckoutSessionPayload;
|
|
88
89
|
|
|
89
|
-
const openOptions = {
|
|
90
|
-
|
|
90
|
+
const openOptions = {
|
|
91
|
+
baseUrl: options.baseUrl,
|
|
92
|
+
catalog: options.catalog,
|
|
93
|
+
now: options.now,
|
|
94
|
+
authorizeOneOffCheckout: options.authorizeOneOffCheckout,
|
|
95
|
+
};
|
|
96
|
+
const requestedInput: OpenCheckoutInput = {
|
|
91
97
|
providerName: payload.providerName,
|
|
92
98
|
priceId: payload.priceId,
|
|
93
99
|
successUrl: payload.successUrl,
|
|
@@ -95,6 +101,7 @@ export function createCheckoutSessionHandler(
|
|
|
95
101
|
...(payload.providerCustomerId && { providerCustomerId: payload.providerCustomerId }),
|
|
96
102
|
...(payload.mode && { mode: payload.mode }),
|
|
97
103
|
};
|
|
104
|
+
const checkoutInput = await withOwnProviderCustomer(ctx, requestedInput);
|
|
98
105
|
|
|
99
106
|
if (!consumerProtection) {
|
|
100
107
|
const result = await openCheckout(ctx, openOptions, checkoutInput);
|
package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts
CHANGED
|
@@ -31,6 +31,7 @@ export const processPaymentEventSchema = z.object({
|
|
|
31
31
|
providerCheckoutId: z.string().min(1).max(200).optional(),
|
|
32
32
|
providerCustomerId: z.string().min(1).max(200),
|
|
33
33
|
priceId: z.string().min(1).max(200),
|
|
34
|
+
quantity: z.number().int().positive().optional(),
|
|
34
35
|
consentId: z.string().min(1).max(100).optional(),
|
|
35
36
|
});
|
|
36
37
|
type ProcessPaymentEventPayload = z.infer<typeof processPaymentEventSchema>;
|
|
@@ -97,6 +98,7 @@ export const processPaymentEventHandler: WriteHandlerDef = {
|
|
|
97
98
|
providerName: payload.providerName,
|
|
98
99
|
providerCustomerId: encryptedFields.providerCustomerId,
|
|
99
100
|
priceId: payload.priceId,
|
|
101
|
+
...(payload.quantity !== undefined && { quantity: payload.quantity }),
|
|
100
102
|
...(payload.consentId !== undefined && { consentId: payload.consentId }),
|
|
101
103
|
};
|
|
102
104
|
const headers: PaymentEventHeaders = {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Gates for mode:"payment" checkouts, run before any provider call.
|
|
2
|
+
|
|
3
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
|
+
import type { BillingFoundationOptions, SubscriptionProviderPlugin } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
type OneOffCheckoutInput = { readonly providerName: string; readonly priceId: string };
|
|
8
|
+
|
|
9
|
+
// Missing/empty oneOffPriceIds rejects every payment checkout: same
|
|
10
|
+
// "no gate configured yet = closed" default as the priceToTier gate.
|
|
11
|
+
export function assertOneOffPriceAllowed(
|
|
12
|
+
plugin: SubscriptionProviderPlugin,
|
|
13
|
+
input: OneOffCheckoutInput,
|
|
14
|
+
): void {
|
|
15
|
+
const oneOffPriceIds = plugin.oneOffPriceIds ?? [];
|
|
16
|
+
if (!oneOffPriceIds.includes(input.priceId)) {
|
|
17
|
+
throw new UnprocessableError("unknown_price", {
|
|
18
|
+
i18nKey: "billing-foundation.errors.unknownPrice",
|
|
19
|
+
message: `subscription-foundation: priceId "${input.priceId}" is not in provider "${input.providerName}"'s oneOffPriceIds`,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The app's own gate (for example a tier check): without it every price in
|
|
25
|
+
// oneOffPriceIds is open to any TenantAdmin.
|
|
26
|
+
export async function assertOneOffAuthorized(
|
|
27
|
+
ctx: HandlerContext,
|
|
28
|
+
authorize: BillingFoundationOptions["authorizeOneOffCheckout"],
|
|
29
|
+
input: OneOffCheckoutInput,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const verdict = authorize
|
|
32
|
+
? await authorize(ctx, {
|
|
33
|
+
tenantId: ctx.user.tenantId,
|
|
34
|
+
providerName: input.providerName,
|
|
35
|
+
priceId: input.priceId,
|
|
36
|
+
})
|
|
37
|
+
: { allowed: true as const };
|
|
38
|
+
if (!verdict.allowed) {
|
|
39
|
+
throw new UnprocessableError(verdict.reason, {
|
|
40
|
+
i18nKey: verdict.i18nKey,
|
|
41
|
+
message: `billing-foundation: authorizeOneOffCheckout rejected the one-off checkout (${verdict.reason})`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { OpenCheckoutInput } from "./checkout-core.ts";
|
|
3
|
+
import { getSubscriptionForTenant } from "./get-subscription-for-tenant.ts";
|
|
4
|
+
|
|
5
|
+
/** A one-off checkout without an explicit customer reuses the provider customer of the
|
|
6
|
+
* tenant's own subscription at the same provider, so the buyer is not asked again and
|
|
7
|
+
* does not become a second customer. The foreign-customer guard still runs afterwards. */
|
|
8
|
+
export async function withOwnProviderCustomer(
|
|
9
|
+
ctx: HandlerContext,
|
|
10
|
+
input: OpenCheckoutInput,
|
|
11
|
+
): Promise<OpenCheckoutInput> {
|
|
12
|
+
if (input.mode !== "payment" || input.providerCustomerId) return input;
|
|
13
|
+
const own = await getSubscriptionForTenant(ctx, ctx.user.tenantId);
|
|
14
|
+
return own && own.providerName === input.providerName && own.providerCustomerId
|
|
15
|
+
? { ...input, providerCustomerId: own.providerCustomerId }
|
|
16
|
+
: input;
|
|
17
|
+
}
|
|
@@ -267,5 +267,6 @@ export function toBillingPlansResult(
|
|
|
267
267
|
plans: views.plans,
|
|
268
268
|
...(views.inAppSwitch && { inAppPlanSwitch: true as const }),
|
|
269
269
|
...(catalog.contact !== undefined && { contact: catalog.contact }),
|
|
270
|
+
...(catalog.priceNoteKey !== undefined && { priceNoteKey: catalog.priceNoteKey }),
|
|
270
271
|
};
|
|
271
272
|
}
|
|
@@ -129,6 +129,8 @@ export type PaymentEvent = {
|
|
|
129
129
|
readonly providerCustomerId: string;
|
|
130
130
|
/** Provider's own price/plan-ID of the purchased item. */
|
|
131
131
|
readonly priceId: string;
|
|
132
|
+
/** Number of units bought; absent = 1. */
|
|
133
|
+
readonly quantity?: number;
|
|
132
134
|
};
|
|
133
135
|
|
|
134
136
|
// =============================================================================
|
|
@@ -368,6 +370,10 @@ export type PlanSwitchPreview = {
|
|
|
368
370
|
readonly currency: string;
|
|
369
371
|
/** Credit for the unused time on the old plan, smallest currency unit. */
|
|
370
372
|
readonly credit: number;
|
|
373
|
+
/** Recurring price per period as charged by the provider, smallest unit, tax included. */
|
|
374
|
+
readonly recurringAmount?: number;
|
|
375
|
+
/** Tax part of `recurringAmount`; null when the provider reports none. */
|
|
376
|
+
readonly recurringTax?: number | null;
|
|
371
377
|
readonly effectiveAt: string;
|
|
372
378
|
};
|
|
373
379
|
|
|
@@ -445,6 +451,8 @@ export type BillingPlanCatalog<TTier extends string = string> = {
|
|
|
445
451
|
readonly providerName?: string;
|
|
446
452
|
/** Rendered as the last card, no price, visible to `viewRoles` regardless of `purchaseRoles`. */
|
|
447
453
|
readonly contact?: BillingPlanContactCard;
|
|
454
|
+
/** App i18n key for a small note under every card's price, e.g. "plus VAT" when catalog prices are net. */
|
|
455
|
+
readonly priceNoteKey?: string;
|
|
448
456
|
};
|
|
449
457
|
|
|
450
458
|
export type LegalLinkSet = {
|
|
@@ -504,6 +512,16 @@ export type ConsumerProtectionOptions = {
|
|
|
504
512
|
readonly legalLinks: LegalLinkSet | Readonly<Record<ConsentLocale, LegalLinkSet>>;
|
|
505
513
|
};
|
|
506
514
|
|
|
515
|
+
export type OneOffCheckoutRequest = {
|
|
516
|
+
readonly tenantId: TenantId;
|
|
517
|
+
readonly providerName: string;
|
|
518
|
+
readonly priceId: string;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
export type OneOffCheckoutVerdict =
|
|
522
|
+
| { readonly allowed: true }
|
|
523
|
+
| { readonly allowed: false; readonly reason: string; readonly i18nKey: string };
|
|
524
|
+
|
|
507
525
|
export type BillingFoundationOptions<TTier extends string = string> = {
|
|
508
526
|
/** Absolute http(s) URL. Its origin is the only redirect origin
|
|
509
527
|
* create-checkout-session/create-portal-session (when set) and the plan
|
|
@@ -513,6 +531,12 @@ export type BillingFoundationOptions<TTier extends string = string> = {
|
|
|
513
531
|
* NOT via `new URL(path, baseUrl)` (which would drop the prefix). */
|
|
514
532
|
readonly baseUrl?: string;
|
|
515
533
|
readonly catalog?: BillingPlanCatalog<TTier>;
|
|
534
|
+
/** Runs before every one-off (mode "payment") checkout, after the oneOffPriceIds gate and
|
|
535
|
+
* before any provider call; a rejection becomes an UnprocessableError with `reason` as code. */
|
|
536
|
+
readonly authorizeOneOffCheckout?: (
|
|
537
|
+
ctx: HandlerContext,
|
|
538
|
+
request: OneOffCheckoutRequest,
|
|
539
|
+
) => Promise<OneOffCheckoutVerdict>;
|
|
516
540
|
/** Enables the consent gate on checkout; requires `baseUrl`. */
|
|
517
541
|
readonly consumerProtection?: ConsumerProtectionOptions;
|
|
518
542
|
/** Injectable clock for the stale-incomplete-subscription check
|
|
@@ -587,6 +611,8 @@ export type BillingPlansResult = {
|
|
|
587
611
|
readonly inAppPlanSwitch?: true;
|
|
588
612
|
/** Present only when `catalog.contact` is configured. */
|
|
589
613
|
readonly contact?: BillingPlanContactCard;
|
|
614
|
+
/** Present only when `catalog.priceNoteKey` is configured. */
|
|
615
|
+
readonly priceNoteKey?: string;
|
|
590
616
|
/** Present only when `consumerProtection` is configured. */
|
|
591
617
|
readonly consumerProtection?: {
|
|
592
618
|
readonly consentTexts: Readonly<
|
|
@@ -238,6 +238,7 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
|
|
|
238
238
|
: {
|
|
239
239
|
amount: formatMoney(shownPrice.unitAmount, shownPrice.currency, locale),
|
|
240
240
|
...(fromPrice !== undefined && { prefix: t("billing-foundation.plans.priceFrom") }),
|
|
241
|
+
...(result.priceNoteKey !== undefined && { note: t(result.priceNoteKey) }),
|
|
241
242
|
...(key !== undefined && {
|
|
242
243
|
period: t(key, {
|
|
243
244
|
count: new Intl.NumberFormat(locale).format(shownPrice.intervalCount ?? 1),
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { SubscriptionFoundationHandlers, SubscriptionFoundationQueries } from "./constants.ts";
|
|
16
16
|
import { resolveConsentLocale } from "./consumer-protection/consent-locale.ts";
|
|
17
17
|
import { intervalKey } from "./plan-price.ts";
|
|
18
|
+
import { recurringAmountsOf } from "./plan-switch-recurring.ts";
|
|
18
19
|
|
|
19
20
|
type ConsumerProtectionView = NonNullable<BillingPlansResult["consumerProtection"]>;
|
|
20
21
|
|
|
@@ -99,13 +100,18 @@ export function PlanSwitchConfirmDialog({
|
|
|
99
100
|
const reply = previewQuery.data;
|
|
100
101
|
const money = (amount: number, currency: string) => formatMoney(amount, currency, locale);
|
|
101
102
|
const recurringKey = reply === null ? undefined : intervalKey(reply.target);
|
|
103
|
+
const recurring = reply === null ? null : recurringAmountsOf(reply);
|
|
102
104
|
const recurringPrice =
|
|
103
|
-
reply === null
|
|
105
|
+
reply === null || recurring === null
|
|
104
106
|
? ""
|
|
105
|
-
: `${money(
|
|
107
|
+
: `${money(recurring.amount, recurring.currency)}${
|
|
106
108
|
recurringKey === undefined
|
|
107
109
|
? ""
|
|
108
110
|
: ` ${t(recurringKey, { count: new Intl.NumberFormat(locale).format(reply.target.intervalCount ?? 1) })}`
|
|
111
|
+
}${
|
|
112
|
+
recurring.tax === null
|
|
113
|
+
? ""
|
|
114
|
+
: ` (${t("billing-foundation.planSwitch.includesTax", { tax: money(recurring.tax, recurring.currency) })})`
|
|
109
115
|
}`;
|
|
110
116
|
|
|
111
117
|
return (
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PlanSwitchPreview } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
type RecurringSource = {
|
|
4
|
+
readonly preview: PlanSwitchPreview;
|
|
5
|
+
readonly target: { readonly unitAmount: number; readonly currency: string };
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type RecurringAmounts = {
|
|
9
|
+
readonly amount: number;
|
|
10
|
+
readonly currency: string;
|
|
11
|
+
readonly tax: number | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** The provider's gross recurring price when it reports one, else the catalog's unit amount. */
|
|
15
|
+
export function recurringAmountsOf({ preview, target }: RecurringSource): RecurringAmounts {
|
|
16
|
+
return preview.recurringAmount === undefined
|
|
17
|
+
? { amount: target.unitAmount, currency: target.currency, tax: null }
|
|
18
|
+
: {
|
|
19
|
+
amount: preview.recurringAmount,
|
|
20
|
+
currency: preview.currency,
|
|
21
|
+
tax: preview.recurringTax ?? null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -105,6 +105,7 @@ export function createSubscriptionWebhookRoute(options: SubscriptionWebhookRoute
|
|
|
105
105
|
providerName: parsed.providerName,
|
|
106
106
|
providerCustomerId: parsed.providerCustomerId,
|
|
107
107
|
priceId: parsed.priceId,
|
|
108
|
+
...(parsed.quantity !== undefined && { quantity: parsed.quantity }),
|
|
108
109
|
...(parsed.consentId !== undefined && { consentId: parsed.consentId }),
|
|
109
110
|
...(parsed.providerCheckoutId !== undefined && {
|
|
110
111
|
providerCheckoutId: parsed.providerCheckoutId,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.394.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Add CapSpec.showLimit to hide a cap's limit from the dashboard",
|
|
6
|
+
"detail": "showLimit: false makes caps:usage send limit null so the card shows the counter only; cap-counter enforcement is unchanged."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.392.1",
|
|
4
10
|
"type": "fix",
|
|
@@ -81,7 +81,7 @@ export function createCapsUsageQuery(
|
|
|
81
81
|
const rows: CapUsageWithMeta[] = await Promise.all(
|
|
82
82
|
caps.map(async (cap) => {
|
|
83
83
|
const used = await cap.usage(db, targetTenantId);
|
|
84
|
-
const limit = await cap.limit(tier, { config });
|
|
84
|
+
const limit = cap.showLimit === false ? null : await cap.limit(tier, { config });
|
|
85
85
|
const base = {
|
|
86
86
|
id: cap.id,
|
|
87
87
|
label: cap.label,
|
|
@@ -90,8 +90,8 @@ export function createCapsUsageQuery(
|
|
|
90
90
|
...(cap.icon !== undefined && { icon: cap.icon }),
|
|
91
91
|
...(cap.accentColor !== undefined && { accentColor: cap.accentColor }),
|
|
92
92
|
};
|
|
93
|
-
if (used === null) {
|
|
94
|
-
return { ...base, used
|
|
93
|
+
if (used === null || cap.showLimit === false) {
|
|
94
|
+
return { ...base, used, fraction: 0, tone: "default" as const, percent: null };
|
|
95
95
|
}
|
|
96
96
|
const fraction = computeFraction(used, limit);
|
|
97
97
|
return {
|
|
@@ -38,6 +38,8 @@ export type CapSpec = {
|
|
|
38
38
|
tenantIds: readonly TenantId[],
|
|
39
39
|
) => Promise<Map<TenantId, number | null>>;
|
|
40
40
|
readonly unit?: CapUnit;
|
|
41
|
+
/** `false` shows the counter only: the limit is not sent to the client; enforcement stays with cap-counter. Default true. */
|
|
42
|
+
readonly showLimit?: boolean;
|
|
41
43
|
/** Icon chip on the dashboard cards. Optional — omitted renders the card
|
|
42
44
|
* without an icon, same as before this field existed. */
|
|
43
45
|
readonly icon?: CapIconKey;
|
package/packages/bundled-features/src/compliance-profiles/web/compliance-profile-catalog.tsx
CHANGED
|
@@ -22,6 +22,14 @@ type ListProfilesResponse = {
|
|
|
22
22
|
readonly profiles: readonly ProfileSummary[];
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// The select options above carry locale translations; the catalog reuses them so both
|
|
26
|
+
// name a profile alike. A profile without an option key keeps its source label.
|
|
27
|
+
function profileLabel(profile: ProfileSummary, t: (key: string) => string): string {
|
|
28
|
+
const optionKey = `compliance-profiles:entity:__action-form__:field:profileKey:option:${profile.key}`;
|
|
29
|
+
const translated = t(optionKey);
|
|
30
|
+
return translated === optionKey ? profile.label : translated;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export function ComplianceProfileCatalog(_props: ExtensionSectionProps): ReactNode {
|
|
26
34
|
const { Banner, Text } = usePrimitives();
|
|
27
35
|
const t = useTranslation();
|
|
@@ -46,7 +54,8 @@ export function ComplianceProfileCatalog(_props: ExtensionSectionProps): ReactNo
|
|
|
46
54
|
<ul className="flex flex-col gap-2 text-sm" data-testid="compliance-profile-catalog">
|
|
47
55
|
{(query.data?.profiles ?? []).map((profile) => (
|
|
48
56
|
<li key={profile.key} data-profile-key={profile.key}>
|
|
49
|
-
<strong>{profile
|
|
57
|
+
<strong>{profileLabel(profile, t)}</strong> — {profile.region} —{" "}
|
|
58
|
+
{profile.authorityContact}
|
|
50
59
|
</li>
|
|
51
60
|
))}
|
|
52
61
|
</ul>
|
|
@@ -13,9 +13,12 @@ export type FileRefContent = {
|
|
|
13
13
|
readonly contentType: string;
|
|
14
14
|
readonly tenantId: string;
|
|
15
15
|
readonly size: number;
|
|
16
|
+
/** As stored: personal data that may be PII ciphertext. Decrypt before showing or sending it. */
|
|
17
|
+
readonly fileName: string;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
type FileRefRow = {
|
|
21
|
+
readonly fileName: string;
|
|
19
22
|
readonly storageKey: string;
|
|
20
23
|
readonly mimeType: string;
|
|
21
24
|
readonly tenantId: string;
|
|
@@ -61,7 +64,6 @@ export class FileStorageUnavailableError extends Error {
|
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
// fileName of the row is never used: it is personal data and may be a PII ciphertext.
|
|
65
67
|
export async function readFileRefContent(args: {
|
|
66
68
|
readonly db: DbRunner | TenantDb;
|
|
67
69
|
readonly filesForTenant: (tenantId: string) => FileContext | undefined;
|
|
@@ -85,7 +87,8 @@ export async function readFileRefContent(args: {
|
|
|
85
87
|
});
|
|
86
88
|
if (row === undefined) return null;
|
|
87
89
|
const size = Number(row.size);
|
|
88
|
-
|
|
90
|
+
// A size that is not a number counts as too large, so the limit cannot be skipped.
|
|
91
|
+
if (args.maxBytes !== undefined && !(size <= args.maxBytes)) {
|
|
89
92
|
throw new FileRefTooLargeError(size);
|
|
90
93
|
}
|
|
91
94
|
const files = args.filesForTenant(row.tenantId);
|
|
@@ -93,5 +96,11 @@ export async function readFileRefContent(args: {
|
|
|
93
96
|
throw new FileStorageUnavailableError();
|
|
94
97
|
}
|
|
95
98
|
const content = await files.refFromFileRef(row).read();
|
|
96
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
content,
|
|
101
|
+
contentType: row.mimeType,
|
|
102
|
+
tenantId: row.tenantId,
|
|
103
|
+
size,
|
|
104
|
+
fileName: row.fileName,
|
|
105
|
+
};
|
|
97
106
|
}
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.394.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "createFileRefFromBytes takes a per-call maxSize",
|
|
6
|
+
"detail": "`CreateFileRefInput.maxSize` (bytes) replaces the route default for that call, so server-side generated files such as mail attachments can exceed the 10 MB upload default without raising it for browsers. A file field's own maxSize still applies first. `configuredMaxUploadBytes(registry)` returns the configured route limit in bytes, or undefined."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.394.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "canCallerReadFileRef answers the file read decision in handlers",
|
|
12
|
+
"detail": "`canCallerReadFileRef(ctx, fileRef)` applies the same rule as GET /files/:id: the configured accessGuard, otherwise the default guard with the configured privilegedRoles and the bound record's detail query. `createDefaultGuard` is exported from the files barrel. A handler that lists or links files no longer needs its own copy of the rule."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.394.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "Projection entities are server-side attach targets for files",
|
|
18
|
+
"detail": "An entity declared on a projection (r.projection with entity) is a read-model entity: a files field on it can be the attachTo target of createFileRefFromBytes and uploadFileBytesBeforeTransaction, and the file read check uses the owning feature's `<name>:detail` query for it. The registry gets getReadModelEntity and getReadModelEntityOwner; two projections declaring an entity for the same source fail the boot. POST /files still accepts only r.entity targets, so a client can never attach to a read model. deleteFileRefsBoundToRecord soft-deletes the fileRefs of one record for owners that remove it without an r.entity delete."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.379.0",
|
|
4
22
|
"type": "improvement",
|