@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.388.0",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.388.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"./billing-foundation": "./src/billing-foundation/index.ts",
|
|
64
64
|
"./billing-foundation/web": "./src/billing-foundation/web/index.ts",
|
|
65
65
|
"./subscription-stripe": "./src/subscription-stripe/index.ts",
|
|
66
|
+
"./subscription-dodo": "./src/subscription-dodo/index.ts",
|
|
66
67
|
"./subscription-mollie": "./src/subscription-mollie/index.ts",
|
|
67
68
|
"./foundation-shared": "./src/foundation-shared/index.ts",
|
|
68
69
|
"./auth-foundation": "./src/auth-foundation/index.ts",
|
|
@@ -359,6 +360,10 @@
|
|
|
359
360
|
"types": "./dist/subscription-stripe/index.d.ts",
|
|
360
361
|
"default": "./dist/subscription-stripe/index.js"
|
|
361
362
|
},
|
|
363
|
+
"./subscription-dodo": {
|
|
364
|
+
"types": "./dist/subscription-dodo/index.d.ts",
|
|
365
|
+
"default": "./dist/subscription-dodo/index.js"
|
|
366
|
+
},
|
|
362
367
|
"./subscription-mollie": {
|
|
363
368
|
"types": "./dist/subscription-mollie/index.d.ts",
|
|
364
369
|
"default": "./dist/subscription-mollie/index.js"
|
|
@@ -728,6 +733,8 @@
|
|
|
728
733
|
"@testing-library/user-event": "^14.6.1",
|
|
729
734
|
"@cosmicdrift/kumiko-locale-de": "workspace:*",
|
|
730
735
|
"@cosmicdrift/kumiko-locale-es": "workspace:*",
|
|
736
|
+
"@cosmicdrift/kumiko-locale-fr": "workspace:*",
|
|
737
|
+
"@cosmicdrift/kumiko-locale-it": "workspace:*",
|
|
731
738
|
"@cosmicdrift/kumiko-renderer-mail-html": "workspace:*",
|
|
732
739
|
"jsqr": "^1.4.0",
|
|
733
740
|
"unpdf": "^1.8.1",
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "dispatchToolCall documents which failures carry a code",
|
|
6
|
+
"detail": "Only handler and dispatcher failures carry a code; local validation errors such as an unknown tool or bad arguments come back without one, and the JSDoc now says so."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.366.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SessionUser, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { isKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
-
import {
|
|
3
|
+
import { runAsAgentToolCallEntry } from "@cosmicdrift/kumiko-framework/internal/request-context";
|
|
4
4
|
import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
5
5
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
6
|
import { FILTER_OPS, isRecord } from "./filter-ops.ts";
|
|
@@ -411,13 +411,13 @@ function dispatchClient(
|
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
/** Executes one LLM-issued tool call. Never throws
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
414
|
+
/** Executes one LLM-issued tool call. Never throws: every failure comes back as `{ ok: false }`
|
|
415
|
+
* so the agent loop can feed the error back to the model instead of crashing.
|
|
416
|
+
* Handler and dispatcher failures carry a `code` (429 adds `retryable`); client errors (< 500)
|
|
417
|
+
* keep their message, server failures (>= 500, unknown throws) are logged and returned as a
|
|
418
|
+
* generic message. Local validation errors (unknown tool, bad arguments) have no `code`.
|
|
419
|
+
* `request.user` must be the caller's real identity: server-kind tools run through the same
|
|
420
|
+
* permission-checked pipeline as an HTTP request. */
|
|
421
421
|
export async function dispatchToolCall(request: ToolCallRequest): Promise<ToolCallResult> {
|
|
422
422
|
const { dispatcher, user, toolName, input, dispatchTable, runId, toolCallId } = request;
|
|
423
423
|
const classifyFailure = createFailureClassifier(toolName, request.log ?? fallbackLog);
|
|
@@ -427,7 +427,7 @@ export async function dispatchToolCall(request: ToolCallRequest): Promise<ToolCa
|
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
try {
|
|
430
|
-
return await
|
|
430
|
+
return await runAsAgentToolCallEntry(async () => {
|
|
431
431
|
switch (descriptor.kind) {
|
|
432
432
|
case "search":
|
|
433
433
|
return await dispatchSearch(descriptor, dispatcher, user, toolName, input);
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Oversized audit cursor is documented as a 400 validation error",
|
|
6
|
+
"detail": "The cursor pattern comment now names the validation error (400) the API actually returns instead of 422."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Tests cover nested agent grants, erased subjects and anonymous change-password",
|
|
12
|
+
"detail": "Integration tests now check that a delegating tool handler keeps the cross-tenant-read marker closed, that an erased subject gets no consumer-protection mail, and that the anonymous change-password check runs once; the audit seed helper uses withEnv."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.384.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
|
|
21
21
|
const MAX_LIMIT = 100;
|
|
22
22
|
|
|
23
|
-
// At most 18 digits keeps the id inside bigint, so an oversized cursor or id is a
|
|
23
|
+
// At most 18 digits keeps the id inside bigint, so an oversized cursor or id is a validation error (400), not a 500.
|
|
24
24
|
const CURSOR_PATTERN = /^\d{1,18}$/;
|
|
25
25
|
|
|
26
26
|
export const auditListPayloadShape = {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Six-cell code input, autofocus, auto-submit and recovery switch",
|
|
6
|
+
"detail": "The MFA login, setup, disable and regenerate screens use the six-cell code input with autofocus. Login and setup submit automatically on the sixth digit; disable and regenerate keep the explicit confirm button. Verify, disable and regenerate can switch to a recovery-code field. New keys auth.mfa.verify.useRecoveryCode, useAuthenticatorCode, recoveryCode and recoverySubtitle in all locale packages; auth.mfa.disable.code now names the authenticator code only."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.379.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -8,7 +8,7 @@ import type { MfaTokenSecretsEnv } from "./token-secrets.ts";
|
|
|
8
8
|
|
|
9
9
|
export type BindMfaRevokeAllOtherSessions = (
|
|
10
10
|
revoker: (userId: string, currentSid: string | undefined) => Promise<number>,
|
|
11
|
-
) => void;
|
|
11
|
+
) => () => void;
|
|
12
12
|
|
|
13
13
|
// Reads the late-bind setter off a mounted auth-mfa feature's exports —
|
|
14
14
|
// mirrors sessions' own bindAutoRevokeFromFeature. run{Prod,Dev}App call
|
|
@@ -79,7 +79,7 @@ export function bindMfaTokenSecretsFromFeature(
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export type BindRevokeAllPatTokens = (revoker: (userId: string) => Promise<number>) => void;
|
|
82
|
+
export type BindRevokeAllPatTokens = (revoker: (userId: string) => Promise<number>) => () => void;
|
|
83
83
|
|
|
84
84
|
// Reads the late-bind setter off a mounted auth-mfa feature's exports —
|
|
85
85
|
// run{Prod,Dev}App call this once the personal-access-tokens feature (if
|
|
@@ -220,10 +220,16 @@ export function createAuthMfaFeature(
|
|
|
220
220
|
|
|
221
221
|
const bindRevokeAllOtherSessions: BindMfaRevokeAllOtherSessions = (revoker) => {
|
|
222
222
|
revokeAllOtherSessions = revoker;
|
|
223
|
+
return () => {
|
|
224
|
+
if (revokeAllOtherSessions === revoker) revokeAllOtherSessions = undefined;
|
|
225
|
+
};
|
|
223
226
|
};
|
|
224
227
|
|
|
225
228
|
const bindRevokeAllPatTokens: BindRevokeAllPatTokens = (revoker) => {
|
|
226
229
|
revokeAllPatTokens = revoker;
|
|
230
|
+
return () => {
|
|
231
|
+
if (revokeAllPatTokens === revoker) revokeAllPatTokens = undefined;
|
|
232
|
+
};
|
|
227
233
|
};
|
|
228
234
|
|
|
229
235
|
return {
|
|
@@ -13,6 +13,11 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
13
13
|
"auth.mfa.verify.submit": "Verify",
|
|
14
14
|
"auth.mfa.verify.submitting": "…",
|
|
15
15
|
"auth.mfa.verify.backToLogin": "Back to login",
|
|
16
|
+
"auth.mfa.verify.recoveryCode": "Recovery code",
|
|
17
|
+
"auth.mfa.verify.recoverySubtitle":
|
|
18
|
+
"Enter one of the recovery codes you saved when you set up two-factor authentication.",
|
|
19
|
+
"auth.mfa.verify.useAuthenticatorCode": "Use the code from your app",
|
|
20
|
+
"auth.mfa.verify.useRecoveryCode": "Use a recovery code instead",
|
|
16
21
|
"auth.mfa.errors.invalidCode": "Invalid code. Please try again.",
|
|
17
22
|
"auth.mfa.errors.invalidTotpCode": "Invalid code. Please try again.",
|
|
18
23
|
"auth.mfa.errors.invalidChallengeToken": "Your sign-in has expired. Please sign in again.",
|
|
@@ -43,7 +48,7 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
43
48
|
"auth.mfa.disable.title": "Disable two-factor authentication",
|
|
44
49
|
"auth.mfa.disable.description":
|
|
45
50
|
"Confirm with a code from your authenticator app or a recovery code. Your account will then be protected by your password alone.",
|
|
46
|
-
"auth.mfa.disable.code": "Code from your authenticator app
|
|
51
|
+
"auth.mfa.disable.code": "Code from your authenticator app",
|
|
47
52
|
"auth.mfa.disable.confirm": "Disable",
|
|
48
53
|
"auth.mfa.disable.cancel": "Cancel",
|
|
49
54
|
"auth.mfa.disable.trigger": "Disable two-factor authentication",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type MfaCodeOrRecoveryFieldProps = {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly codeLabel: string;
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly onCodeChange: (code: string) => void;
|
|
9
|
+
readonly useRecovery: boolean;
|
|
10
|
+
readonly onUseRecoveryChange: (useRecovery: boolean) => void;
|
|
11
|
+
readonly disabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function MfaCodeOrRecoveryField({
|
|
15
|
+
id,
|
|
16
|
+
codeLabel,
|
|
17
|
+
code,
|
|
18
|
+
onCodeChange,
|
|
19
|
+
useRecovery,
|
|
20
|
+
onUseRecoveryChange,
|
|
21
|
+
disabled,
|
|
22
|
+
}: MfaCodeOrRecoveryFieldProps): ReactNode {
|
|
23
|
+
const t = useTranslation();
|
|
24
|
+
const { Button, Field, Input } = usePrimitives();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<Field id={id} label={useRecovery ? t("auth.mfa.verify.recoveryCode") : codeLabel} required>
|
|
29
|
+
{useRecovery ? (
|
|
30
|
+
<Input
|
|
31
|
+
kind="text"
|
|
32
|
+
id={id}
|
|
33
|
+
name={id}
|
|
34
|
+
value={code}
|
|
35
|
+
onChange={onCodeChange}
|
|
36
|
+
disabled={disabled}
|
|
37
|
+
autoFocus
|
|
38
|
+
autoComplete="off"
|
|
39
|
+
/>
|
|
40
|
+
) : (
|
|
41
|
+
<Input
|
|
42
|
+
kind="code"
|
|
43
|
+
length={6}
|
|
44
|
+
id={id}
|
|
45
|
+
name={id}
|
|
46
|
+
value={code}
|
|
47
|
+
onChange={onCodeChange}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
autoFocus
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</Field>
|
|
53
|
+
<Button
|
|
54
|
+
type="button"
|
|
55
|
+
variant="link"
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
onClick={() => {
|
|
58
|
+
onUseRecoveryChange(!useRecovery);
|
|
59
|
+
onCodeChange("");
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{useRecovery
|
|
63
|
+
? t("auth.mfa.verify.useAuthenticatorCode")
|
|
64
|
+
: t("auth.mfa.verify.useRecoveryCode")}
|
|
65
|
+
</Button>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
13
13
|
import { type ReactNode, useState } from "react";
|
|
14
14
|
import { AuthMfaHandlers } from "./constants.ts";
|
|
15
|
+
import { MfaCodeOrRecoveryField } from "./mfa-code-or-recovery-field.tsx";
|
|
15
16
|
|
|
16
17
|
export type MfaDisableDialogProps = {
|
|
17
18
|
readonly open: boolean;
|
|
@@ -28,13 +29,24 @@ export function MfaDisableDialog({
|
|
|
28
29
|
onError,
|
|
29
30
|
}: MfaDisableDialogProps): ReactNode {
|
|
30
31
|
const t = useTranslation();
|
|
31
|
-
const { Dialog
|
|
32
|
+
const { Dialog } = usePrimitives();
|
|
32
33
|
const dispatcher = useDispatcher();
|
|
33
34
|
const [code, setCode] = useState("");
|
|
35
|
+
const [useRecovery, setUseRecovery] = useState(false);
|
|
36
|
+
|
|
37
|
+
const reset = (): void => {
|
|
38
|
+
setCode("");
|
|
39
|
+
setUseRecovery(false);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleOpenChange = (nextOpen: boolean): void => {
|
|
43
|
+
if (!nextOpen) reset();
|
|
44
|
+
onOpenChange(nextOpen);
|
|
45
|
+
};
|
|
34
46
|
|
|
35
47
|
const confirm = async (): Promise<void> => {
|
|
36
48
|
const res = await dispatcher.write<{ disabled: boolean }>(AuthMfaHandlers.disable, { code });
|
|
37
|
-
|
|
49
|
+
reset();
|
|
38
50
|
if (!res.isSuccess) {
|
|
39
51
|
onError(res.error.i18nKey);
|
|
40
52
|
return;
|
|
@@ -45,7 +57,7 @@ export function MfaDisableDialog({
|
|
|
45
57
|
return (
|
|
46
58
|
<Dialog
|
|
47
59
|
open={open}
|
|
48
|
-
onOpenChange={
|
|
60
|
+
onOpenChange={handleOpenChange}
|
|
49
61
|
variant="danger"
|
|
50
62
|
title={t("auth.mfa.disable.title")}
|
|
51
63
|
description={t("auth.mfa.disable.description")}
|
|
@@ -54,16 +66,14 @@ export function MfaDisableDialog({
|
|
|
54
66
|
onConfirm={confirm}
|
|
55
67
|
testId="mfa-disable-dialog"
|
|
56
68
|
>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/>
|
|
66
|
-
</Field>
|
|
69
|
+
<MfaCodeOrRecoveryField
|
|
70
|
+
id="mfa-disable-code"
|
|
71
|
+
codeLabel={t("auth.mfa.disable.code")}
|
|
72
|
+
code={code}
|
|
73
|
+
onCodeChange={setCode}
|
|
74
|
+
useRecovery={useRecovery}
|
|
75
|
+
onUseRecoveryChange={setUseRecovery}
|
|
76
|
+
/>
|
|
67
77
|
</Dialog>
|
|
68
78
|
);
|
|
69
79
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
10
10
|
import { type ReactNode, useState } from "react";
|
|
11
11
|
import { AuthMfaHandlers } from "./constants.ts";
|
|
12
|
+
import { MfaCodeOrRecoveryField } from "./mfa-code-or-recovery-field.tsx";
|
|
12
13
|
|
|
13
14
|
export type MfaRegenerateRecoveryDialogProps = {
|
|
14
15
|
readonly open: boolean;
|
|
@@ -25,16 +26,27 @@ export function MfaRegenerateRecoveryDialog({
|
|
|
25
26
|
onError,
|
|
26
27
|
}: MfaRegenerateRecoveryDialogProps): ReactNode {
|
|
27
28
|
const t = useTranslation();
|
|
28
|
-
const { Dialog
|
|
29
|
+
const { Dialog } = usePrimitives();
|
|
29
30
|
const dispatcher = useDispatcher();
|
|
30
31
|
const [code, setCode] = useState("");
|
|
32
|
+
const [useRecovery, setUseRecovery] = useState(false);
|
|
33
|
+
|
|
34
|
+
const reset = (): void => {
|
|
35
|
+
setCode("");
|
|
36
|
+
setUseRecovery(false);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleOpenChange = (nextOpen: boolean): void => {
|
|
40
|
+
if (!nextOpen) reset();
|
|
41
|
+
onOpenChange(nextOpen);
|
|
42
|
+
};
|
|
31
43
|
|
|
32
44
|
const confirm = async (): Promise<void> => {
|
|
33
45
|
const res = await dispatcher.write<{ recoveryCodes: readonly string[] }>(
|
|
34
46
|
AuthMfaHandlers.regenerateRecovery,
|
|
35
47
|
{ code },
|
|
36
48
|
);
|
|
37
|
-
|
|
49
|
+
reset();
|
|
38
50
|
if (!res.isSuccess) {
|
|
39
51
|
onError(res.error.i18nKey);
|
|
40
52
|
return;
|
|
@@ -45,7 +57,7 @@ export function MfaRegenerateRecoveryDialog({
|
|
|
45
57
|
return (
|
|
46
58
|
<Dialog
|
|
47
59
|
open={open}
|
|
48
|
-
onOpenChange={
|
|
60
|
+
onOpenChange={handleOpenChange}
|
|
49
61
|
title={t("auth.mfa.regenerate.title")}
|
|
50
62
|
description={t("auth.mfa.regenerate.description")}
|
|
51
63
|
confirmLabel={t("auth.mfa.regenerate.confirm")}
|
|
@@ -53,16 +65,14 @@ export function MfaRegenerateRecoveryDialog({
|
|
|
53
65
|
onConfirm={confirm}
|
|
54
66
|
testId="mfa-regenerate-recovery-dialog"
|
|
55
67
|
>
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/>
|
|
65
|
-
</Field>
|
|
68
|
+
<MfaCodeOrRecoveryField
|
|
69
|
+
id="mfa-regenerate-code"
|
|
70
|
+
codeLabel={t("auth.mfa.regenerate.code")}
|
|
71
|
+
code={code}
|
|
72
|
+
onCodeChange={setCode}
|
|
73
|
+
useRecovery={useRecovery}
|
|
74
|
+
onUseRecoveryChange={setUseRecovery}
|
|
75
|
+
/>
|
|
66
76
|
</Dialog>
|
|
67
77
|
);
|
|
68
78
|
}
|
|
@@ -79,6 +79,7 @@ export function MfaSetupPreauthScreen({
|
|
|
79
79
|
const [setup, setSetup] = useState<SetupState | null>(null);
|
|
80
80
|
const [acknowledged, setAcknowledged] = useState(false);
|
|
81
81
|
const [code, setCode] = useState("");
|
|
82
|
+
const [attempt, setAttempt] = useState(0);
|
|
82
83
|
const [busy, setBusy] = useState(false);
|
|
83
84
|
const [error, setError] = useState<string | null>(null);
|
|
84
85
|
const [retryAfterSeconds, setRetryAfterSeconds] = useState<number | null>(null);
|
|
@@ -109,13 +110,13 @@ export function MfaSetupPreauthScreen({
|
|
|
109
110
|
}
|
|
110
111
|
};
|
|
111
112
|
|
|
112
|
-
const confirmSetup = async (): Promise<void> => {
|
|
113
|
-
if (!setup) return;
|
|
113
|
+
const confirmSetup = async (submittedCode: string): Promise<void> => {
|
|
114
|
+
if (!setup || busy) return;
|
|
114
115
|
setBusy(true);
|
|
115
116
|
setError(null);
|
|
116
117
|
setRetryAfterSeconds(null);
|
|
117
118
|
try {
|
|
118
|
-
const res = await confirmMfaSetupPreauth(setup.setupToken,
|
|
119
|
+
const res = await confirmMfaSetupPreauth(setup.setupToken, submittedCode);
|
|
119
120
|
if (res.kind !== "success") {
|
|
120
121
|
setError(res.error.reason);
|
|
121
122
|
setRetryAfterSeconds(res.error.retryAfterSeconds ?? null);
|
|
@@ -130,7 +131,11 @@ export function MfaSetupPreauthScreen({
|
|
|
130
131
|
setSetup(null);
|
|
131
132
|
setAcknowledged(false);
|
|
132
133
|
setCode("");
|
|
134
|
+
return;
|
|
133
135
|
}
|
|
136
|
+
// Remount: the field was disabled during confirm and lost focus.
|
|
137
|
+
setCode("");
|
|
138
|
+
setAttempt((n) => n + 1);
|
|
134
139
|
return;
|
|
135
140
|
}
|
|
136
141
|
onSuccess?.(res.data.landingPath);
|
|
@@ -143,7 +148,7 @@ export function MfaSetupPreauthScreen({
|
|
|
143
148
|
|
|
144
149
|
const onSubmit = (e?: FormEvent): void => {
|
|
145
150
|
e?.preventDefault();
|
|
146
|
-
void confirmSetup();
|
|
151
|
+
void confirmSetup(code);
|
|
147
152
|
};
|
|
148
153
|
|
|
149
154
|
return (
|
|
@@ -226,13 +231,17 @@ export function MfaSetupPreauthScreen({
|
|
|
226
231
|
|
|
227
232
|
<Field id="mfa-setup-preauth-code" label={t("auth.mfa.enable.code")} required>
|
|
228
233
|
<Input
|
|
229
|
-
|
|
234
|
+
key={attempt}
|
|
235
|
+
kind="code"
|
|
236
|
+
length={6}
|
|
230
237
|
id="mfa-setup-preauth-code"
|
|
231
238
|
name="mfa-setup-preauth-code"
|
|
232
239
|
value={code}
|
|
233
240
|
onChange={setCode}
|
|
241
|
+
onComplete={(next) => void confirmSetup(next)}
|
|
234
242
|
disabled={busy || !acknowledged}
|
|
235
|
-
|
|
243
|
+
hasError={error !== null}
|
|
244
|
+
autoFocus={attempt > 0}
|
|
236
245
|
/>
|
|
237
246
|
</Field>
|
|
238
247
|
|
|
@@ -27,6 +27,10 @@ export type MfaVerifyScreenProps = {
|
|
|
27
27
|
readonly onCancel?: () => void;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
type CodeEntryMode = "authenticator" | "recovery";
|
|
31
|
+
|
|
32
|
+
const MFA_CODE_LENGTH = 6;
|
|
33
|
+
|
|
30
34
|
function reasonToKey(reason: string): string {
|
|
31
35
|
switch (reason) {
|
|
32
36
|
case "invalid_totp_code":
|
|
@@ -56,14 +60,17 @@ export function MfaVerifyScreen({
|
|
|
56
60
|
const t = useTranslation();
|
|
57
61
|
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
58
62
|
const session = useSession();
|
|
63
|
+
const [mode, setMode] = useState<CodeEntryMode>("authenticator");
|
|
59
64
|
const [code, setCode] = useState("");
|
|
65
|
+
const [attempt, setAttempt] = useState(0);
|
|
60
66
|
const [submitting, setSubmitting] = useState(false);
|
|
61
67
|
const [error, setError] = useState<string | null>(null);
|
|
62
68
|
|
|
63
|
-
const doSubmit = async (): Promise<void> => {
|
|
69
|
+
const doSubmit = async (submittedCode: string): Promise<void> => {
|
|
70
|
+
if (submitting) return;
|
|
64
71
|
setSubmitting(true);
|
|
65
72
|
setError(null);
|
|
66
|
-
const res = await verifyMfaChallenge(challengeToken,
|
|
73
|
+
const res = await verifyMfaChallenge(challengeToken, submittedCode);
|
|
67
74
|
if (res.kind === "success") {
|
|
68
75
|
await session.refresh();
|
|
69
76
|
setSubmitting(false);
|
|
@@ -72,30 +79,65 @@ export function MfaVerifyScreen({
|
|
|
72
79
|
}
|
|
73
80
|
setSubmitting(false);
|
|
74
81
|
setError(res.error.reason);
|
|
82
|
+
// Remount: the input was disabled during submit and lost focus.
|
|
83
|
+
setAttempt((n) => n + 1);
|
|
84
|
+
if (mode === "authenticator") setCode("");
|
|
75
85
|
};
|
|
76
86
|
|
|
77
87
|
const onSubmit = (e?: FormEvent): void => {
|
|
78
88
|
e?.preventDefault();
|
|
79
|
-
void doSubmit();
|
|
89
|
+
void doSubmit(code);
|
|
80
90
|
};
|
|
81
91
|
|
|
92
|
+
const toggleMode = (): void => {
|
|
93
|
+
setMode(mode === "authenticator" ? "recovery" : "authenticator");
|
|
94
|
+
setCode("");
|
|
95
|
+
setError(null);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const isRecovery = mode === "recovery";
|
|
99
|
+
const fieldId = isRecovery ? "mfa-verify-recovery-code" : "mfa-verify-code";
|
|
100
|
+
const defaultSubtitle = isRecovery
|
|
101
|
+
? t("auth.mfa.verify.recoverySubtitle")
|
|
102
|
+
: t("auth.mfa.verify.subtitle");
|
|
103
|
+
|
|
82
104
|
return (
|
|
83
|
-
<AuthCard
|
|
84
|
-
title={title ?? t("auth.mfa.verify.title")}
|
|
85
|
-
subtitle={subtitle ?? t("auth.mfa.verify.subtitle")}
|
|
86
|
-
>
|
|
105
|
+
<AuthCard title={title ?? t("auth.mfa.verify.title")} subtitle={subtitle ?? defaultSubtitle}>
|
|
87
106
|
<Form onSubmit={onSubmit}>
|
|
88
|
-
<Field
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
<Field
|
|
108
|
+
id={fieldId}
|
|
109
|
+
label={isRecovery ? t("auth.mfa.verify.recoveryCode") : t("auth.mfa.verify.code")}
|
|
110
|
+
required
|
|
111
|
+
>
|
|
112
|
+
{isRecovery ? (
|
|
113
|
+
<Input
|
|
114
|
+
key={`${mode}-${attempt}`}
|
|
115
|
+
kind="text"
|
|
116
|
+
id={fieldId}
|
|
117
|
+
name={fieldId}
|
|
118
|
+
value={code}
|
|
119
|
+
onChange={setCode}
|
|
120
|
+
disabled={submitting}
|
|
121
|
+
required
|
|
122
|
+
autoFocus
|
|
123
|
+
autoComplete="off"
|
|
124
|
+
/>
|
|
125
|
+
) : (
|
|
126
|
+
<Input
|
|
127
|
+
key={`${mode}-${attempt}`}
|
|
128
|
+
kind="code"
|
|
129
|
+
length={MFA_CODE_LENGTH}
|
|
130
|
+
id={fieldId}
|
|
131
|
+
name={fieldId}
|
|
132
|
+
value={code}
|
|
133
|
+
onChange={setCode}
|
|
134
|
+
onComplete={(next) => void doSubmit(next)}
|
|
135
|
+
disabled={submitting}
|
|
136
|
+
hasError={error !== null}
|
|
137
|
+
required
|
|
138
|
+
autoFocus
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
99
141
|
</Field>
|
|
100
142
|
{error !== null ? <Banner variant="error">{t(reasonToKey(error))}</Banner> : null}
|
|
101
143
|
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
@@ -103,6 +145,11 @@ export function MfaVerifyScreen({
|
|
|
103
145
|
? t("auth.mfa.verify.submitting")
|
|
104
146
|
: (submitLabel ?? t("auth.mfa.verify.submit"))}
|
|
105
147
|
</Button>
|
|
148
|
+
<Button type="button" variant="link" onClick={toggleMode} disabled={submitting}>
|
|
149
|
+
{isRecovery
|
|
150
|
+
? t("auth.mfa.verify.useAuthenticatorCode")
|
|
151
|
+
: t("auth.mfa.verify.useRecoveryCode")}
|
|
152
|
+
</Button>
|
|
106
153
|
{onCancel ? (
|
|
107
154
|
<Button type="button" variant="secondary" onClick={onCancel} disabled={submitting}>
|
|
108
155
|
{t("auth.mfa.verify.backToLogin")}
|
|
@@ -1,4 +1,59 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Concurrent termination runs, revocation resume and sync ordering",
|
|
6
|
+
"detail": "record-contract-termination re-checks the requestId after the provider call so a concurrent run appends nothing. Revocation resumes a period-end cancel that was recorded between its two stream reads. sync-subscription stamps the snapshot time as providerEventAtIso so a late older webhook cannot reactivate a synced cancellation."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Ordered terms lists keep their numbers in the confirmation mail",
|
|
12
|
+
"detail": "The contract confirmation mail prefixes each item of an ordered Markdown list with its number, so references such as \"see no. 3\" in terms and cancellation policies stay intact."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.388.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Operator notice tells an unavailable receipt limiter from a reached limit",
|
|
18
|
+
"detail": "When the termination receipt mail is withheld because the rate limiter failed or has no digest key, the operator notice reports \"receipt_not_sent\" instead of the per-recipient limit reason."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.388.0",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "An abandoned plan-change checkout no longer restarts the withdrawal period",
|
|
24
|
+
"detail": "The withdrawal period now runs from the latest subscription start only; a checkout consent without a following subscription start does not reopen it, so a later withdrawal of an old contract is cancelled at period end."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.388.0",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "Same-second provider update cannot revive a canceled subscription",
|
|
30
|
+
"detail": "When a non-terminal state event carries the same provider timestamp as a stored cancellation, it is treated as late and discarded."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.388.0",
|
|
34
|
+
"type": "improvement",
|
|
35
|
+
"title": "Several subscription providers in one catalog",
|
|
36
|
+
"detail": "The billing-plans query returns providers (name, displayName, plans, note) when more than one provider is offered, and start-plan-checkout takes providerName. consumerProtection.providerTexts overrides checkoutNote and vatNote per provider, and the consent record carries providerName. switch-plan always uses the provider of the existing subscription."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"version": "0.388.0",
|
|
40
|
+
"type": "improvement",
|
|
41
|
+
"title": "In-app plan switch with a preview and consent",
|
|
42
|
+
"detail": "A provider plugin that implements previewPlanSwitch and applyPlanSwitch switches plans without a hosted portal. The new plan-switch-preview query returns the proration, the billing-plans result carries inAppPlanSwitch, and the panel shows PlanSwitchConfirmDialog with the withdrawal consent checkboxes. The confirmation mail is sent when the plan_changed webhook arrives with the consent id."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"version": "0.388.0",
|
|
46
|
+
"type": "breaking",
|
|
47
|
+
"title": "A catalog with several providers no longer throws an ambiguous-provider error",
|
|
48
|
+
"detail": "With more than one subscription provider mounted, billing-plans and start-plan-checkout used to throw. They now list the providers; start-plan-checkout answers with errors.providerRequired when several are offered and no providerName is given, and errors.providerNotOffered for an unknown one. Plugins have to set displayName.",
|
|
49
|
+
"migration": "Set displayName on custom subscriptionProvider plugins. Callers that send start-plan-checkout with several providers mounted must pass providerName."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"version": "0.388.0",
|
|
53
|
+
"type": "improvement",
|
|
54
|
+
"title": "Provider choice moved into the checkout dialog",
|
|
55
|
+
"detail": "The plan panel no longer shows a provider switch. Cards show the first provider's price, or the cheapest one with a \"from\" prefix when providers disagree. With several providers, or with consumerProtection, choosing a plan opens CheckoutConsentDialog, which now has an optional providerChoice (ChoiceCards with the checkout note and, when prices differ, the price), an optional note, and an optional consumerProtection (without it there are no checkboxes and the order carries no consent). The plan switch dialog shows the recurring price with its interval."
|
|
56
|
+
},
|
|
2
57
|
{
|
|
3
58
|
"version": "0.379.0",
|
|
4
59
|
"type": "fix",
|