@cosmicdrift/kumiko-bundled-features 0.352.0 → 0.354.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/agent-doc-lint.d.ts +2 -0
- package/dist/agent-tools/agent-doc-lint.js +61 -1
- package/dist/agent-tools/agent-manifest.d.ts +1 -0
- package/dist/agent-tools/agent-manifest.js +17 -5
- package/dist/agent-tools/feature.js +7 -2
- package/dist/agent-tools/index.d.ts +1 -1
- package/dist/agent-tools/index.js +1 -1
- package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
- package/dist/agent-tools/manifest-deny-qns.js +17 -0
- package/dist/agent-tools/tool-catalog.d.ts +2 -1
- package/dist/agent-tools/tool-catalog.js +4 -1
- package/dist/agent-tools/tool-dispatch.js +1 -1
- package/dist/audit/handlers/list.query.js +13 -8
- package/dist/auth-email-password/handlers/change-password.write.js +1 -0
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-create.write.js +17 -0
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +4 -1
- package/dist/auth-email-password/handlers/login.write.js +1 -0
- package/dist/auth-email-password/handlers/reset-password.write.js +1 -0
- package/dist/auth-email-password/handlers/signup-confirm.write.js +6 -4
- package/dist/auth-email-password/handlers/signup-request.write.js +51 -77
- package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
- package/dist/auth-email-password/handlers/verify-email.write.js +1 -0
- package/dist/auth-email-password/index.d.ts +2 -0
- package/dist/auth-email-password/index.js +2 -0
- package/dist/auth-email-password/seeding.d.ts +2 -0
- package/dist/auth-email-password/seeding.js +30 -8
- package/dist/auth-email-password/signup-activation.d.ts +24 -0
- package/dist/auth-email-password/signup-activation.js +41 -0
- package/dist/auth-email-password/signup-token-store.d.ts +5 -0
- package/dist/auth-email-password/signup-token-store.js +8 -3
- package/dist/auth-email-password/web/auth-gate.d.ts +2 -2
- package/dist/auth-email-password/web/auth-gate.js +5 -5
- package/dist/auth-email-password/web/auth-redirect.d.ts +1 -0
- package/dist/auth-email-password/web/auth-redirect.js +14 -0
- package/dist/auth-email-password/web/login-screen.js +2 -2
- package/dist/auth-email-password/web/user-menu.js +1 -1
- package/dist/auth-mfa/feature.js +5 -0
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -0
- package/dist/auth-mfa/handlers/verify.write.js +1 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/screens.js +2 -2
- package/dist/auth-mfa/web/mfa-recovery-codes-reveal.js +1 -1
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +3 -3
- package/dist/auth-mfa/web/mfa-verify-screen.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-verify-screen.js +1 -1
- package/dist/billing-foundation/billing-info-query.d.ts +5 -2
- package/dist/billing-foundation/checkout-core.d.ts +7 -0
- package/dist/billing-foundation/checkout-core.js +7 -2
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-overview/access.js +2 -9
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
- package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
- package/dist/channel-in-app/in-app-channel.js +1 -1
- package/dist/channel-in-app/tables.js +4 -2
- package/dist/compliance-profiles/schema/profile-selection.d.ts +10 -8
- package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
- package/dist/config/handlers/values.query.d.ts +3 -1
- package/dist/config/handlers/values.query.js +7 -2
- package/dist/config/table.d.ts +6 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +110 -61
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/db/queries/address-opt-outs.d.ts +3 -1
- package/dist/delivery/db/queries/address-opt-outs.js +12 -7
- package/dist/delivery/delivery-service.js +60 -12
- package/dist/delivery/public-names.js +1 -1
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/unsubscribe.js +66 -14
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/feature-toggles/feature.js +2 -3
- package/dist/feature-toggles/toggle-runtime.d.ts +5 -1
- package/dist/feature-toggles/toggle-runtime.js +30 -6
- package/dist/feature-toggles/toggle-sync-signal.d.ts +3 -2
- package/dist/feature-toggles/toggle-sync-signal.js +29 -10
- package/dist/file-derivatives/handlers/public-variant.query.d.ts +1 -0
- package/dist/file-derivatives/handlers/public-variant.query.js +7 -1
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +5 -0
- package/dist/files-tenant-data/hooks.js +61 -24
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/feature.js +2 -10
- package/dist/folders/handlers/delete-folder.write.js +31 -4
- package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
- package/dist/form-draft/db/queries/cleanup.js +4 -3
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
- package/dist/form-draft/handlers/cleanup.job.js +11 -5
- package/dist/foundation-shared/index.d.ts +1 -1
- package/dist/foundation-shared/index.js +1 -1
- package/dist/foundation-shared/mail-host-policy.d.ts +4 -0
- package/dist/foundation-shared/mail-host-policy.js +13 -4
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/inbound-provider-imap/feature.js +2 -0
- package/dist/inbound-provider-imap/imap-client.js +3 -4
- package/dist/jobs/constants.d.ts +1 -0
- package/dist/jobs/constants.js +1 -0
- package/dist/jobs/db/queries/retention.d.ts +1 -0
- package/dist/jobs/db/queries/retention.js +5 -0
- package/dist/jobs/db/queries/stale-run-sweep.d.ts +2 -0
- package/dist/jobs/db/queries/stale-run-sweep.js +13 -2
- package/dist/jobs/feature.js +7 -1
- package/dist/jobs/handlers/list.query.d.ts +2 -1
- package/dist/jobs/handlers/list.query.js +40 -15
- package/dist/jobs/handlers/tenant-failures.query.js +1 -1
- package/dist/jobs/handlers/tenant-runs.query.d.ts +17 -0
- package/dist/jobs/handlers/tenant-runs.query.js +72 -0
- package/dist/jobs/i18n.js +1 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.js +1 -0
- package/dist/jobs/job-run-logger.d.ts +2 -2
- package/dist/jobs/job-run-logger.js +98 -9
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/jobs/tenant-job-failure-table.js +15 -2
- package/dist/jobs/tenant-job-run-table.d.ts +4 -0
- package/dist/jobs/tenant-job-run-table.js +34 -0
- package/dist/ledger/entity.d.ts +24 -8
- package/dist/ledger/entity.js +17 -5
- package/dist/ledger/feature.js +3 -0
- package/dist/ledger/handlers/confirm-schedule-period.write.js +9 -3
- package/dist/ledger/handlers/create-transaction.write.js +1 -0
- package/dist/ledger/handlers/reverse-transaction.write.js +1 -0
- package/dist/ledger/schedule-subject-pair.d.ts +2 -0
- package/dist/ledger/schedule-subject-pair.js +24 -0
- package/dist/ledger/schemas.js +8 -2
- package/dist/mail-transport-smtp/feature.d.ts +3 -4
- package/dist/mail-transport-smtp/feature.js +4 -13
- package/dist/managed-pages/feature.js +28 -10
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/entity.js +4 -0
- package/dist/notes-history/feature.js +2 -10
- package/dist/personal-access-tokens/handlers/create.write.js +1 -0
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/screens.js +14 -3
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/feature.js +28 -6
- package/dist/sessions/feature.d.ts +1 -0
- package/dist/sessions/feature.js +9 -8
- package/dist/sessions/handlers/detail.query.d.ts +1 -1
- package/dist/sessions/handlers/detail.query.js +2 -2
- package/dist/sessions/handlers/list.query.d.ts +1 -1
- package/dist/sessions/handlers/list.query.js +5 -3
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/screens.d.ts +2 -2
- package/dist/sessions/screens.js +6 -7
- package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
- package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
- package/dist/shared/encrypt-for-direct-write.js +10 -2
- package/dist/shared/index.d.ts +4 -3
- package/dist/shared/index.js +4 -3
- package/dist/shared/parent-visibility.d.ts +2 -1
- package/dist/shared/parent-visibility.js +21 -0
- package/dist/shared/rate-limited-text-response.d.ts +2 -0
- package/dist/shared/rate-limited-text-response.js +9 -0
- package/dist/shared/row-bound-grant.js +16 -2
- package/dist/shared/session-locale-field.d.ts +1 -0
- package/dist/shared/session-locale-field.js +6 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/feature.js +2 -10
- package/dist/template-resolver/api.d.ts +2 -2
- package/dist/template-resolver/handlers/list.query.d.ts +2 -0
- package/dist/template-resolver/handlers/list.query.js +2 -2
- package/dist/template-resolver/table.d.ts +48 -30
- package/dist/template-resolver/user-content-table.d.ts +34 -20
- package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
- package/dist/tenant/decrypt-tenant-names.js +10 -0
- package/dist/tenant/feature.js +7 -1
- package/dist/tenant/handlers/me.query.js +5 -1
- package/dist/tenant/handlers/memberships.query.js +3 -2
- package/dist/tenant/handlers/tenant-directory.query.js +9 -3
- package/dist/tenant/handlers/toggle-enabled.write.js +2 -0
- package/dist/tenant/handlers/update-member-roles.write.js +1 -0
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +54 -8
- package/dist/tenant/invitation-table.js +6 -1
- package/dist/tenant/invited-membership.d.ts +8 -6
- package/dist/tenant/invited-membership.js +28 -7
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +18 -10
- package/dist/tenant/schema/tenant.js +4 -6
- package/dist/tenant/screens.js +1 -1
- package/dist/tenant-handover/move-entity-graph.js +11 -18
- package/dist/tenant-handover/transfer-graph.d.ts +2 -21
- package/dist/tenant-handover/transfer-graph.js +8 -75
- package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
- package/dist/tenant-lifecycle/boot-checks.js +13 -0
- package/dist/tenant-lifecycle/constants.d.ts +3 -0
- package/dist/tenant-lifecycle/constants.js +5 -0
- package/dist/tenant-lifecycle/events.d.ts +5 -0
- package/dist/tenant-lifecycle/events.js +5 -0
- package/dist/tenant-lifecycle/feature.js +6 -3
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +10 -5
- package/dist/tenant-lifecycle/run-tenant-destroy.js +59 -17
- package/dist/tenant-lifecycle/stages.d.ts +8 -1
- package/dist/tenant-lifecycle/stages.js +33 -4
- package/dist/tenant-lifecycle/testing.d.ts +8 -0
- package/dist/tenant-lifecycle/testing.js +42 -0
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -0
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/handlers/detail.query.js +2 -0
- package/dist/user/handlers/list.query.js +6 -2
- package/dist/user/handlers/update.write.js +1 -0
- package/dist/user/schema/user.d.ts +22 -8
- package/dist/user/seeding.d.ts +1 -0
- package/dist/user/seeding.js +20 -0
- package/dist/user-data-rights/boot-checks.js +5 -0
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -0
- package/dist/user-data-rights/index.d.ts +1 -0
- package/dist/user-data-rights/index.js +1 -0
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-attempt.js +17 -9
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +16 -8
- package/dist/user-profile/handlers/change-email.write.js +8 -2
- package/dist/waitlist/entity.d.ts +22 -6
- package/dist/workflow-runner/event-trigger.d.ts +1 -0
- package/dist/workflow-runner/event-trigger.js +63 -15
- package/dist/workflow-runner/handlers/resume-run.write.js +8 -2
- package/dist/workflow-runner/runner.d.ts +2 -1
- package/dist/workflow-runner/runner.js +5 -1
- package/package.json +35 -10
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +24 -0
- package/src/auth-mfa/changes.json +7 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/channel-in-app/changes.json +8 -1
- package/src/crypto-shredding/changes.json +12 -0
- package/src/delivery/changes.json +6 -0
- package/src/files/changes.json +24 -0
- package/src/files-tenant-data/changes.json +5 -0
- package/src/folders/changes.json +6 -0
- package/src/form-draft/changes.json +7 -1
- package/src/inbound-provider-imap/changes.json +6 -0
- package/src/jobs/changes.json +30 -0
- package/src/ledger/changes.json +23 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/notes-history/changes.json +13 -0
- package/src/seo/changes.json +5 -0
- package/src/template-resolver/changes.json +5 -0
- package/src/tenant/changes.json +19 -0
- package/src/tenant-handover/changes.json +12 -0
- package/src/tenant-lifecycle/changes.json +22 -0
- package/src/tier-engine/changes.json +5 -0
- package/src/user/changes.json +15 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +5 -0
- package/dist/shared/has-where-rule.d.ts +0 -2
- package/dist/shared/has-where-rule.js +0 -10
|
@@ -20,18 +20,14 @@
|
|
|
20
20
|
// stays always-200 + resend-idempotent so the request path leaks nothing;
|
|
21
21
|
// suppressing the link on the request side would be defense-in-depth, but
|
|
22
22
|
// with an enumeration risk of its own (separate concern).
|
|
23
|
-
import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
24
23
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
25
24
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
26
|
-
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
27
25
|
import * as z from "zod";
|
|
28
26
|
import { findSignupHandoverProvider } from "../../shared/index.js";
|
|
29
|
-
import {
|
|
30
|
-
import { renderActivationEmail } from "../email-templates.js";
|
|
31
|
-
import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.js";
|
|
27
|
+
import { resolveHandlerMailLocale } from "../magic-link-mail.js";
|
|
32
28
|
import { AUTH_SELF_REGISTRATION_FEATURE } from "../self-registration-toggle.js";
|
|
33
|
-
import {
|
|
34
|
-
|
|
29
|
+
import { issueSignupActivation } from "../signup-activation.js";
|
|
30
|
+
import { storeSignupHandover, takeOverSignupHandoverForResend } from "../signup-token-store.js";
|
|
35
31
|
const SignupRequestSchema = z.object({
|
|
36
32
|
email: z.email(),
|
|
37
33
|
// Cross-device try-first handover (kumiko-framework#3035 follow-up,
|
|
@@ -47,8 +43,6 @@ const SignupRequestSchema = z.object({
|
|
|
47
43
|
});
|
|
48
44
|
const HANDOVER_GRANT_VERIFY_REASON = "verifies a tenant-handover grant against its root row in the source tenant (read-only anchor lookup) before binding it to the signup token";
|
|
49
45
|
export function createSignupRequestHandler(opts) {
|
|
50
|
-
const ttlMinutes = opts.tokenTtlMinutes ?? AUTH_SIGNUP_DEFAULT_TTL_MINUTES;
|
|
51
|
-
const ttlSeconds = ttlMinutes * 60;
|
|
52
46
|
return defineWriteHandler({
|
|
53
47
|
name: "signup-request",
|
|
54
48
|
schema: SignupRequestSchema,
|
|
@@ -93,84 +87,64 @@ export function createSignupRequestHandler(opts) {
|
|
|
93
87
|
// the visitor just retyped their email) must still carry the earlier
|
|
94
88
|
// verified grant over to the new token instead of silently dropping it.
|
|
95
89
|
const priorBinding = await takeOverSignupHandoverForResend(ctx.redis, email);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const provider = findSignupHandoverProvider(ctx.registry);
|
|
122
|
-
if (!provider) {
|
|
123
|
-
ctx.log?.warn("signup-request: handover payload present but no signupHandover provider mounted");
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
try {
|
|
127
|
-
const verified = await provider.verifyGrant({
|
|
128
|
-
db: ctx.db.unsafeRaw(),
|
|
129
|
-
registry: ctx.registry,
|
|
130
|
-
entityType,
|
|
131
|
-
token: grantToken,
|
|
132
|
-
});
|
|
133
|
-
if (verified) {
|
|
134
|
-
binding = verified;
|
|
90
|
+
const locale = resolveHandlerMailLocale(ctx, opts.locale);
|
|
91
|
+
const redis = ctx.redis;
|
|
92
|
+
const issued = await issueSignupActivation({
|
|
93
|
+
redis,
|
|
94
|
+
notify: ctx.notify,
|
|
95
|
+
email,
|
|
96
|
+
appUrl: opts.appUrl,
|
|
97
|
+
timeZone: ctx.tz.user,
|
|
98
|
+
...(opts.tokenTtlMinutes !== undefined && { tokenTtlMinutes: opts.tokenTtlMinutes }),
|
|
99
|
+
...(opts.appName !== undefined && { appName: opts.appName }),
|
|
100
|
+
...(locale !== undefined && { locale }),
|
|
101
|
+
// A handover grant in this request re-verifies and replaces the prior
|
|
102
|
+
// binding; anything short of a fresh, verified grant (no provider
|
|
103
|
+
// mounted, or verification failed) falls back to it instead —
|
|
104
|
+
// dropping a still-live binding on a bad request would strand the
|
|
105
|
+
// visitor's run. A throw here must never skip the mail, or the
|
|
106
|
+
// signup itself silently breaks for a bug in a provider this handler
|
|
107
|
+
// doesn't own.
|
|
108
|
+
onTokenStored: async ({ token, ttlSeconds }) => {
|
|
109
|
+
let binding = priorBinding;
|
|
110
|
+
if (event.payload.handover) {
|
|
111
|
+
const { entityType, token: grantToken } = event.payload.handover;
|
|
112
|
+
const provider = findSignupHandoverProvider(ctx.registry);
|
|
113
|
+
if (!provider) {
|
|
114
|
+
ctx.log?.warn("signup-request: handover payload present but no signupHandover provider mounted");
|
|
135
115
|
}
|
|
136
116
|
else {
|
|
137
|
-
|
|
117
|
+
try {
|
|
118
|
+
const verified = await provider.verifyGrant({
|
|
119
|
+
db: ctx.db.unsafeRaw(),
|
|
120
|
+
registry: ctx.registry,
|
|
121
|
+
entityType,
|
|
122
|
+
token: grantToken,
|
|
123
|
+
});
|
|
124
|
+
if (verified) {
|
|
125
|
+
binding = verified;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
ctx.log?.warn("signup-request: handover grant did not verify", { entityType });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
ctx.log?.warn("signup-request: handover grant verification threw", { entityType });
|
|
133
|
+
}
|
|
138
134
|
}
|
|
139
135
|
}
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
if (binding) {
|
|
137
|
+
await storeSignupHandover(redis, { token, binding, ttlSeconds });
|
|
142
138
|
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (binding) {
|
|
146
|
-
await storeSignupHandover(ctx.redis, { token, binding, ttlSeconds });
|
|
147
|
-
}
|
|
148
|
-
// normalizeEmail from the store — one source of truth for
|
|
149
|
-
// normalization; the delivery recipient + lookup path consistently
|
|
150
|
-
// get the same format.
|
|
151
|
-
const normalizedEmail = normalizeEmail(email);
|
|
152
|
-
const locale = resolveHandlerMailLocale(ctx, opts.locale);
|
|
153
|
-
await dispatchMagicLinkMail(ctx.notify, {
|
|
154
|
-
handlerName: "signup-request",
|
|
155
|
-
notificationType: SIGNUP_NOTIFICATION_TYPE,
|
|
156
|
-
renderContent: renderActivationEmail,
|
|
157
|
-
}, {
|
|
158
|
-
email: normalizedEmail,
|
|
159
|
-
appUrl: opts.appUrl,
|
|
160
|
-
token,
|
|
161
|
-
expiresAt: expiresAtIso,
|
|
162
|
-
issuedAt: issuedAt.toString(),
|
|
163
|
-
timeZone: ctx.tz.user,
|
|
164
|
-
...(opts.appName !== undefined && { appName: opts.appName }),
|
|
165
|
-
locale,
|
|
139
|
+
},
|
|
166
140
|
});
|
|
167
141
|
return {
|
|
168
142
|
isSuccess: true,
|
|
169
143
|
data: {
|
|
170
144
|
kind: "signup-requested",
|
|
171
|
-
email:
|
|
172
|
-
token,
|
|
173
|
-
expiresAt:
|
|
145
|
+
email: issued.email,
|
|
146
|
+
token: issued.token,
|
|
147
|
+
expiresAt: issued.expiresAt,
|
|
174
148
|
},
|
|
175
149
|
};
|
|
176
150
|
},
|
|
@@ -35,6 +35,7 @@ export function createTokenRequestHandler(spec, opts) {
|
|
|
35
35
|
rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
|
|
36
36
|
additionalRateLimits: [{ per: { payloadField: "email" }, limit: 5, windowSeconds: 86400 }],
|
|
37
37
|
escapeHatch: {
|
|
38
|
+
grants: ["systemIdentity"],
|
|
38
39
|
reason: "Anonymous requester has no session — looks up the target user by email via " +
|
|
39
40
|
"ctx.queryAs(SYSTEM, user:findForAuth) to mint the out-of-band token.",
|
|
40
41
|
},
|
|
@@ -21,6 +21,7 @@ export function createVerifyEmailHandler(opts) {
|
|
|
21
21
|
access: { roles: ["anonymous"] },
|
|
22
22
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
23
23
|
escapeHatch: {
|
|
24
|
+
grants: ["systemIdentity"],
|
|
24
25
|
reason: "Anonymous token holder has no session — runConfirmTokenFlow loads and updates the " +
|
|
25
26
|
"target user via ctx.writeAs(SYSTEM, user:update) to set emailVerified.",
|
|
26
27
|
},
|
|
@@ -9,3 +9,5 @@ export type { AccountLockoutOptions, AccountUnlockOptions, AuthEmailPasswordOpti
|
|
|
9
9
|
export { authEmailPasswordEnvSchema, createAuthEmailPasswordFeature } from "./feature.js";
|
|
10
10
|
export { AUTH_SELF_REGISTRATION_FEATURE, createAuthSelfRegistrationToggleFeature, } from "./self-registration-toggle.js";
|
|
11
11
|
export { signToken, TokenPurpose, type VerifyResult, verifyToken } from "./signed-token.js";
|
|
12
|
+
export { type IssuedSignupActivation, type IssueSignupActivationArgs, issueSignupActivation, SIGNUP_ACTIVATION_NOTIFICATION_TYPE, } from "./signup-activation.js";
|
|
13
|
+
export { invalidateExistingSignupToken, normalizeEmail, SIGNUP_TOKEN_KEY_PREFIXES, storeSignupToken, } from "./signup-token-store.js";
|
|
@@ -17,3 +17,5 @@ export { AUTH_SELF_REGISTRATION_FEATURE, createAuthSelfRegistrationToggleFeature
|
|
|
17
17
|
// statt eigene HMAC-Logik zu duplizieren. Purpose-string diskriminiert
|
|
18
18
|
// Cross-Replay zwischen Flows.
|
|
19
19
|
export { signToken, TokenPurpose, verifyToken } from "./signed-token.js";
|
|
20
|
+
export { issueSignupActivation, SIGNUP_ACTIVATION_NOTIFICATION_TYPE, } from "./signup-activation.js";
|
|
21
|
+
export { invalidateExistingSignupToken, normalizeEmail, SIGNUP_TOKEN_KEY_PREFIXES, storeSignupToken, } from "./signup-token-store.js";
|
|
@@ -64,6 +64,8 @@ export type ProvisionSignupAccountOptions = {
|
|
|
64
64
|
readonly tenantName: string;
|
|
65
65
|
readonly tenantId: TenantId;
|
|
66
66
|
readonly memberRoles?: readonly string[];
|
|
67
|
+
/** Stored on the new user row so the first session already carries the locale claim. */
|
|
68
|
+
readonly locale?: string;
|
|
67
69
|
};
|
|
68
70
|
export declare function provisionSignupAccount(db: DbConnection, options: ProvisionSignupAccountOptions, hooks?: SeedTenantHooks): Promise<{
|
|
69
71
|
readonly userId: string;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
15
15
|
import { configuredBlindIndexKey, configuredPiiSubjectKms, decryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
16
16
|
import { ConflictError } from "@cosmicdrift/kumiko-framework/errors";
|
|
17
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
17
18
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
18
19
|
import { hashPassword } from "../shared/index.js";
|
|
19
20
|
// kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
|
|
@@ -21,7 +22,8 @@ import { seedTenant, seedTenantMembership } from "../tenant/seeding.js";
|
|
|
21
22
|
// kumiko-lint-ignore cross-feature-import signup create-only guard reads the user projection by email
|
|
22
23
|
import { USER_STATUS, userTable } from "../user/schema/user.js";
|
|
23
24
|
// kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
|
|
24
|
-
import { reconcileSeededUserEmailVerified, seedUser } from "../user/seeding.js";
|
|
25
|
+
import { reconcileSeededUserEmailVerified, rewriteSeededUserEmail, seedUser, } from "../user/seeding.js";
|
|
26
|
+
const log = createFallbackLogger("auth-email-password-seeding");
|
|
25
27
|
// Re-export für ergonomische Single-Import-Site in tests/seed-scripts.
|
|
26
28
|
// Das Auth-Feature ist der natürliche Aufrufer für "seed admin user mit
|
|
27
29
|
// password + tenant + membership" — wer das nutzt soll nicht aus drei
|
|
@@ -114,6 +116,7 @@ hooks) {
|
|
|
114
116
|
password: options.password,
|
|
115
117
|
displayName: options.displayName,
|
|
116
118
|
emailVerified: true,
|
|
119
|
+
...(options.locale !== undefined && { locale: options.locale }),
|
|
117
120
|
}, hooks);
|
|
118
121
|
await seedTenantMembership(db, {
|
|
119
122
|
userId,
|
|
@@ -186,16 +189,30 @@ async function findExistingUserIdForEmail(db, kms, email) {
|
|
|
186
189
|
status: { ne: USER_STATUS.Deleted },
|
|
187
190
|
});
|
|
188
191
|
const matches = [];
|
|
192
|
+
let undecryptableRows = 0;
|
|
189
193
|
for (const row of rows) {
|
|
190
194
|
// Compared byte-exact, deliberately not case-folded: email_bidx is an
|
|
191
195
|
// HMAC over the raw value and login matches case-sensitively, so a case
|
|
192
196
|
// variant is a different account to every other code path.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
// One undecryptable foreign row (missing key row, ciphertext from another
|
|
198
|
+
// KMS store) must not abort the boot seed: the account we look for is
|
|
199
|
+
// decryptable by definition.
|
|
200
|
+
let decrypted;
|
|
201
|
+
try {
|
|
202
|
+
decrypted = await decryptPiiFieldValues({ email: row.email }, ["email"], kms, {
|
|
203
|
+
requestId: "seed-admin-guarded",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
undecryptableRows += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
196
210
|
if (decrypted["email"] === email)
|
|
197
211
|
matches.push(row);
|
|
198
212
|
}
|
|
213
|
+
if (undecryptableRows > 0) {
|
|
214
|
+
log.warn(`seedAdminGuarded: skipped ${undecryptableRows} user row(s) whose email could not be decrypted`);
|
|
215
|
+
}
|
|
199
216
|
return matches.length === 0 ? undefined : pickCanonicalUserId(matches);
|
|
200
217
|
}
|
|
201
218
|
/**
|
|
@@ -225,10 +242,15 @@ export async function seedAdminGuarded(db, options) {
|
|
|
225
242
|
if (existingId === undefined)
|
|
226
243
|
return seedAdmin(db, options);
|
|
227
244
|
const by = options.by ?? TestUsers.systemAdmin;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
245
|
+
const existingRow = await fetchOne(db, userTable, { id: existingId });
|
|
246
|
+
if (existingRow) {
|
|
247
|
+
// Re-write the email so email_bidx is recomputed; without it the account
|
|
248
|
+
// stays unfindable for login and every boot re-scans the whole table.
|
|
249
|
+
await rewriteSeededUserEmail(db, existingRow, options.email, by);
|
|
250
|
+
if (options.emailVerified === true) {
|
|
251
|
+
const refreshed = (await fetchOne(db, userTable, { id: existingId })) ?? existingRow;
|
|
252
|
+
await reconcileSeededUserEmailVerified(db, refreshed, by);
|
|
253
|
+
}
|
|
232
254
|
}
|
|
233
255
|
for (const m of options.memberships) {
|
|
234
256
|
await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, by });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NotifyFn } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { Redis } from "ioredis";
|
|
3
|
+
export declare const SIGNUP_ACTIVATION_NOTIFICATION_TYPE = "auth-email-password:signup-activation";
|
|
4
|
+
export type IssueSignupActivationArgs = {
|
|
5
|
+
readonly redis: Redis;
|
|
6
|
+
readonly notify: NotifyFn | undefined;
|
|
7
|
+
readonly email: string;
|
|
8
|
+
readonly appUrl: string | ((locale: string) => string);
|
|
9
|
+
readonly appName?: string;
|
|
10
|
+
readonly locale?: string;
|
|
11
|
+
readonly timeZone?: string;
|
|
12
|
+
readonly tokenTtlMinutes?: number;
|
|
13
|
+
/** Runs after the token is stored and before the mail goes out. */
|
|
14
|
+
readonly onTokenStored?: (stored: {
|
|
15
|
+
readonly token: string;
|
|
16
|
+
readonly ttlSeconds: number;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export type IssuedSignupActivation = {
|
|
20
|
+
readonly email: string;
|
|
21
|
+
readonly token: string;
|
|
22
|
+
readonly expiresAt: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function issueSignupActivation(args: IssueSignupActivationArgs): Promise<IssuedSignupActivation>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Mints a signup activation link: invalidates any live token for the email,
|
|
2
|
+
// stores a fresh one and mails the activation link. Shared by the
|
|
3
|
+
// signup-request handler and by apps that issue the same link from their own
|
|
4
|
+
// flow (e.g. a waitlist invite), so TTL, store keys and notification type
|
|
5
|
+
// cannot drift between the two.
|
|
6
|
+
import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
7
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
8
|
+
import { AUTH_SIGNUP_DEFAULT_TTL_MINUTES } from "./constants.js";
|
|
9
|
+
import { renderActivationEmail } from "./email-templates.js";
|
|
10
|
+
import { dispatchMagicLinkMail } from "./magic-link-mail.js";
|
|
11
|
+
import { invalidateExistingSignupToken, normalizeEmail, storeSignupToken, } from "./signup-token-store.js";
|
|
12
|
+
export const SIGNUP_ACTIVATION_NOTIFICATION_TYPE = "auth-email-password:signup-activation";
|
|
13
|
+
export async function issueSignupActivation(args) {
|
|
14
|
+
const ttlSeconds = (args.tokenTtlMinutes ?? AUTH_SIGNUP_DEFAULT_TTL_MINUTES) * 60;
|
|
15
|
+
// At most one live signup token per email.
|
|
16
|
+
await invalidateExistingSignupToken(args.redis, args.email);
|
|
17
|
+
// 256 bits from node:crypto — never Math.random: xorshift128+ state is
|
|
18
|
+
// reconstructible from a few observed outputs, which would let an attacker
|
|
19
|
+
// predict other users' tokens.
|
|
20
|
+
const token = generateToken();
|
|
21
|
+
const issuedAt = Temporal.Now.instant();
|
|
22
|
+
const expiresAt = issuedAt.add({ seconds: ttlSeconds }).toString();
|
|
23
|
+
await storeSignupToken(args.redis, { email: args.email, token, ttlSeconds });
|
|
24
|
+
await args.onTokenStored?.({ token, ttlSeconds });
|
|
25
|
+
const email = normalizeEmail(args.email);
|
|
26
|
+
await dispatchMagicLinkMail(args.notify, {
|
|
27
|
+
handlerName: "signup-request",
|
|
28
|
+
notificationType: SIGNUP_ACTIVATION_NOTIFICATION_TYPE,
|
|
29
|
+
renderContent: renderActivationEmail,
|
|
30
|
+
}, {
|
|
31
|
+
email,
|
|
32
|
+
appUrl: args.appUrl,
|
|
33
|
+
token,
|
|
34
|
+
expiresAt,
|
|
35
|
+
issuedAt: issuedAt.toString(),
|
|
36
|
+
...(args.timeZone !== undefined && { timeZone: args.timeZone }),
|
|
37
|
+
...(args.appName !== undefined && { appName: args.appName }),
|
|
38
|
+
...(args.locale !== undefined && { locale: args.locale }),
|
|
39
|
+
});
|
|
40
|
+
return { email, token, expiresAt };
|
|
41
|
+
}
|
|
@@ -5,6 +5,11 @@ import { type SignupHandoverBinding } from "../shared/index.js";
|
|
|
5
5
|
* in the return body / mail send). Previously two places called
|
|
6
6
|
* `.toLowerCase()` — one source means no drift. */
|
|
7
7
|
export declare function normalizeEmail(email: string): string;
|
|
8
|
+
export declare const SIGNUP_TOKEN_KEY_PREFIXES: {
|
|
9
|
+
readonly token: "signup:by-token:";
|
|
10
|
+
readonly email: "signup:by-email:";
|
|
11
|
+
readonly burn: "signup:burn:";
|
|
12
|
+
};
|
|
8
13
|
/** Stores the pair bidirectionally and sets TTL on both keys.
|
|
9
14
|
* Idempotent — re-writing the same token-email pair is fine. */
|
|
10
15
|
export declare function storeSignupToken(redis: Redis, args: {
|
|
@@ -18,10 +18,15 @@ import { createSingleUseTokenStore, hashSingleUseToken, } from "../shared/index.
|
|
|
18
18
|
export function normalizeEmail(email) {
|
|
19
19
|
return email.toLowerCase();
|
|
20
20
|
}
|
|
21
|
+
export const SIGNUP_TOKEN_KEY_PREFIXES = {
|
|
22
|
+
token: "signup:by-token:",
|
|
23
|
+
email: "signup:by-email:",
|
|
24
|
+
burn: "signup:burn:",
|
|
25
|
+
};
|
|
21
26
|
const store = createSingleUseTokenStore({
|
|
22
|
-
tokenPrefix:
|
|
23
|
-
subjectPrefix:
|
|
24
|
-
burnPrefix:
|
|
27
|
+
tokenPrefix: SIGNUP_TOKEN_KEY_PREFIXES.token,
|
|
28
|
+
subjectPrefix: SIGNUP_TOKEN_KEY_PREFIXES.email,
|
|
29
|
+
burnPrefix: SIGNUP_TOKEN_KEY_PREFIXES.burn,
|
|
25
30
|
});
|
|
26
31
|
/** Stores the pair bidirectionally and sets TTL on both keys.
|
|
27
32
|
* Idempotent — re-writing the same token-email pair is fine. */
|
|
@@ -2,13 +2,13 @@ import { type ComponentType, type ReactNode } from "react";
|
|
|
2
2
|
import { type LoginScreenProps } from "./login-screen.js";
|
|
3
3
|
export type MfaVerifyComponentProps = {
|
|
4
4
|
readonly challengeToken: string;
|
|
5
|
-
readonly onSuccess?: () => void;
|
|
5
|
+
readonly onSuccess?: (landingPath?: string) => void;
|
|
6
6
|
readonly onCancel?: () => void;
|
|
7
7
|
};
|
|
8
8
|
export type MfaSetupComponentProps = {
|
|
9
9
|
readonly preauthSetupToken: string;
|
|
10
10
|
readonly accountLabel: string;
|
|
11
|
-
readonly onSuccess?: () => void;
|
|
11
|
+
readonly onSuccess?: (landingPath?: string) => void;
|
|
12
12
|
readonly onCancel?: () => void;
|
|
13
13
|
};
|
|
14
14
|
export type LoginRouteOptions = {
|
|
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
|
11
11
|
// Screen rein konfigurieren, ohne den Gate selbst ersetzen zu müssen.
|
|
12
12
|
import { useLocale } from "@cosmicdrift/kumiko-renderer";
|
|
13
13
|
import { useEffect, useState } from "react";
|
|
14
|
-
import { assertNavigableUrl, buildLoginRedirectUrl,
|
|
14
|
+
import { assertNavigableUrl, buildLoginRedirectUrl, followNextOrLandingAfterLogin, } from "./auth-redirect.js";
|
|
15
15
|
import { LoginScreen } from "./login-screen.js";
|
|
16
16
|
import { SessionProvider, useSession } from "./session.js";
|
|
17
17
|
import { SessionBootstrapErrorScreen } from "./session-bootstrap-error.js";
|
|
@@ -59,15 +59,15 @@ export function createLoginRoute(opts = {}) {
|
|
|
59
59
|
if (status === "authenticated" && onAuthenticated)
|
|
60
60
|
return null;
|
|
61
61
|
if (challengeToken !== null && MfaVerifyComponent) {
|
|
62
|
-
return (_jsx(MfaVerifyComponent, { challengeToken: challengeToken, onSuccess: () => {
|
|
62
|
+
return (_jsx(MfaVerifyComponent, { challengeToken: challengeToken, onSuccess: (landingPath) => {
|
|
63
63
|
setChallengeToken(null);
|
|
64
|
-
|
|
64
|
+
followNextOrLandingAfterLogin(landingPath);
|
|
65
65
|
}, onCancel: () => setChallengeToken(null) }));
|
|
66
66
|
}
|
|
67
67
|
// Pending preauthSetupToken from LoginScreen's onMfaSetupRequired. Same
|
|
68
68
|
// reasoning as challengeToken — a UI-only transition, not session state.
|
|
69
69
|
if (setupRequest !== null && MfaSetupComponent) {
|
|
70
|
-
return (_jsx(MfaSetupComponent, { preauthSetupToken: setupRequest.preauthSetupToken, accountLabel: setupRequest.accountLabel, onSuccess: () => {
|
|
70
|
+
return (_jsx(MfaSetupComponent, { preauthSetupToken: setupRequest.preauthSetupToken, accountLabel: setupRequest.accountLabel, onSuccess: (landingPath) => {
|
|
71
71
|
// MfaSetupPreauthScreen has no session to refresh itself with
|
|
72
72
|
// (it runs pre-auth) — the gate owns the session, so it refreshes.
|
|
73
73
|
// refresh() never rejects: a failed refresh surfaces as status
|
|
@@ -75,7 +75,7 @@ export function createLoginRoute(opts = {}) {
|
|
|
75
75
|
// success path needs to clear setupRequest.
|
|
76
76
|
void refresh().then(() => {
|
|
77
77
|
setSetupRequest(null);
|
|
78
|
-
|
|
78
|
+
followNextOrLandingAfterLogin(landingPath);
|
|
79
79
|
});
|
|
80
80
|
}, onCancel: () => setSetupRequest(null) }));
|
|
81
81
|
}
|
|
@@ -4,3 +4,4 @@ export declare function readNextFromSearch(search: string): string | null;
|
|
|
4
4
|
export declare function assertNavigableUrl(url: string, optionName: string): void;
|
|
5
5
|
export declare function buildLoginRedirectUrl(loginUrl: string, returnPath: string, currentOrigin: string): string;
|
|
6
6
|
export declare function followNextAfterLogin(location?: Pick<Location, "search" | "pathname" | "replace">): boolean;
|
|
7
|
+
export declare function followNextOrLandingAfterLogin(landingPath: string | undefined, location?: Pick<Location, "search" | "pathname" | "replace">): boolean;
|
|
@@ -69,3 +69,17 @@ export function followNextAfterLogin(location = window.location) {
|
|
|
69
69
|
location.replace(next);
|
|
70
70
|
return true;
|
|
71
71
|
}
|
|
72
|
+
// An explicit `next` (the page the user was sent away from) beats the server's
|
|
73
|
+
// landingPath (auth.postAuthLanding); without either the caller keeps its own
|
|
74
|
+
// client-side routing. landingPath is re-validated like `next`, so a
|
|
75
|
+
// misconfigured resolver cannot turn the login into an open redirect.
|
|
76
|
+
export function followNextOrLandingAfterLogin(landingPath, location = window.location) {
|
|
77
|
+
if (followNextAfterLogin(location))
|
|
78
|
+
return true;
|
|
79
|
+
if (landingPath === undefined || !isSafeNextPath(landingPath))
|
|
80
|
+
return false;
|
|
81
|
+
if (new URL(landingPath, "https://next.invalid").pathname === location.pathname)
|
|
82
|
+
return false;
|
|
83
|
+
location.replace(landingPath);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
@@ -13,7 +13,7 @@ import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
|
13
13
|
import { useState } from "react";
|
|
14
14
|
import { requestEmailVerification, } from "./auth-client.js";
|
|
15
15
|
import { AuthCard } from "./auth-form-primitives.js";
|
|
16
|
-
import {
|
|
16
|
+
import { followNextOrLandingAfterLogin } from "./auth-redirect.js";
|
|
17
17
|
import { useSession } from "./session.js";
|
|
18
18
|
// Map vom Reason-Code des Login-Handlers auf einen i18n-Key plus
|
|
19
19
|
// optional extrahierte Interpolations-Parameter. Ungekannte Codes
|
|
@@ -66,7 +66,7 @@ export function LoginScreen({ title, subtitle, submitLabel, forgotPasswordHref,
|
|
|
66
66
|
const res = await session.login({ email, password });
|
|
67
67
|
setSubmitting(false);
|
|
68
68
|
if (res.kind === "success") {
|
|
69
|
-
|
|
69
|
+
followNextOrLandingAfterLogin(res.data.landingPath);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
if (res.kind === "mfa-challenge") {
|
|
@@ -34,7 +34,7 @@ export function UserMenu({ children, variant = "pill" }) {
|
|
|
34
34
|
const hasName = user.displayName.length > 0;
|
|
35
35
|
const displayName = hasName ? user.displayName : user.email;
|
|
36
36
|
const avatarText = initials(displayName);
|
|
37
|
-
const content = (_jsxs(DropdownMenuContent, { align: variant === "sidebar" ? "start" : "end", "aria-label": t("auth.user.menu.label"), children: [_jsxs(DropdownMenuLabel, { className: "text-xs", children: [_jsx("div", { className: "font-medium text-foreground truncate", children: displayName }), hasName && _jsx("div", { className: "truncate", children: user.email })] }), _jsx(DropdownMenuSeparator, {}), children !== undefined && (_jsxs(_Fragment, { children: [children, _jsx(DropdownMenuSeparator, {})] })), _jsxs(DropdownMenuItem, { onSelect: () => void logout(), children: [_jsx(LogOut, { className: "h-4 w-4" }), _jsx("span", { children: t("auth.user.menu.logout") })] })] }));
|
|
37
|
+
const content = (_jsxs(DropdownMenuContent, { align: variant === "sidebar" ? "start" : "end", "aria-label": t("auth.user.menu.label"), children: [_jsxs(DropdownMenuLabel, { className: "text-xs", children: [_jsx("div", { className: "font-medium text-foreground truncate", children: displayName }), hasName && _jsx("div", { className: "truncate", children: user.email })] }), _jsx(DropdownMenuSeparator, {}), children !== undefined && (_jsxs(_Fragment, { children: [children, _jsx(DropdownMenuSeparator, {})] })), _jsxs(DropdownMenuItem, { "data-testid": "user-menu-logout", onSelect: () => void logout(), children: [_jsx(LogOut, { className: "h-4 w-4" }), _jsx("span", { children: t("auth.user.menu.logout") })] })] }));
|
|
38
38
|
// Sidebar-Footer: volle NavUser-Row (sidebar-07) als Dropdown-Trigger —
|
|
39
39
|
// gleiche Optik wie SidebarUser, aber klickbar mit Logout/Profil.
|
|
40
40
|
if (variant === "sidebar") {
|
package/dist/auth-mfa/feature.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { secretsEnvSchema } from "../secrets/index.js";
|
|
2
3
|
import { mfaRequiredConfigKey } from "./config.js";
|
|
3
4
|
import { createDisableHandler } from "./handlers/disable.write.js";
|
|
4
5
|
import { createEnableConfirmHandler } from "./handlers/enable-confirm.write.js";
|
|
@@ -81,6 +82,10 @@ export function createAuthMfaFeature(opts) {
|
|
|
81
82
|
category: "identity",
|
|
82
83
|
recommended: true,
|
|
83
84
|
});
|
|
85
|
+
// Same field instances as `secrets`, so composeEnvSchema treats them as one
|
|
86
|
+
// shared variable; an app mounting auth-mfa without `secrets` still gets the
|
|
87
|
+
// KEK slot (and its _CIPHERTEXT twin) declared.
|
|
88
|
+
r.envSchema(secretsEnvSchema);
|
|
84
89
|
r.requires("user");
|
|
85
90
|
r.requires("config");
|
|
86
91
|
// verify.write.ts dispatches "tenant:query:memberships" — without this
|
|
@@ -36,6 +36,7 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
36
36
|
access: { roles: ["anonymous"], personalData: "public-intake" },
|
|
37
37
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
38
38
|
escapeHatch: {
|
|
39
|
+
grants: ["systemIdentity", "unsafeRaw"],
|
|
39
40
|
reason: "Pre-auth MFA enrollment step has no session yet — re-checks status via ctx.queryAs(SYSTEM, " +
|
|
40
41
|
"user:findForAuth) and membership via ctx.resolveActiveMembership for the setup token's user. " +
|
|
41
42
|
"Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
|
|
@@ -28,6 +28,7 @@ export function createMfaVerifyHandler(opts) {
|
|
|
28
28
|
access: { roles: ["anonymous"], personalData: "public-intake" },
|
|
29
29
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
30
30
|
escapeHatch: {
|
|
31
|
+
grants: ["systemIdentity", "unsafeRaw"],
|
|
31
32
|
reason: "Pre-auth MFA step has no session yet — re-derives it via ctx.queryAs(SYSTEM, " +
|
|
32
33
|
"user:findForAuth) and ctx.resolveActiveMembership for the user the challenge token names. " +
|
|
33
34
|
"Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
|
|
@@ -3,6 +3,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
3
3
|
readonly type: "text";
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
|
+
readonly minLength?: number;
|
|
6
7
|
readonly required?: boolean;
|
|
7
8
|
readonly searchable?: boolean;
|
|
8
9
|
readonly sortable?: boolean;
|
|
@@ -24,6 +25,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
24
25
|
readonly type: "text";
|
|
25
26
|
readonly description?: string;
|
|
26
27
|
readonly maxLength?: number;
|
|
28
|
+
readonly minLength?: number;
|
|
27
29
|
readonly required?: boolean;
|
|
28
30
|
readonly searchable?: boolean;
|
|
29
31
|
readonly sortable?: boolean;
|
|
@@ -45,6 +47,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
45
47
|
readonly type: "text";
|
|
46
48
|
readonly description?: string;
|
|
47
49
|
readonly maxLength?: number;
|
|
50
|
+
readonly minLength?: number;
|
|
48
51
|
readonly required?: boolean;
|
|
49
52
|
readonly searchable?: boolean;
|
|
50
53
|
readonly sortable?: boolean;
|
|
@@ -98,6 +101,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
98
101
|
readonly type: "text";
|
|
99
102
|
readonly description?: string;
|
|
100
103
|
readonly maxLength?: number;
|
|
104
|
+
readonly minLength?: number;
|
|
101
105
|
readonly required?: boolean;
|
|
102
106
|
readonly searchable?: boolean;
|
|
103
107
|
readonly sortable?: boolean;
|
|
@@ -119,6 +123,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
119
123
|
readonly type: "text";
|
|
120
124
|
readonly description?: string;
|
|
121
125
|
readonly maxLength?: number;
|
|
126
|
+
readonly minLength?: number;
|
|
122
127
|
readonly required?: boolean;
|
|
123
128
|
readonly searchable?: boolean;
|
|
124
129
|
readonly sortable?: boolean;
|
|
@@ -140,6 +145,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
140
145
|
readonly type: "text";
|
|
141
146
|
readonly description?: string;
|
|
142
147
|
readonly maxLength?: number;
|
|
148
|
+
readonly minLength?: number;
|
|
143
149
|
readonly required?: boolean;
|
|
144
150
|
readonly searchable?: boolean;
|
|
145
151
|
readonly sortable?: boolean;
|
package/dist/auth-mfa/screens.js
CHANGED
|
@@ -47,7 +47,7 @@ export const mfaEnableScreen = {
|
|
|
47
47
|
confirm: {
|
|
48
48
|
handler: AuthMfaHandlers.enableConfirm,
|
|
49
49
|
fields: {
|
|
50
|
-
code: { type: "text", required: true, maxLength: 6 },
|
|
50
|
+
code: { type: "text", required: true, minLength: 6, maxLength: 6 },
|
|
51
51
|
},
|
|
52
52
|
layout: { sections: [{ fields: ["code"] }] },
|
|
53
53
|
carry: ["setupToken"],
|
|
@@ -57,7 +57,7 @@ export const mfaEnableScreen = {
|
|
|
57
57
|
};
|
|
58
58
|
// Either a 6-digit TOTP code or a 9-char recovery code proves possession —
|
|
59
59
|
// same bounds as the disable/regenerate-recovery handler schemas.
|
|
60
|
-
const possessionCodeField = { type: "text", required: true, maxLength: 9 };
|
|
60
|
+
const possessionCodeField = { type: "text", required: true, minLength: 6, maxLength: 9 };
|
|
61
61
|
export const mfaDisableScreen = {
|
|
62
62
|
id: MFA_DISABLE_SCREEN_ID,
|
|
63
63
|
type: "actionForm",
|
|
@@ -11,5 +11,5 @@ export function MfaRecoveryCodesReveal({ codes, onDismiss, }) {
|
|
|
11
11
|
const t = useTranslation();
|
|
12
12
|
const { Button, Field, Input, Section } = usePrimitives();
|
|
13
13
|
const [acknowledged, setAcknowledged] = useState(false);
|
|
14
|
-
return (_jsxs(Section, { testId: "mfa-regenerate-reveal",
|
|
14
|
+
return (_jsxs(Section, { testId: "mfa-regenerate-reveal", children: [_jsxs("div", { className: "flex flex-col items-center gap-2 text-center", children: [_jsx("span", { className: "text-sm font-semibold", children: t("auth.mfa.regenerate.newCodesTitle") }), _jsx("span", { className: "text-xs text-muted-foreground", children: t("auth.mfa.regenerate.newCodesHint") }), _jsx("code", { className: "inline-block whitespace-pre-wrap break-all rounded bg-muted px-3 py-2 font-mono text-sm", children: codes.join("\n") })] }), _jsx(Field, { id: "mfa-regenerate-ack", label: t("auth.mfa.regenerate.acknowledge"), children: _jsx(Input, { kind: "boolean", id: "mfa-regenerate-ack", name: "mfa-regenerate-ack", value: acknowledged, onChange: setAcknowledged }) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { variant: "primary", onClick: onDismiss, disabled: !acknowledged, children: t("auth.mfa.regenerate.done") }) })] }));
|
|
15
15
|
}
|
|
@@ -8,7 +8,7 @@ export type MfaSetupPreauthScreenProps = {
|
|
|
8
8
|
readonly subtitle?: ReactNode;
|
|
9
9
|
/** Fired once confirm succeeds — the caller owns session refresh, see the
|
|
10
10
|
* file-level comment above. */
|
|
11
|
-
readonly onSuccess?: () => void;
|
|
11
|
+
readonly onSuccess?: (landingPath?: string) => void;
|
|
12
12
|
readonly onCancel?: () => void;
|
|
13
13
|
};
|
|
14
14
|
export declare function MfaSetupPreauthScreen({ preauthSetupToken, accountLabel, title, subtitle, onSuccess, onCancel, }: MfaSetupPreauthScreenProps): ReactNode;
|