@cosmicdrift/kumiko-bundled-features 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/dist/agent-tools/tool-dispatch.d.ts +7 -7
- package/dist/agent-tools/tool-dispatch.js +9 -9
- package/dist/audit/handlers/audit-events.js +1 -1
- package/dist/auth-mfa/feature-exports.d.ts +2 -2
- package/dist/auth-mfa/feature.js +8 -0
- package/dist/auth-mfa/web/i18n.js +5 -1
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
- package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
- package/dist/billing-foundation/checkout-core.d.ts +9 -14
- package/dist/billing-foundation/checkout-core.js +24 -39
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
- package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
- package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
- package/dist/billing-foundation/events.d.ts +1 -0
- package/dist/billing-foundation/events.js +1 -0
- package/dist/billing-foundation/feature.js +3 -1
- package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
- package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
- package/dist/billing-foundation/handlers/process-event.write.js +8 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
- package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
- package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
- package/dist/billing-foundation/i18n.js +22 -0
- package/dist/billing-foundation/index.d.ts +1 -1
- package/dist/billing-foundation/plan-catalog.d.ts +21 -5
- package/dist/billing-foundation/plan-catalog.js +25 -16
- package/dist/billing-foundation/plan-switch.d.ts +12 -0
- package/dist/billing-foundation/plan-switch.js +60 -0
- package/dist/billing-foundation/types.d.ts +57 -1
- package/dist/billing-foundation/types.js +9 -0
- package/dist/billing-foundation/validate-options.js +17 -0
- package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
- package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
- package/dist/billing-foundation/web/constants.d.ts +4 -0
- package/dist/billing-foundation/web/constants.js +4 -0
- package/dist/billing-foundation/web/plan-price.d.ts +15 -0
- package/dist/billing-foundation/web/plan-price.js +32 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
- package/dist/cap-counter/cap-reservation.js +5 -1
- package/dist/cap-counter/enforce-cap.js +2 -1
- package/dist/cap-counter/stock-cap-guard.js +7 -1
- package/dist/channel-email/feature.js +1 -1
- package/dist/config/handlers/cascade.query.d.ts +5 -0
- package/dist/config/handlers/cascade.query.js +4 -3
- package/dist/config/handlers/values.query.d.ts +5 -0
- package/dist/config/handlers/values.query.js +4 -3
- package/dist/config/write-helpers.d.ts +1 -0
- package/dist/config/write-helpers.js +18 -9
- package/dist/crypto-shredding/handlers/forget-subject.write.js +13 -10
- package/dist/delivery/channel-context.js +6 -3
- package/dist/delivery/db/queries/preferences.js +2 -2
- package/dist/delivery/delivery-service.js +22 -7
- package/dist/delivery/handlers/preferences.query.d.ts +1 -1
- package/dist/delivery/handlers/preferences.query.js +20 -1
- package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
- package/dist/delivery/unsubscribe-link.d.ts +3 -1
- package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
- package/dist/delivery/unsubscribe-page-strings.js +1 -0
- package/dist/delivery/unsubscribe-page.d.ts +2 -0
- package/dist/delivery/unsubscribe-page.js +5 -0
- package/dist/delivery/unsubscribe.js +25 -7
- package/dist/file-foundation/index.d.ts +1 -1
- package/dist/file-foundation/index.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
- package/dist/file-foundation/read-file-ref-content.js +6 -1
- package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
- package/dist/form-draft/db/queries/cleanup.js +2 -2
- package/dist/form-draft/decrypt-draft.js +5 -1
- package/dist/host-routing/dispatch.js +11 -20
- package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
- package/dist/inbound-mail-foundation/i18n.js +0 -1
- package/dist/inbound-mail-foundation/screens.js +0 -3
- package/dist/inbound-mail-foundation/types.d.ts +4 -3
- package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
- package/dist/jobs/feature.d.ts +1 -0
- package/dist/jobs/feature.js +8 -4
- package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-failures.query.js +45 -41
- package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-runs.query.js +54 -50
- package/dist/jobs/job-run-logger.js +5 -4
- package/dist/mail-foundation/feature.d.ts +3 -3
- package/dist/mail-foundation/feature.js +3 -0
- package/dist/personal-access-tokens/feature.d.ts +1 -1
- package/dist/personal-access-tokens/feature.js +4 -0
- package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
- package/dist/renderer-pdf/render-table-pdf.js +22 -10
- package/dist/sessions/feature.d.ts +2 -2
- package/dist/sessions/feature.js +4 -0
- package/dist/shared/decrypt-stored-pii.d.ts +1 -0
- package/dist/shared/decrypt-stored-pii.js +6 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/mail-template-body.d.ts +6 -2
- package/dist/shared/mail-template-body.js +7 -4
- package/dist/step-dispatcher/feature.js +1 -0
- package/dist/subscription-dodo/client.d.ts +17 -0
- package/dist/subscription-dodo/client.js +31 -0
- package/dist/subscription-dodo/constants.d.ts +12 -0
- package/dist/subscription-dodo/constants.js +19 -0
- package/dist/subscription-dodo/feature.d.ts +37 -0
- package/dist/subscription-dodo/feature.js +144 -0
- package/dist/subscription-dodo/index.d.ts +2 -0
- package/dist/subscription-dodo/index.js +4 -0
- package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
- package/dist/subscription-dodo/plugin-methods.js +260 -0
- package/dist/subscription-dodo/runtime.d.ts +27 -0
- package/dist/subscription-dodo/runtime.js +89 -0
- package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
- package/dist/subscription-dodo/verify-webhook.js +189 -0
- package/dist/subscription-stripe/feature.js +1 -0
- package/dist/subscription-stripe/runtime.js +7 -6
- package/dist/template-resolver/api.d.ts +3 -2
- package/dist/template-resolver/api.js +24 -2
- package/dist/template-resolver/compile-document-body.js +1 -0
- package/dist/template-resolver/feature.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.js +3 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
- package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
- package/dist/template-resolver/handlers/document-base.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
- package/dist/template-resolver/handlers/list.query.js +5 -6
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +6 -3
- package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
- package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
- package/dist/template-resolver/handlers/usage.query.js +26 -16
- package/dist/template-resolver/handlers/validate.query.js +7 -2
- package/dist/template-resolver/seeding.js +26 -10
- package/dist/template-resolver/store-template-resource.d.ts +1 -0
- package/dist/template-resolver/store-template-resource.js +19 -4
- package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
- package/dist/template-resolver/template-delivery-policy.js +0 -6
- package/dist/template-resolver/template-meta.js +7 -4
- package/dist/template-resolver/template-preview.js +4 -3
- package/dist/template-resolver/web/constants.d.ts +2 -2
- package/dist/template-resolver/web/constants.js +3 -3
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
- package/dist/template-resolver/web/mail-layout-constants.js +7 -0
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +10 -3
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +8 -4
- package/dist/tenant-lifecycle/constants.d.ts +1 -0
- package/dist/tenant-lifecycle/constants.js +4 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
- package/dist/tier-engine/web/constants.js +2 -2
- package/dist/user-data-rights/run-export-jobs.js +3 -4
- package/dist/user-data-rights/run-forget-cleanup.js +2 -1
- package/dist/waitlist/handlers/export-purge.write.js +2 -1
- package/dist/waitlist/handlers/submit.write.js +5 -5
- package/dist/waitlist/import.d.ts +1 -0
- package/dist/waitlist/import.js +4 -0
- package/dist/waitlist/legacy-migrate.js +17 -6
- package/dist/waitlist/link-on-signup.js +18 -6
- package/dist/waitlist/options.d.ts +3 -0
- package/package.json +19 -9
- package/src/agent-tools/changes.json +6 -0
- package/src/audit/changes.json +12 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +55 -0
- package/src/cap-counter/changes.json +18 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +12 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +30 -0
- package/src/form-draft/changes.json +6 -0
- package/src/host-routing/changes.json +6 -0
- package/src/inbound-mail-foundation/changes.json +18 -0
- package/src/jobs/changes.json +13 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/renderer-pdf/changes.json +6 -0
- package/src/step-dispatcher/changes.json +6 -0
- package/src/subscription-dodo/changes.json +14 -0
- package/src/subscription-stripe/changes.json +12 -0
- package/src/template-resolver/changes.json +48 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/waitlist/changes.json +18 -0
- package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
- package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
|
@@ -37,11 +37,11 @@ export type ToolCallRequest = {
|
|
|
37
37
|
/** Receives the cause of unexpected (5xx) failures, which the tool result never carries. */
|
|
38
38
|
readonly log?: Pick<Logger, "error">;
|
|
39
39
|
};
|
|
40
|
-
/** Executes one LLM-issued tool call. Never throws
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
40
|
+
/** Executes one LLM-issued tool call. Never throws: every failure comes back as `{ ok: false }`
|
|
41
|
+
* so the agent loop can feed the error back to the model instead of crashing.
|
|
42
|
+
* Handler and dispatcher failures carry a `code` (429 adds `retryable`); client errors (< 500)
|
|
43
|
+
* keep their message, server failures (>= 500, unknown throws) are logged and returned as a
|
|
44
|
+
* generic message. Local validation errors (unknown tool, bad arguments) have no `code`.
|
|
45
|
+
* `request.user` must be the caller's real identity: server-kind tools run through the same
|
|
46
|
+
* permission-checked pipeline as an HTTP request. */
|
|
47
47
|
export declare function dispatchToolCall(request: ToolCallRequest): Promise<ToolCallResult>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
-
import {
|
|
2
|
+
import { runAsAgentToolCallEntry } from "@cosmicdrift/kumiko-framework/internal/request-context";
|
|
3
3
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
4
4
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
5
5
|
import { FILTER_OPS, isRecord } from "./filter-ops.js";
|
|
@@ -274,13 +274,13 @@ function dispatchClient(descriptor, input) {
|
|
|
274
274
|
return dispatchClientAskUser(input);
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
-
/** Executes one LLM-issued tool call. Never throws
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
277
|
+
/** Executes one LLM-issued tool call. Never throws: every failure comes back as `{ ok: false }`
|
|
278
|
+
* so the agent loop can feed the error back to the model instead of crashing.
|
|
279
|
+
* Handler and dispatcher failures carry a `code` (429 adds `retryable`); client errors (< 500)
|
|
280
|
+
* keep their message, server failures (>= 500, unknown throws) are logged and returned as a
|
|
281
|
+
* generic message. Local validation errors (unknown tool, bad arguments) have no `code`.
|
|
282
|
+
* `request.user` must be the caller's real identity: server-kind tools run through the same
|
|
283
|
+
* permission-checked pipeline as an HTTP request. */
|
|
284
284
|
export async function dispatchToolCall(request) {
|
|
285
285
|
const { dispatcher, user, toolName, input, dispatchTable, runId, toolCallId } = request;
|
|
286
286
|
const classifyFailure = createFailureClassifier(toolName, request.log ?? fallbackLog);
|
|
@@ -289,7 +289,7 @@ export async function dispatchToolCall(request) {
|
|
|
289
289
|
return { ok: false, error: `Unknown tool "${toolName}"` };
|
|
290
290
|
}
|
|
291
291
|
try {
|
|
292
|
-
return await
|
|
292
|
+
return await runAsAgentToolCallEntry(async () => {
|
|
293
293
|
switch (descriptor.kind) {
|
|
294
294
|
case "search":
|
|
295
295
|
return await dispatchSearch(descriptor, dispatcher, user, toolName, input);
|
|
@@ -12,7 +12,7 @@ import { redactPiiCiphertext } from "../../shared/index.js";
|
|
|
12
12
|
import { eventLabelKeyOf, TECHNICAL_EVENT_TYPES } from "../event-classification.js";
|
|
13
13
|
import { AUDIT_EVENT_ACTIONS, INCLUDE_TECHNICAL_FILTER, } from "../web/constants.js";
|
|
14
14
|
const MAX_LIMIT = 100;
|
|
15
|
-
// At most 18 digits keeps the id inside bigint, so an oversized cursor or id is a
|
|
15
|
+
// At most 18 digits keeps the id inside bigint, so an oversized cursor or id is a validation error (400), not a 500.
|
|
16
16
|
const CURSOR_PATTERN = /^\d{1,18}$/;
|
|
17
17
|
export const auditListPayloadShape = {
|
|
18
18
|
cursor: z.string().regex(CURSOR_PATTERN, "cursor must be a positive integer").optional(),
|
|
@@ -2,11 +2,11 @@ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
2
2
|
import type { MfaCodeVerifier } from "./mfa-code-verifier.ts";
|
|
3
3
|
import type { MfaStatusChecker } from "./mfa-status-checker.ts";
|
|
4
4
|
import type { MfaTokenSecretsEnv } from "./token-secrets.ts";
|
|
5
|
-
export type BindMfaRevokeAllOtherSessions = (revoker: (userId: string, currentSid: string | undefined) => Promise<number>) => void;
|
|
5
|
+
export type BindMfaRevokeAllOtherSessions = (revoker: (userId: string, currentSid: string | undefined) => Promise<number>) => () => void;
|
|
6
6
|
export declare function bindMfaRevokeAllOtherSessionsFromFeature(feature: FeatureDefinition): BindMfaRevokeAllOtherSessions | undefined;
|
|
7
7
|
export declare function mfaStatusCheckerFromFeature(feature: FeatureDefinition): MfaStatusChecker | undefined;
|
|
8
8
|
export declare function mfaVerifierFromFeature(feature: FeatureDefinition): MfaCodeVerifier | undefined;
|
|
9
9
|
export type BindMfaTokenSecrets = (masterSecret: string, env: MfaTokenSecretsEnv) => void;
|
|
10
10
|
export declare function bindMfaTokenSecretsFromFeature(feature: FeatureDefinition): BindMfaTokenSecrets | undefined;
|
|
11
|
-
export type BindRevokeAllPatTokens = (revoker: (userId: string) => Promise<number>) => void;
|
|
11
|
+
export type BindRevokeAllPatTokens = (revoker: (userId: string) => Promise<number>) => () => void;
|
|
12
12
|
export declare function bindRevokeAllPatTokensFromFeature(feature: FeatureDefinition): BindRevokeAllPatTokens | undefined;
|
package/dist/auth-mfa/feature.js
CHANGED
|
@@ -147,9 +147,17 @@ export function createAuthMfaFeature(opts) {
|
|
|
147
147
|
const verifyMfaCode = createMfaCodeVerifier();
|
|
148
148
|
const bindRevokeAllOtherSessions = (revoker) => {
|
|
149
149
|
revokeAllOtherSessions = revoker;
|
|
150
|
+
return () => {
|
|
151
|
+
if (revokeAllOtherSessions === revoker)
|
|
152
|
+
revokeAllOtherSessions = undefined;
|
|
153
|
+
};
|
|
150
154
|
};
|
|
151
155
|
const bindRevokeAllPatTokens = (revoker) => {
|
|
152
156
|
revokeAllPatTokens = revoker;
|
|
157
|
+
return () => {
|
|
158
|
+
if (revokeAllPatTokens === revoker)
|
|
159
|
+
revokeAllPatTokens = undefined;
|
|
160
|
+
};
|
|
153
161
|
};
|
|
154
162
|
return {
|
|
155
163
|
handlers,
|
|
@@ -10,6 +10,10 @@ export const defaultTranslations = {
|
|
|
10
10
|
"auth.mfa.verify.submit": "Verify",
|
|
11
11
|
"auth.mfa.verify.submitting": "…",
|
|
12
12
|
"auth.mfa.verify.backToLogin": "Back to login",
|
|
13
|
+
"auth.mfa.verify.recoveryCode": "Recovery code",
|
|
14
|
+
"auth.mfa.verify.recoverySubtitle": "Enter one of the recovery codes you saved when you set up two-factor authentication.",
|
|
15
|
+
"auth.mfa.verify.useAuthenticatorCode": "Use the code from your app",
|
|
16
|
+
"auth.mfa.verify.useRecoveryCode": "Use a recovery code instead",
|
|
13
17
|
"auth.mfa.errors.invalidCode": "Invalid code. Please try again.",
|
|
14
18
|
"auth.mfa.errors.invalidTotpCode": "Invalid code. Please try again.",
|
|
15
19
|
"auth.mfa.errors.invalidChallengeToken": "Your sign-in has expired. Please sign in again.",
|
|
@@ -35,7 +39,7 @@ export const defaultTranslations = {
|
|
|
35
39
|
"auth.mfa.setup.confirm": "Complete setup",
|
|
36
40
|
"auth.mfa.disable.title": "Disable two-factor authentication",
|
|
37
41
|
"auth.mfa.disable.description": "Confirm with a code from your authenticator app or a recovery code. Your account will then be protected by your password alone.",
|
|
38
|
-
"auth.mfa.disable.code": "Code from your authenticator app
|
|
42
|
+
"auth.mfa.disable.code": "Code from your authenticator app",
|
|
39
43
|
"auth.mfa.disable.confirm": "Disable",
|
|
40
44
|
"auth.mfa.disable.cancel": "Cancel",
|
|
41
45
|
"auth.mfa.disable.trigger": "Disable two-factor authentication",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type MfaCodeOrRecoveryFieldProps = {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly codeLabel: string;
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly onCodeChange: (code: string) => void;
|
|
7
|
+
readonly useRecovery: boolean;
|
|
8
|
+
readonly onUseRecoveryChange: (useRecovery: boolean) => void;
|
|
9
|
+
readonly disabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function MfaCodeOrRecoveryField({ id, codeLabel, code, onCodeChange, useRecovery, onUseRecoveryChange, disabled, }: MfaCodeOrRecoveryFieldProps): ReactNode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
export function MfaCodeOrRecoveryField({ id, codeLabel, code, onCodeChange, useRecovery, onUseRecoveryChange, disabled, }) {
|
|
4
|
+
const t = useTranslation();
|
|
5
|
+
const { Button, Field, Input } = usePrimitives();
|
|
6
|
+
return (_jsxs(_Fragment, { children: [_jsx(Field, { id: id, label: useRecovery ? t("auth.mfa.verify.recoveryCode") : codeLabel, required: true, children: useRecovery ? (_jsx(Input, { kind: "text", id: id, name: id, value: code, onChange: onCodeChange, disabled: disabled, autoFocus: true, autoComplete: "off" })) : (_jsx(Input, { kind: "code", length: 6, id: id, name: id, value: code, onChange: onCodeChange, disabled: disabled, autoFocus: true })) }), _jsx(Button, { type: "button", variant: "link", disabled: disabled, onClick: () => {
|
|
7
|
+
onUseRecoveryChange(!useRecovery);
|
|
8
|
+
onCodeChange("");
|
|
9
|
+
}, children: useRecovery
|
|
10
|
+
? t("auth.mfa.verify.useAuthenticatorCode")
|
|
11
|
+
: t("auth.mfa.verify.useRecoveryCode") })] }));
|
|
12
|
+
}
|
|
@@ -12,20 +12,31 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
12
12
|
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
13
13
|
import { useState } from "react";
|
|
14
14
|
import { AuthMfaHandlers } from "./constants.js";
|
|
15
|
+
import { MfaCodeOrRecoveryField } from "./mfa-code-or-recovery-field.js";
|
|
15
16
|
// kumiko-lint-ignore no-custom-primitives Uses usePrimitives().Dialog internally, name is an MFA domain action, not a primitive reimplementation
|
|
16
17
|
export function MfaDisableDialog({ open, onOpenChange, onDisabled, onError, }) {
|
|
17
18
|
const t = useTranslation();
|
|
18
|
-
const { Dialog
|
|
19
|
+
const { Dialog } = usePrimitives();
|
|
19
20
|
const dispatcher = useDispatcher();
|
|
20
21
|
const [code, setCode] = useState("");
|
|
22
|
+
const [useRecovery, setUseRecovery] = useState(false);
|
|
23
|
+
const reset = () => {
|
|
24
|
+
setCode("");
|
|
25
|
+
setUseRecovery(false);
|
|
26
|
+
};
|
|
27
|
+
const handleOpenChange = (nextOpen) => {
|
|
28
|
+
if (!nextOpen)
|
|
29
|
+
reset();
|
|
30
|
+
onOpenChange(nextOpen);
|
|
31
|
+
};
|
|
21
32
|
const confirm = async () => {
|
|
22
33
|
const res = await dispatcher.write(AuthMfaHandlers.disable, { code });
|
|
23
|
-
|
|
34
|
+
reset();
|
|
24
35
|
if (!res.isSuccess) {
|
|
25
36
|
onError(res.error.i18nKey);
|
|
26
37
|
return;
|
|
27
38
|
}
|
|
28
39
|
onDisabled();
|
|
29
40
|
};
|
|
30
|
-
return (_jsx(Dialog, { open: open, onOpenChange:
|
|
41
|
+
return (_jsx(Dialog, { open: open, onOpenChange: handleOpenChange, variant: "danger", title: t("auth.mfa.disable.title"), description: t("auth.mfa.disable.description"), confirmLabel: t("auth.mfa.disable.confirm"), cancelLabel: t("auth.mfa.disable.cancel"), onConfirm: confirm, testId: "mfa-disable-dialog", children: _jsx(MfaCodeOrRecoveryField, { id: "mfa-disable-code", codeLabel: t("auth.mfa.disable.code"), code: code, onCodeChange: setCode, useRecovery: useRecovery, onUseRecoveryChange: setUseRecovery }) }));
|
|
31
42
|
}
|
|
@@ -9,20 +9,31 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
9
9
|
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
10
10
|
import { useState } from "react";
|
|
11
11
|
import { AuthMfaHandlers } from "./constants.js";
|
|
12
|
+
import { MfaCodeOrRecoveryField } from "./mfa-code-or-recovery-field.js";
|
|
12
13
|
// kumiko-lint-ignore no-custom-primitives Uses usePrimitives().Dialog internally, name is an MFA domain action, not a primitive reimplementation
|
|
13
14
|
export function MfaRegenerateRecoveryDialog({ open, onOpenChange, onRegenerated, onError, }) {
|
|
14
15
|
const t = useTranslation();
|
|
15
|
-
const { Dialog
|
|
16
|
+
const { Dialog } = usePrimitives();
|
|
16
17
|
const dispatcher = useDispatcher();
|
|
17
18
|
const [code, setCode] = useState("");
|
|
19
|
+
const [useRecovery, setUseRecovery] = useState(false);
|
|
20
|
+
const reset = () => {
|
|
21
|
+
setCode("");
|
|
22
|
+
setUseRecovery(false);
|
|
23
|
+
};
|
|
24
|
+
const handleOpenChange = (nextOpen) => {
|
|
25
|
+
if (!nextOpen)
|
|
26
|
+
reset();
|
|
27
|
+
onOpenChange(nextOpen);
|
|
28
|
+
};
|
|
18
29
|
const confirm = async () => {
|
|
19
30
|
const res = await dispatcher.write(AuthMfaHandlers.regenerateRecovery, { code });
|
|
20
|
-
|
|
31
|
+
reset();
|
|
21
32
|
if (!res.isSuccess) {
|
|
22
33
|
onError(res.error.i18nKey);
|
|
23
34
|
return;
|
|
24
35
|
}
|
|
25
36
|
onRegenerated(res.data.recoveryCodes);
|
|
26
37
|
};
|
|
27
|
-
return (_jsx(Dialog, { open: open, onOpenChange:
|
|
38
|
+
return (_jsx(Dialog, { open: open, onOpenChange: handleOpenChange, title: t("auth.mfa.regenerate.title"), description: t("auth.mfa.regenerate.description"), confirmLabel: t("auth.mfa.regenerate.confirm"), cancelLabel: t("auth.mfa.regenerate.cancel"), onConfirm: confirm, testId: "mfa-regenerate-recovery-dialog", children: _jsx(MfaCodeOrRecoveryField, { id: "mfa-regenerate-code", codeLabel: t("auth.mfa.regenerate.code"), code: code, onCodeChange: setCode, useRecovery: useRecovery, onUseRecoveryChange: setUseRecovery }) }));
|
|
28
39
|
}
|
|
@@ -48,6 +48,7 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
|
|
|
48
48
|
const [setup, setSetup] = useState(null);
|
|
49
49
|
const [acknowledged, setAcknowledged] = useState(false);
|
|
50
50
|
const [code, setCode] = useState("");
|
|
51
|
+
const [attempt, setAttempt] = useState(0);
|
|
51
52
|
const [busy, setBusy] = useState(false);
|
|
52
53
|
const [error, setError] = useState(null);
|
|
53
54
|
const [retryAfterSeconds, setRetryAfterSeconds] = useState(null);
|
|
@@ -78,14 +79,14 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
|
|
|
78
79
|
setBusy(false);
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
|
-
const confirmSetup = async () => {
|
|
82
|
-
if (!setup)
|
|
82
|
+
const confirmSetup = async (submittedCode) => {
|
|
83
|
+
if (!setup || busy)
|
|
83
84
|
return;
|
|
84
85
|
setBusy(true);
|
|
85
86
|
setError(null);
|
|
86
87
|
setRetryAfterSeconds(null);
|
|
87
88
|
try {
|
|
88
|
-
const res = await confirmMfaSetupPreauth(setup.setupToken,
|
|
89
|
+
const res = await confirmMfaSetupPreauth(setup.setupToken, submittedCode);
|
|
89
90
|
if (res.kind !== "success") {
|
|
90
91
|
setError(res.error.reason);
|
|
91
92
|
setRetryAfterSeconds(res.error.retryAfterSeconds ?? null);
|
|
@@ -98,7 +99,11 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
|
|
|
98
99
|
setSetup(null);
|
|
99
100
|
setAcknowledged(false);
|
|
100
101
|
setCode("");
|
|
102
|
+
return;
|
|
101
103
|
}
|
|
104
|
+
// Remount: the field was disabled during confirm and lost focus.
|
|
105
|
+
setCode("");
|
|
106
|
+
setAttempt((n) => n + 1);
|
|
102
107
|
return;
|
|
103
108
|
}
|
|
104
109
|
onSuccess?.(res.data.landingPath);
|
|
@@ -112,7 +117,7 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
|
|
|
112
117
|
};
|
|
113
118
|
const onSubmit = (e) => {
|
|
114
119
|
e?.preventDefault();
|
|
115
|
-
void confirmSetup();
|
|
120
|
+
void confirmSetup(code);
|
|
116
121
|
};
|
|
117
122
|
return (_jsxs(AuthCard, { title: title ?? t("auth.mfa.setup.title"), subtitle: subtitle ?? t("auth.mfa.setup.subtitle"), children: [error !== null ? (_jsx(Banner, { variant: "error", children: error === "too_many_attempts" && retryAfterSeconds !== null
|
|
118
123
|
? t("auth.mfa.errors.tooManyAttemptsWithSeconds", { seconds: retryAfterSeconds })
|
|
@@ -124,5 +129,5 @@ export function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title,
|
|
|
124
129
|
copyable: false,
|
|
125
130
|
multiline: false,
|
|
126
131
|
},
|
|
127
|
-
], copyLabel: t("kumiko.secretMint.copy"), copiedLabel: t("kumiko.secretMint.copied"), testId: "mfa-setup-preauth-qr" })) : (_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.scanTitle") })), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.manualEntry") }), _jsx("code", { className: "inline-block break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.secretParam })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.recoveryTitle") }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.recoveryHint") }), _jsx("code", { className: "block whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.recoveryCodes.join("\n") }), _jsx(Field, { id: "mfa-setup-preauth-ack", label: t("auth.mfa.enable.acknowledge"), children: _jsx(Input, { kind: "boolean", id: "mfa-setup-preauth-ack", name: "mfa-setup-preauth-ack", value: acknowledged, onChange: setAcknowledged }) })] }), _jsx(Field, { id: "mfa-setup-preauth-code", label: t("auth.mfa.enable.code"), required: true, children: _jsx(Input, { kind: "
|
|
132
|
+
], copyLabel: t("kumiko.secretMint.copy"), copiedLabel: t("kumiko.secretMint.copied"), testId: "mfa-setup-preauth-qr" })) : (_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.scanTitle") })), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.manualEntry") }), _jsx("code", { className: "inline-block break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.secretParam })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.enable.recoveryTitle") }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.enable.recoveryHint") }), _jsx("code", { className: "block whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: setup.recoveryCodes.join("\n") }), _jsx(Field, { id: "mfa-setup-preauth-ack", label: t("auth.mfa.enable.acknowledge"), children: _jsx(Input, { kind: "boolean", id: "mfa-setup-preauth-ack", name: "mfa-setup-preauth-ack", value: acknowledged, onChange: setAcknowledged }) })] }), _jsx(Field, { id: "mfa-setup-preauth-code", label: t("auth.mfa.enable.code"), required: true, children: _jsx(Input, { kind: "code", length: 6, id: "mfa-setup-preauth-code", name: "mfa-setup-preauth-code", value: code, onChange: setCode, onComplete: (next) => void confirmSetup(next), disabled: busy || !acknowledged, hasError: error !== null, autoFocus: attempt > 0 }, attempt) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", variant: "primary", loading: busy, disabled: busy || !acknowledged || code.length !== 6, children: t("auth.mfa.setup.confirm") }) })] }) })), onCancel ? (_jsx("div", { className: "flex justify-center", children: _jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: busy, children: t("auth.mfa.verify.backToLogin") }) })) : null] }));
|
|
128
133
|
}
|
|
@@ -10,6 +10,7 @@ import { useState } from "react";
|
|
|
10
10
|
// kumiko-lint-ignore cross-feature-import client-only barrel, the feature's server barrel has no web/ re-export
|
|
11
11
|
import { AuthCard, useSession } from "../../auth-email-password/web/index.js";
|
|
12
12
|
import { verifyMfaChallenge } from "./mfa-client.js";
|
|
13
|
+
const MFA_CODE_LENGTH = 6;
|
|
13
14
|
function reasonToKey(reason) {
|
|
14
15
|
switch (reason) {
|
|
15
16
|
case "invalid_totp_code":
|
|
@@ -31,13 +32,17 @@ export function MfaVerifyScreen({ challengeToken, title, subtitle, submitLabel,
|
|
|
31
32
|
const t = useTranslation();
|
|
32
33
|
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
33
34
|
const session = useSession();
|
|
35
|
+
const [mode, setMode] = useState("authenticator");
|
|
34
36
|
const [code, setCode] = useState("");
|
|
37
|
+
const [attempt, setAttempt] = useState(0);
|
|
35
38
|
const [submitting, setSubmitting] = useState(false);
|
|
36
39
|
const [error, setError] = useState(null);
|
|
37
|
-
const doSubmit = async () => {
|
|
40
|
+
const doSubmit = async (submittedCode) => {
|
|
41
|
+
if (submitting)
|
|
42
|
+
return;
|
|
38
43
|
setSubmitting(true);
|
|
39
44
|
setError(null);
|
|
40
|
-
const res = await verifyMfaChallenge(challengeToken,
|
|
45
|
+
const res = await verifyMfaChallenge(challengeToken, submittedCode);
|
|
41
46
|
if (res.kind === "success") {
|
|
42
47
|
await session.refresh();
|
|
43
48
|
setSubmitting(false);
|
|
@@ -46,12 +51,28 @@ export function MfaVerifyScreen({ challengeToken, title, subtitle, submitLabel,
|
|
|
46
51
|
}
|
|
47
52
|
setSubmitting(false);
|
|
48
53
|
setError(res.error.reason);
|
|
54
|
+
// Remount: the input was disabled during submit and lost focus.
|
|
55
|
+
setAttempt((n) => n + 1);
|
|
56
|
+
if (mode === "authenticator")
|
|
57
|
+
setCode("");
|
|
49
58
|
};
|
|
50
59
|
const onSubmit = (e) => {
|
|
51
60
|
e?.preventDefault();
|
|
52
|
-
void doSubmit();
|
|
61
|
+
void doSubmit(code);
|
|
62
|
+
};
|
|
63
|
+
const toggleMode = () => {
|
|
64
|
+
setMode(mode === "authenticator" ? "recovery" : "authenticator");
|
|
65
|
+
setCode("");
|
|
66
|
+
setError(null);
|
|
53
67
|
};
|
|
54
|
-
|
|
68
|
+
const isRecovery = mode === "recovery";
|
|
69
|
+
const fieldId = isRecovery ? "mfa-verify-recovery-code" : "mfa-verify-code";
|
|
70
|
+
const defaultSubtitle = isRecovery
|
|
71
|
+
? t("auth.mfa.verify.recoverySubtitle")
|
|
72
|
+
: t("auth.mfa.verify.subtitle");
|
|
73
|
+
return (_jsx(AuthCard, { title: title ?? t("auth.mfa.verify.title"), subtitle: subtitle ?? defaultSubtitle, children: _jsxs(Form, { onSubmit: onSubmit, children: [_jsx(Field, { id: fieldId, label: isRecovery ? t("auth.mfa.verify.recoveryCode") : t("auth.mfa.verify.code"), required: true, children: isRecovery ? (_jsx(Input, { kind: "text", id: fieldId, name: fieldId, value: code, onChange: setCode, disabled: submitting, required: true, autoFocus: true, autoComplete: "off" }, `${mode}-${attempt}`)) : (_jsx(Input, { kind: "code", length: MFA_CODE_LENGTH, id: fieldId, name: fieldId, value: code, onChange: setCode, onComplete: (next) => void doSubmit(next), disabled: submitting, hasError: error !== null, required: true, autoFocus: true }, `${mode}-${attempt}`)) }), error !== null ? _jsx(Banner, { variant: "error", children: t(reasonToKey(error)) }) : null, _jsx(Button, { type: "submit", loading: submitting, disabled: submitting, children: submitting
|
|
55
74
|
? t("auth.mfa.verify.submitting")
|
|
56
|
-
: (submitLabel ?? t("auth.mfa.verify.submit")) }),
|
|
75
|
+
: (submitLabel ?? t("auth.mfa.verify.submit")) }), _jsx(Button, { type: "button", variant: "link", onClick: toggleMode, disabled: submitting, children: isRecovery
|
|
76
|
+
? t("auth.mfa.verify.useAuthenticatorCode")
|
|
77
|
+
: t("auth.mfa.verify.useRecoveryCode") }), onCancel ? (_jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: submitting, children: t("auth.mfa.verify.backToLogin") })) : null] }) }));
|
|
57
78
|
}
|
|
@@ -17,20 +17,15 @@ export type ResolvedProvider = {
|
|
|
17
17
|
* entityName is registered. */
|
|
18
18
|
export declare function findProviderPlugin(ctx: HandlerContext, providerName: string): ResolvedProvider | null;
|
|
19
19
|
export declare function resolveProviderPlugin(ctx: HandlerContext, providerName: string): ResolvedProvider;
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* start-plan-checkout and switch-plan. `FeatureDisabledError` (not
|
|
30
|
-
* `UnconfiguredError`) when none is found: an unconfigured provider is
|
|
31
|
-
* the same "billing isn't live yet" state as `isPluginBillingEnabled`
|
|
32
|
-
* returning false, and the panel already renders both the same way. */
|
|
33
|
-
export declare function resolveCatalogProvider(ctx: HandlerContext, catalog: BillingPlanCatalog, handlerName: string): ResolvedProvider;
|
|
20
|
+
/** The providers a catalog sells through, in registration order: an explicit
|
|
21
|
+
* `catalog.providerName` (only that one), otherwise every registered provider
|
|
22
|
+
* exposing `priceToTier`. Empty when none qualifies, a state `billing-plans`
|
|
23
|
+
* renders as `enabled: false` instead of erroring. An explicit `providerName`
|
|
24
|
+
* that is not registered is a config bug and throws `UnconfiguredError`. */
|
|
25
|
+
export declare function listCatalogProviders(ctx: HandlerContext, catalog: BillingPlanCatalog): readonly ResolvedProvider[];
|
|
26
|
+
export declare function filterOfferedProviders(ctx: HandlerContext, providers: readonly ResolvedProvider[]): Promise<readonly ResolvedProvider[]>;
|
|
27
|
+
/** The catalog providers that are live right now: what a buyer can choose from. */
|
|
28
|
+
export declare function listOfferedCatalogProviders(ctx: HandlerContext, catalog: BillingPlanCatalog): Promise<readonly ResolvedProvider[]>;
|
|
34
29
|
/** `plugin.isBillingEnabled?.(ctx) ?? true` — the one place this fallback is
|
|
35
30
|
* spelled out, so no call-site forgets to await the optional method before
|
|
36
31
|
* applying the `?? true` default. */
|
|
@@ -15,14 +15,18 @@ import { BILLING_FOUNDATION_FEATURE, isSubscriptionBlockingCheckout, SUBSCRIPTIO
|
|
|
15
15
|
export function isNonEmptyStringArray(arr) {
|
|
16
16
|
return arr.length > 0;
|
|
17
17
|
}
|
|
18
|
+
/** Every registered provider plugin, in registration order. */
|
|
19
|
+
function listProviderPlugins(ctx) {
|
|
20
|
+
return ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION).map((usage) => ({
|
|
21
|
+
name: usage.entityName,
|
|
22
|
+
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
23
|
+
plugin: usage.options,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
18
26
|
/** Non-throwing provider lookup by name — null when no plugin with that
|
|
19
27
|
* entityName is registered. */
|
|
20
28
|
export function findProviderPlugin(ctx, providerName) {
|
|
21
|
-
|
|
22
|
-
.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION)
|
|
23
|
-
.find((u) => u.entityName === providerName);
|
|
24
|
-
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
25
|
-
return usage ? { name: providerName, plugin: usage.options } : null;
|
|
29
|
+
return listProviderPlugins(ctx).find((provider) => provider.name === providerName) ?? null;
|
|
26
30
|
}
|
|
27
31
|
export function resolveProviderPlugin(ctx, providerName) {
|
|
28
32
|
const found = findProviderPlugin(ctx, providerName);
|
|
@@ -35,14 +39,12 @@ export function resolveProviderPlugin(ctx, providerName) {
|
|
|
35
39
|
}
|
|
36
40
|
return found;
|
|
37
41
|
}
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
* `UnconfiguredError`. */
|
|
45
|
-
export function findCatalogProvider(ctx, catalog) {
|
|
42
|
+
/** The providers a catalog sells through, in registration order: an explicit
|
|
43
|
+
* `catalog.providerName` (only that one), otherwise every registered provider
|
|
44
|
+
* exposing `priceToTier`. Empty when none qualifies, a state `billing-plans`
|
|
45
|
+
* renders as `enabled: false` instead of erroring. An explicit `providerName`
|
|
46
|
+
* that is not registered is a config bug and throws `UnconfiguredError`. */
|
|
47
|
+
export function listCatalogProviders(ctx, catalog) {
|
|
46
48
|
if (catalog.providerName) {
|
|
47
49
|
const found = findProviderPlugin(ctx, catalog.providerName);
|
|
48
50
|
if (!found) {
|
|
@@ -52,34 +54,17 @@ export function findCatalogProvider(ctx, catalog) {
|
|
|
52
54
|
hint: `provider "${catalog.providerName}" not registered`,
|
|
53
55
|
});
|
|
54
56
|
}
|
|
55
|
-
return found;
|
|
56
|
-
}
|
|
57
|
-
const usages = ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION);
|
|
58
|
-
const withPriceCatalog = usages.filter((u) => u.options.priceToTier !== undefined);
|
|
59
|
-
if (withPriceCatalog.length > 1) {
|
|
60
|
-
throw new UnconfiguredError({
|
|
61
|
-
feature: "billing-foundation",
|
|
62
|
-
key: "catalog.providerName",
|
|
63
|
-
hint: `ambiguous — ${withPriceCatalog.length} registered plugins expose priceToTier, set catalog.providerName explicitly`,
|
|
64
|
-
});
|
|
57
|
+
return [found];
|
|
65
58
|
}
|
|
66
|
-
|
|
67
|
-
if (!usage)
|
|
68
|
-
return null;
|
|
69
|
-
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
70
|
-
return { name: usage.entityName, plugin: usage.options };
|
|
59
|
+
return listProviderPlugins(ctx).filter(({ plugin }) => plugin.priceToTier !== undefined);
|
|
71
60
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
export function
|
|
78
|
-
|
|
79
|
-
if (!found) {
|
|
80
|
-
throw new FeatureDisabledError(BILLING_FOUNDATION_FEATURE, handlerName);
|
|
81
|
-
}
|
|
82
|
-
return found;
|
|
61
|
+
export async function filterOfferedProviders(ctx, providers) {
|
|
62
|
+
const enabledFlags = await Promise.all(providers.map(({ plugin }) => isPluginBillingEnabled(ctx, plugin)));
|
|
63
|
+
return providers.filter((_, index) => enabledFlags[index]);
|
|
64
|
+
}
|
|
65
|
+
/** The catalog providers that are live right now: what a buyer can choose from. */
|
|
66
|
+
export async function listOfferedCatalogProviders(ctx, catalog) {
|
|
67
|
+
return filterOfferedProviders(ctx, listCatalogProviders(ctx, catalog));
|
|
83
68
|
}
|
|
84
69
|
/** `plugin.isBillingEnabled?.(ctx) ?? true` — the one place this fallback is
|
|
85
70
|
* spelled out, so no call-site forgets to await the optional method before
|
|
@@ -112,8 +112,14 @@ inlineHtmlMarkdown.use({
|
|
|
112
112
|
blockquote({ tokens }) {
|
|
113
113
|
return this.parser.parse(tokens);
|
|
114
114
|
},
|
|
115
|
-
list({ items }) {
|
|
116
|
-
|
|
115
|
+
list({ items, ordered, start }) {
|
|
116
|
+
const firstNumber = typeof start === "number" ? start : 1;
|
|
117
|
+
return items
|
|
118
|
+
.map((item, index) => {
|
|
119
|
+
const marker = ordered ? `${firstNumber + index}. ` : "";
|
|
120
|
+
return `${marker}${this.parser.parse(item.tokens)}`;
|
|
121
|
+
})
|
|
122
|
+
.join("");
|
|
117
123
|
},
|
|
118
124
|
hr: () => "",
|
|
119
125
|
space: () => "",
|
|
@@ -30,6 +30,7 @@ export type ConsentPriceDetails = {
|
|
|
30
30
|
readonly priceId: string;
|
|
31
31
|
readonly price: Pick<ProviderPrice, "unitAmount" | "currency" | "interval" | "intervalCount"> | null;
|
|
32
32
|
readonly orderItem?: OrderItem;
|
|
33
|
+
readonly providerName?: string;
|
|
33
34
|
};
|
|
34
35
|
export declare function prepareConsent(ctx: HandlerContext, options: ConsumerProtectionOptions, consent: ConsentPayload | undefined, mode: CheckoutMode): Promise<PreparedConsent>;
|
|
35
36
|
export declare function consentCheckoutFields(prepared: PreparedConsent): {
|
|
@@ -104,6 +104,7 @@ export async function recordConsent(ctx, prepared, details) {
|
|
|
104
104
|
termsTemplateVersion: prepared.termsTemplateVersion,
|
|
105
105
|
locale: prepared.locale,
|
|
106
106
|
actorUserId: String(ctx.user.id),
|
|
107
|
+
...(details.providerName !== undefined && { providerName: details.providerName }),
|
|
107
108
|
...(details.orderItem && {
|
|
108
109
|
itemLabelKey: details.orderItem.labelKey,
|
|
109
110
|
...(details.orderItem.params && { itemLabelParams: details.orderItem.params }),
|
|
@@ -6,6 +6,7 @@ import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework
|
|
|
6
6
|
import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
|
|
7
7
|
import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
8
8
|
import * as z from "zod";
|
|
9
|
+
import { decryptStoredUserEmail } from "../../shared/index.js";
|
|
9
10
|
import { requireTemplateResolver, TEXT_BLOCK_KIND } from "../../template-resolver/index.js";
|
|
10
11
|
import { UserQueries } from "../../user/index.js";
|
|
11
12
|
import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
|
|
@@ -13,7 +14,6 @@ import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, CONTRACT_CONFIRMATION_ISSUED_EVENT_
|
|
|
13
14
|
import { CONTRACT_CONFIRMATION_NOTIFICATION_TYPE } from "../web/constants.js";
|
|
14
15
|
import { contractConfirmationVariables } from "./confirmation-mail.js";
|
|
15
16
|
import { resolveConsentLocale } from "./consent-text.js";
|
|
16
|
-
import { openStoredUserEmail } from "./stored-user-email.js";
|
|
17
17
|
export const issueContractConfirmationSchema = z
|
|
18
18
|
.object({
|
|
19
19
|
consentId: z.string().min(1).max(100),
|
|
@@ -31,6 +31,12 @@ const recipientRowSchema = z.object({
|
|
|
31
31
|
function translatedOrUndefined(translation, key) {
|
|
32
32
|
return translation === key ? undefined : translation;
|
|
33
33
|
}
|
|
34
|
+
function vatNoteFor(consumerProtection, providerName) {
|
|
35
|
+
const providerNote = providerName === undefined
|
|
36
|
+
? undefined
|
|
37
|
+
: consumerProtection.providerTexts?.[providerName]?.vatNote;
|
|
38
|
+
return providerNote ?? consumerProtection.vatNote;
|
|
39
|
+
}
|
|
34
40
|
function pickTimeZone(userZone, tenantZone) {
|
|
35
41
|
if (userZone && isValidIanaTimeZone(userZone))
|
|
36
42
|
return userZone;
|
|
@@ -102,7 +108,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
|
|
|
102
108
|
id: consent.actorUserId,
|
|
103
109
|
}));
|
|
104
110
|
const recipientEmail = recipient.success && recipient.data.isDeleted !== true
|
|
105
|
-
? await
|
|
111
|
+
? await decryptStoredUserEmail(recipient.data.email, "billing-foundation:issue-contract-confirmation")
|
|
106
112
|
: null;
|
|
107
113
|
if (!recipient.success || recipientEmail === null) {
|
|
108
114
|
ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] recipient of consent ${payload.consentId} not found; no mail sent`);
|
|
@@ -142,7 +148,7 @@ export function createIssueContractConfirmationHandler(options, consumerProtecti
|
|
|
142
148
|
...(payload.currentPeriodEndIso !== undefined && {
|
|
143
149
|
currentPeriodEndIso: payload.currentPeriodEndIso,
|
|
144
150
|
}),
|
|
145
|
-
vatNote: consumerProtection.
|
|
151
|
+
vatNote: vatNoteFor(consumerProtection, consent.providerName),
|
|
146
152
|
operatorEmail: consumerProtection.operatorEmail,
|
|
147
153
|
termsContent: terms.content,
|
|
148
154
|
});
|
|
@@ -44,7 +44,9 @@ export function terminationReceiptTemplate() {
|
|
|
44
44
|
mailRepeatedRows("details", "text"),
|
|
45
45
|
mailParagraph(texts.receiptNextSteps),
|
|
46
46
|
mailParagraph(texts.receiptRevoke(OPERATOR_EMAIL_SLOT)),
|
|
47
|
-
mailParagraphWithLink(texts.receiptNotMe, texts.receiptNotMeLink, "revokeUrl"
|
|
47
|
+
mailParagraphWithLink(texts.receiptNotMe, texts.receiptNotMeLink, "revokeUrl", {
|
|
48
|
+
showIf: "revokeUrl",
|
|
49
|
+
}),
|
|
48
50
|
mailParagraph(`${texts.contact} {operatorEmail}`),
|
|
49
51
|
].join(""),
|
|
50
52
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export declare const RECEIPT_MAILS_PER_RECIPIENT = 3;
|
|
3
3
|
export declare const RECEIPT_WINDOW_SECONDS: number;
|
|
4
|
+
export type ReceiptMailVerdict = "allowed" | "limited" | "unavailable";
|
|
4
5
|
/** Consumes one receipt token for the recipient. Fails closed when the limiter
|
|
5
|
-
* errors or no digest key is wired: the declaration is already
|
|
6
|
-
|
|
6
|
+
* errors or no digest key is wired ("unavailable"): the declaration is already
|
|
7
|
+
* recorded, a missing receipt is the lesser harm. */
|
|
8
|
+
export declare function mayMailReceipt(ctx: HandlerContext, email: string): Promise<ReceiptMailVerdict>;
|
|
@@ -10,25 +10,26 @@ function receiptBucketKey(ctx, email) {
|
|
|
10
10
|
return digest === undefined ? undefined : `${RECEIPT_BUCKET_PREFIX}:${digest}`;
|
|
11
11
|
}
|
|
12
12
|
/** Consumes one receipt token for the recipient. Fails closed when the limiter
|
|
13
|
-
* errors or no digest key is wired: the declaration is already
|
|
13
|
+
* errors or no digest key is wired ("unavailable"): the declaration is already
|
|
14
|
+
* recorded, a missing receipt is the lesser harm. */
|
|
14
15
|
export async function mayMailReceipt(ctx, email) {
|
|
15
16
|
// skip: no rate limiter wired, nothing to count against
|
|
16
17
|
if (!ctx.rateLimit)
|
|
17
|
-
return
|
|
18
|
+
return "allowed";
|
|
18
19
|
try {
|
|
19
20
|
const bucketKey = receiptBucketKey(ctx, email);
|
|
20
21
|
if (bucketKey === undefined) {
|
|
21
22
|
ctx.log?.warn("[billing-foundation:termination] no rate-limit digest available, receipt mail withheld");
|
|
22
|
-
return
|
|
23
|
+
return "unavailable";
|
|
23
24
|
}
|
|
24
25
|
const decision = await ctx.rateLimit.check(bucketKey, {
|
|
25
26
|
limit: RECEIPT_MAILS_PER_RECIPIENT,
|
|
26
27
|
windowSeconds: RECEIPT_WINDOW_SECONDS,
|
|
27
28
|
});
|
|
28
|
-
return decision.allowed;
|
|
29
|
+
return decision.allowed ? "allowed" : "limited";
|
|
29
30
|
}
|
|
30
31
|
catch (error) {
|
|
31
32
|
ctx.log?.warn(`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
-
return
|
|
33
|
+
return "unavailable";
|
|
33
34
|
}
|
|
34
35
|
}
|