@cosmicdrift/kumiko-bundled-features 0.389.0 → 0.390.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/audit/feature.js +1 -1
- package/dist/auth-email-password/auth-user-row.js +1 -1
- package/dist/auth-email-password/bootstrap.js +6 -1
- package/dist/auth-email-password/feature.js +1 -1
- package/dist/auth-email-password/handlers/change-password.write.js +7 -7
- package/dist/auth-email-password/handlers/confirm-account-unlock.write.js +3 -3
- package/dist/auth-email-password/handlers/confirm-token-flow.js +4 -4
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +8 -7
- package/dist/auth-email-password/handlers/invite-accept.write.js +10 -7
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +2 -2
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +8 -7
- package/dist/auth-email-password/handlers/login.write.js +17 -15
- package/dist/auth-email-password/handlers/reset-password.write.js +2 -2
- package/dist/auth-email-password/handlers/signup-confirm.write.js +12 -12
- package/dist/auth-email-password/handlers/signup-request.write.js +12 -8
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +4 -2
- package/dist/auth-email-password/handlers/token-request-handler.js +2 -2
- package/dist/auth-email-password/handlers/verify-email.write.js +2 -2
- package/dist/auth-email-password/seeding.d.ts +1 -1
- package/dist/auth-email-password/seeding.js +9 -8
- package/dist/auth-foundation/feature.js +12 -4
- package/dist/auth-mfa/handlers/disable.write.js +5 -5
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +19 -14
- package/dist/auth-mfa/handlers/enable-confirm.write.js +3 -3
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +6 -1
- package/dist/auth-mfa/handlers/enable-start.write.js +1 -1
- package/dist/auth-mfa/handlers/reencrypt.job.js +14 -9
- package/dist/auth-mfa/handlers/regenerate-recovery.write.js +4 -4
- package/dist/auth-mfa/handlers/verify.write.js +19 -14
- package/dist/auth-mfa/mfa-code-verifier.js +9 -4
- package/dist/auth-mfa/mfa-status-checker.js +8 -3
- package/dist/auth-mfa/verify-factor.js +1 -1
- package/dist/billing-foundation/billing-info-query.js +2 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -1
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +5 -5
- package/dist/billing-foundation/consumer-protection/register-consent.js +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +1 -1
- package/dist/billing-foundation/consumer-protection/termination-open-cancels.d.ts +4 -0
- package/dist/billing-foundation/consumer-protection/termination-open-cancels.js +14 -0
- package/dist/billing-foundation/consumer-protection/termination-provider-lock.d.ts +2 -0
- package/dist/billing-foundation/consumer-protection/termination-provider-lock.js +3 -0
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +4 -4
- package/dist/billing-foundation/consumer-protection/termination-record.js +11 -48
- package/dist/billing-foundation/consumer-protection/termination-request.js +10 -8
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +10 -12
- package/dist/billing-foundation/events.js +1 -1
- package/dist/billing-foundation/feature.js +3 -3
- package/dist/billing-foundation/get-subscription-for-tenant.d.ts +1 -1
- package/dist/billing-foundation/handlers/process-event.write.js +5 -5
- package/dist/billing-foundation/handlers/process-payment-event.write.js +3 -3
- package/dist/billing-foundation/plan-catalog.js +2 -2
- package/dist/billing-foundation/projection.js +1 -1
- package/dist/billing-foundation/types.js +1 -1
- package/dist/cap-counter/aggregate-id.d.ts +1 -1
- package/dist/cap-counter/aggregate-id.js +1 -1
- package/dist/cap-counter/book-cap-usage.d.ts +2 -2
- package/dist/cap-counter/book-cap-usage.js +3 -3
- package/dist/cap-counter/cap-reservation.js +1 -1
- package/dist/cap-counter/constants.js +1 -1
- package/dist/cap-counter/feature.js +2 -2
- package/dist/cap-counter/handlers/increment-rolling.write.js +1 -1
- package/dist/cap-counter/reservation-chain.js +1 -1
- package/dist/cap-counter/with-cap-enforcement.d.ts +3 -3
- package/dist/cap-counter/with-cap-enforcement.js +1 -1
- package/dist/cap-overview/handlers/caps-usage.query.js +1 -1
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +1 -1
- package/dist/compliance-profiles/seeding.d.ts +1 -1
- package/dist/compliance-profiles/seeding.js +9 -3
- package/dist/config/feature.js +6 -6
- package/dist/config/handlers/cascade.query.js +1 -1
- package/dist/config/handlers/readiness.query.js +1 -1
- package/dist/config/handlers/reencrypt.job.js +23 -13
- package/dist/config/handlers/reset.write.js +3 -3
- package/dist/config/handlers/selected-extensions.query.js +1 -1
- package/dist/config/handlers/set.write.js +4 -4
- package/dist/config/handlers/values.query.js +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +8 -8
- package/dist/crypto-shredding/resolve-file-bytes-deleter.js +3 -3
- package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
- package/dist/custom-fields/handlers/delete-system-field.write.js +1 -1
- package/dist/custom-fields/handlers/delete-tenant-field.write.js +1 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
- package/dist/custom-fields/index.d.ts +1 -0
- package/dist/custom-fields/index.js +1 -0
- package/dist/data-retention/feature.js +1 -1
- package/dist/data-retention/run-retention-cleanup.js +12 -2
- package/dist/delivery/attempt-log.js +1 -1
- package/dist/delivery/delivery-service.js +5 -5
- package/dist/delivery/feature.js +2 -2
- package/dist/delivery/jobs.js +11 -11
- package/dist/document-ingest-foundation/events.js +1 -1
- package/dist/feature-toggles/handlers/registered.query.js +1 -1
- package/dist/feature-toggles/handlers/set.write.js +1 -1
- package/dist/feature-toggles/toggle-sync-signal.js +1 -1
- package/dist/file-derivatives/feature.d.ts +1 -1
- package/dist/file-derivatives/feature.js +1 -1
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/file-derivatives/handlers/public-variant.query.js +5 -5
- package/dist/file-foundation/feature.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +4 -2
- package/dist/file-foundation/read-file-ref-content.js +3 -3
- package/dist/file-provider-s3/feature.js +1 -1
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +8 -8
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/cleanup.job.js +22 -12
- package/dist/form-draft/handlers/discard.write.js +3 -3
- package/dist/form-draft/handlers/get.query.js +1 -1
- package/dist/foundation-shared/config-helpers.d.ts +1 -1
- package/dist/foundation-shared/config-helpers.js +1 -1
- package/dist/inbound-mail-foundation/events.js +1 -1
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/account-state.d.ts +3 -1
- package/dist/inbound-mail-foundation/handlers/account-state.js +1 -1
- package/dist/inbound-mail-foundation/handlers/connect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/disconnect-account.write.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +10 -10
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +1 -1
- package/dist/inbound-mail-foundation/projection.js +1 -1
- package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
- package/dist/inbound-provider-imap/feature.js +1 -1
- package/dist/jobs/events.js +1 -1
- package/dist/jobs/handlers/consumer.write.js +1 -1
- package/dist/jobs/handlers/projection-rebuild.job.js +1 -1
- package/dist/jobs/handlers/reindex-entity.job.js +5 -5
- package/dist/jobs/handlers/retention-cleanup.job.js +1 -1
- package/dist/jobs/handlers/retry.write.js +1 -1
- package/dist/jobs/handlers/stale-run-sweep.job.js +1 -1
- package/dist/jobs/handlers/trigger.write.js +2 -2
- package/dist/jobs/job-run-logger.d.ts +1 -0
- package/dist/jobs/job-run-logger.js +38 -18
- package/dist/legal-pages/lib/boot-check.d.ts +10 -4
- package/dist/legal-pages/lib/boot-check.js +2 -2
- package/dist/mail-foundation/feature.d.ts +2 -2
- package/dist/mail-transport-smtp/feature.js +1 -1
- package/dist/managed-pages/seeding.d.ts +1 -1
- package/dist/managed-pages/seeding.js +10 -4
- package/dist/notes-history/handlers/add-note.write.js +1 -1
- package/dist/personal-access-tokens/feature.js +1 -1
- package/dist/personal-access-tokens/handlers/create.write.js +2 -2
- package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
- package/dist/rate-limiting/handlers/status.query.js +2 -2
- package/dist/readiness/handlers/status.query.js +1 -1
- package/dist/renderer-foundation/api.d.ts +4 -2
- package/dist/renderer-foundation/api.js +2 -2
- package/dist/secrets/announce-secret-change.d.ts +6 -2
- package/dist/secrets/announce-secret-change.js +3 -3
- package/dist/secrets/feature.d.ts +1 -1
- package/dist/secrets/feature.js +8 -8
- package/dist/secrets/handlers/delete.write.js +1 -1
- package/dist/secrets/handlers/rotate.job.js +13 -8
- package/dist/secrets/handlers/set.write.js +1 -1
- package/dist/secrets/secrets-context.js +16 -6
- package/dist/seo/feature.js +1 -1
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/cleanup.job.js +2 -2
- package/dist/sessions/handlers/detail.query.d.ts +1 -0
- package/dist/sessions/handlers/detail.query.js +5 -2
- package/dist/sessions/handlers/list.query.d.ts +1 -0
- package/dist/sessions/handlers/list.query.js +5 -2
- package/dist/sessions/handlers/mine.query.d.ts +1 -0
- package/dist/sessions/handlers/mine.query.js +5 -2
- package/dist/sessions/handlers/revoke-all-for-user.write.js +3 -3
- package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
- package/dist/sessions/handlers/revoke.write.js +1 -1
- package/dist/sessions/screens.js +8 -4
- package/dist/sessions/session-revoked-event.js +1 -1
- package/dist/sessions/web/session-device-cell.js +10 -12
- package/dist/sessions/web/user-agent.d.ts +1 -0
- package/dist/sessions/web/user-agent.js +11 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/purge-files-reaching-subject.js +12 -2
- package/dist/shared/require-for-tenant.d.ts +1 -1
- package/dist/shared/require-for-tenant.js +6 -4
- package/dist/shared/stream-tenant-backfill.d.ts +28 -0
- package/dist/shared/stream-tenant-backfill.js +119 -0
- package/dist/shared/stream-tenant-derived-state.d.ts +25 -0
- package/dist/shared/stream-tenant-derived-state.js +68 -0
- package/dist/step-dispatcher/feature.js +1 -1
- package/dist/subscription-dodo/plugin-methods.js +1 -1
- package/dist/subscription-dodo/runtime.js +4 -4
- package/dist/subscription-stripe/plugin-methods.js +3 -3
- package/dist/subscription-stripe/runtime.js +4 -4
- package/dist/template-resolver/api.d.ts +4 -2
- package/dist/template-resolver/api.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.d.ts +4 -3
- package/dist/template-resolver/handlers/export-bundle.query.js +1 -1
- package/dist/template-resolver/handlers/overview.query.js +2 -2
- package/dist/template-resolver/handlers/send-test-shared.js +1 -1
- package/dist/template-resolver/handlers/send-test.write.js +2 -2
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +3 -1
- package/dist/template-resolver/handlers/tenant-override-selection.js +1 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +1 -1
- package/dist/template-resolver/handlers/usage.query.js +1 -1
- package/dist/template-resolver/seeding.js +12 -2
- package/dist/template-resolver/store-template-resource.d.ts +7 -6
- package/dist/template-resolver/store-template-resource.js +3 -3
- package/dist/template-resolver/template-resources.d.ts +2 -2
- package/dist/template-resolver/template-resources.js +2 -2
- package/dist/template-resolver/testing.js +2 -2
- package/dist/template-resolver/web/document-insert-fields.d.ts +4 -0
- package/dist/template-resolver/web/document-insert-fields.js +69 -0
- package/dist/template-resolver/web/document-insert-model.d.ts +43 -0
- package/dist/template-resolver/web/document-insert-model.js +106 -0
- package/dist/template-resolver/web/document-insert-panel.d.ts +13 -0
- package/dist/template-resolver/web/document-insert-panel.js +49 -0
- package/dist/template-resolver/web/document-tiptap-bubble.d.ts +4 -0
- package/dist/template-resolver/web/document-tiptap-bubble.js +85 -0
- package/dist/template-resolver/web/document-tiptap-content.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-content.js +12 -0
- package/dist/template-resolver/web/document-tiptap-context.d.ts +41 -0
- package/dist/template-resolver/web/document-tiptap-context.js +43 -0
- package/dist/template-resolver/web/document-tiptap-editor.js +50 -495
- package/dist/template-resolver/web/document-tiptap-helpers.d.ts +44 -0
- package/dist/template-resolver/web/document-tiptap-helpers.js +129 -0
- package/dist/template-resolver/web/document-tiptap-layout.d.ts +4 -0
- package/dist/template-resolver/web/document-tiptap-layout.js +24 -0
- package/dist/template-resolver/web/document-tiptap-toolbar-groups.d.ts +6 -0
- package/dist/template-resolver/web/document-tiptap-toolbar-groups.js +33 -0
- package/dist/template-resolver/web/document-tiptap-toolbar.d.ts +3 -0
- package/dist/template-resolver/web/document-tiptap-toolbar.js +91 -0
- package/dist/template-resolver/web/editor-handle.d.ts +1 -0
- package/dist/template-resolver/web/editor-handle.js +15 -0
- package/dist/template-resolver/web/placeholder-line-editor.js +16 -5
- package/dist/template-resolver/web/use-document-tiptap-model.d.ts +54 -0
- package/dist/template-resolver/web/use-document-tiptap-model.js +96 -0
- package/dist/tenant/handlers/cancel-invitation.write.js +1 -1
- package/dist/tenant/handlers/memberships.query.js +1 -1
- package/dist/tenant/handlers/remove-member.write.js +2 -2
- package/dist/tenant/handlers/update-member-roles.write.js +1 -1
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invited-membership.js +6 -1
- package/dist/tenant/schema/tenant.d.ts +2 -2
- package/dist/tenant/schema/tenant.js +4 -1
- package/dist/tenant/seeding.d.ts +2 -2
- package/dist/tenant/seeding.js +52 -52
- package/dist/tenant/stream-tenant-backfill.d.ts +21 -0
- package/dist/tenant/stream-tenant-backfill.js +63 -0
- package/dist/tenant/testing.d.ts +12 -0
- package/dist/tenant/testing.js +33 -0
- package/dist/tenant-handover/events.js +1 -1
- package/dist/tenant-handover/handlers/claim.write.js +1 -1
- package/dist/tenant-lifecycle/feature.js +5 -5
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +14 -5
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +12 -4
- package/dist/tenant-lifecycle/run-tenant-destroy.js +23 -13
- package/dist/tenant-lifecycle/stages.js +6 -1
- package/dist/tenant-lifecycle/testing.d.ts +1 -1
- package/dist/tenant-lifecycle/testing.js +6 -5
- package/dist/tier-engine/entity.js +1 -1
- package/dist/tier-engine/feature.js +8 -3
- package/dist/tier-engine/handlers/active-tier.query.js +3 -3
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +6 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -4
- package/dist/user/db/queries/stream-tenant-backfill.d.ts +20 -1
- package/dist/user/db/queries/stream-tenant-backfill.js +40 -77
- package/dist/user/handlers/find-for-auth.query.js +2 -2
- package/dist/user/handlers/update-roles.js +2 -2
- package/dist/user/index.d.ts +1 -1
- package/dist/user/index.js +1 -1
- package/dist/user/invited-global-roles.js +6 -1
- package/dist/user/principal-status.js +1 -1
- package/dist/user/seeding.d.ts +1 -1
- package/dist/user/seeding.js +20 -5
- package/dist/user/testing.d.ts +3 -0
- package/dist/user/testing.js +10 -0
- package/dist/user-data-rights/audit-download.js +12 -2
- package/dist/user-data-rights/feature.js +29 -26
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/deletion-grace-period.js +1 -1
- package/dist/user-data-rights/handlers/download-by-job.query.js +2 -2
- package/dist/user-data-rights/handlers/download-by-token.query.js +2 -2
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +4 -4
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +8 -6
- package/dist/user-data-rights/index.d.ts +1 -1
- package/dist/user-data-rights/index.js +1 -1
- package/dist/user-data-rights/lib/mail-transport-resolver.js +1 -1
- package/dist/user-data-rights/lib/storage-provider-resolver.js +1 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -1
- package/dist/user-data-rights/run-export-jobs.js +7 -2
- package/dist/user-data-rights/run-forget-cleanup.d.ts +1 -0
- package/dist/user-data-rights/run-forget-cleanup.js +9 -1
- package/dist/user-data-rights/run-user-export.js +7 -1
- package/dist/user-data-rights/web/constants.d.ts +8 -0
- package/dist/user-data-rights/web/constants.js +8 -0
- package/dist/user-data-rights/web/index.d.ts +1 -1
- package/dist/user-data-rights/web/index.js +1 -1
- package/dist/user-profile/handlers/change-email.write.js +8 -6
- package/dist/waitlist/handlers/export-purge.write.js +1 -1
- package/dist/waitlist/handlers/invite.write.js +2 -2
- package/dist/waitlist/handlers/reject.write.js +1 -1
- package/dist/waitlist/handlers/submit.write.js +9 -4
- package/dist/waitlist/legacy-migrate.js +6 -1
- package/dist/waitlist/link-on-signup.js +6 -1
- package/dist/workflow-runner/db/queries/due-runs.js +1 -1
- package/dist/workflow-runner/event-subscriber.js +4 -3
- package/dist/workflow-runner/event-trigger.js +2 -1
- package/dist/workflow-runner/feature.js +2 -2
- package/dist/workflow-runner/handlers/resume-run.write.js +6 -6
- package/dist/workflow-runner/runner.d.ts +3 -1
- package/dist/workflow-runner/runner.js +3 -3
- package/package.json +41 -23
- package/src/auth-email-password/changes.json +6 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/custom-fields/changes.json +6 -0
- package/src/jobs/changes.json +6 -0
- package/src/mail-transport-smtp/changes.json +6 -0
- package/src/sessions/changes.json +12 -0
- package/src/template-resolver/changes.json +18 -0
- package/src/tenant/changes.json +19 -0
- package/src/user-data-rights/changes.json +24 -0
- package/src/workflow-runner/changes.json +6 -0
|
@@ -23,7 +23,7 @@ export function createVerifyEmailHandler(opts) {
|
|
|
23
23
|
escapeHatch: {
|
|
24
24
|
grants: ["systemIdentity"],
|
|
25
25
|
reason: "Anonymous token holder has no session — runConfirmTokenFlow loads and updates the " +
|
|
26
|
-
"target user via ctx.writeAs(SYSTEM, user:update) to set emailVerified.",
|
|
26
|
+
"target user via ctx.dispatch.writeAs(SYSTEM, user:update) to set emailVerified.",
|
|
27
27
|
},
|
|
28
28
|
agent: { expose: false },
|
|
29
29
|
handler: async (event, ctx) => {
|
|
@@ -37,7 +37,7 @@ export function createVerifyEmailHandler(opts) {
|
|
|
37
37
|
return invalidVerificationToken();
|
|
38
38
|
return runConfirmTokenFlow(ctx, verify.userId, verify.expiresAtMs, {
|
|
39
39
|
purpose: "verify",
|
|
40
|
-
redisRequiredMessage: "email-verification requires ctx.redis to enforce token single-use",
|
|
40
|
+
redisRequiredMessage: "email-verification requires ctx.services.redis to enforce token single-use",
|
|
41
41
|
invalidToken: invalidVerificationToken,
|
|
42
42
|
buildChanges: async () => ({ emailVerified: true }),
|
|
43
43
|
successData: { kind: "verified" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import type
|
|
2
|
+
import { type SessionUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { type SeedTenantHooks } from "../tenant/seeding.ts";
|
|
4
4
|
export { seedTenant, seedTenantMembership } from "../tenant/seeding.ts";
|
|
5
5
|
export { seedUser } from "../user/seeding.ts";
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
// sub-paths themselves.
|
|
14
14
|
import { configuredBlindIndexKey, configuredPiiSubjectKms, decryptPiiFieldValues, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
15
15
|
import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
16
|
+
import { createSystemUser, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
17
|
import { ConflictError } from "@cosmicdrift/kumiko-framework/errors";
|
|
17
18
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
18
|
-
import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
|
|
19
19
|
import { hashPassword } from "../shared/index.js";
|
|
20
20
|
// kumiko-lint-ignore cross-feature-import auth-tests need user+tenant seed-helpers
|
|
21
21
|
import { seedTenant, seedTenantMembership } from "../tenant/seeding.js";
|
|
@@ -129,9 +129,9 @@ hooks) {
|
|
|
129
129
|
* persistent-DB mode succeeds. Returns the userId.
|
|
130
130
|
*/
|
|
131
131
|
export async function seedAdmin(db, options) {
|
|
132
|
-
const
|
|
132
|
+
const byOption = options.by === undefined ? {} : { by: options.by };
|
|
133
133
|
for (const m of options.memberships) {
|
|
134
|
-
await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName,
|
|
134
|
+
await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, ...byOption });
|
|
135
135
|
}
|
|
136
136
|
const { id: userId } = await seedUserWithPassword(db, {
|
|
137
137
|
email: options.email,
|
|
@@ -139,14 +139,14 @@ export async function seedAdmin(db, options) {
|
|
|
139
139
|
displayName: options.displayName,
|
|
140
140
|
...(options.globalRoles !== undefined && { roles: options.globalRoles }),
|
|
141
141
|
...(options.emailVerified !== undefined && { emailVerified: options.emailVerified }),
|
|
142
|
-
|
|
142
|
+
...byOption,
|
|
143
143
|
});
|
|
144
144
|
for (const m of options.memberships) {
|
|
145
145
|
await seedTenantMembership(db, {
|
|
146
146
|
userId,
|
|
147
147
|
tenantId: m.tenantId,
|
|
148
148
|
roles: m.roles,
|
|
149
|
-
|
|
149
|
+
...byOption,
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
return { id: userId };
|
|
@@ -239,7 +239,8 @@ export async function seedAdminGuarded(db, options) {
|
|
|
239
239
|
const existingId = await findExistingUserIdForEmail(db, kms, options.email);
|
|
240
240
|
if (existingId === undefined)
|
|
241
241
|
return seedAdmin(db, options);
|
|
242
|
-
const
|
|
242
|
+
const byOption = options.by === undefined ? {} : { by: options.by };
|
|
243
|
+
const by = options.by ?? createSystemUser(SYSTEM_TENANT_ID);
|
|
243
244
|
const existingRow = await fetchOne(db, userTable, { id: existingId });
|
|
244
245
|
if (existingRow) {
|
|
245
246
|
// Re-write the email so email_bidx is recomputed; without it the account
|
|
@@ -251,12 +252,12 @@ export async function seedAdminGuarded(db, options) {
|
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
for (const m of options.memberships) {
|
|
254
|
-
await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName,
|
|
255
|
+
await seedTenant(db, { id: m.tenantId, key: m.tenantKey, name: m.tenantName, ...byOption });
|
|
255
256
|
await seedTenantMembership(db, {
|
|
256
257
|
userId: existingId,
|
|
257
258
|
tenantId: m.tenantId,
|
|
258
259
|
roles: m.roles,
|
|
259
|
-
|
|
260
|
+
...byOption,
|
|
260
261
|
});
|
|
261
262
|
}
|
|
262
263
|
return { id: existingId };
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Middleware / runProdApp resolve providers from the registry at boot
|
|
11
11
|
// (tokenVerifier/sessionStore) or request-time (tokenVerifier shape match).
|
|
12
12
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
|
+
import { systemScopedRunner } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
|
|
13
14
|
import { validateSessionStoreMultiplicity, validateTenantExistenceMultiplicity, validateTenantResolverMultiplicity, validateTokenVerifierMultiplicity, } from "./boot-checks.js";
|
|
14
15
|
import { EXT_SESSION_STORE, EXT_TENANT_EXISTENCE, EXT_TENANT_RESOLVER, EXT_TOKEN_VERIFIER, isAuthProviderPlugin, isSessionStoreProvider, isTenantExistenceProvider, isTenantResolverProvider, tokenShapeMatches, } from "./types.js";
|
|
15
16
|
export { EXT_SESSION_STORE, EXT_TENANT_EXISTENCE, EXT_TENANT_RESOLVER, EXT_TOKEN_VERIFIER, } from "./types.js";
|
|
@@ -49,6 +50,13 @@ export function createAuthFoundationFeature() {
|
|
|
49
50
|
r.bootCheck(({ features }) => validateTenantExistenceMultiplicity(features));
|
|
50
51
|
}, { dedupeOptions: {} });
|
|
51
52
|
}
|
|
53
|
+
// Providers run before any tenant or session exists (token verify, host to tenant), so their reads are cross-tenant by nature.
|
|
54
|
+
function systemScopedDeps(deps) {
|
|
55
|
+
return {
|
|
56
|
+
...deps,
|
|
57
|
+
db: systemScopedRunner(deps.db, "auth lookups run before a tenant is established"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
52
60
|
/**
|
|
53
61
|
* Tries every registered tokenVerifier provider's shape against `rawToken`
|
|
54
62
|
* in registration order and calls the first match's verifier. Returns null
|
|
@@ -62,7 +70,7 @@ export async function resolveTokenVerifier(deps, rawToken) {
|
|
|
62
70
|
continue;
|
|
63
71
|
if (!tokenShapeMatches(usage.options.shape, rawToken))
|
|
64
72
|
continue;
|
|
65
|
-
const verify = await usage.options.build(deps);
|
|
73
|
+
const verify = await usage.options.build(systemScopedDeps(deps));
|
|
66
74
|
return verify(rawToken);
|
|
67
75
|
}
|
|
68
76
|
return null;
|
|
@@ -78,7 +86,7 @@ export async function resolveSessionStore(deps) {
|
|
|
78
86
|
if (!usage || !isSessionStoreProvider(usage.options)) {
|
|
79
87
|
throw new Error("[auth-foundation] no sessionStore provider registered — did the boot validator run?");
|
|
80
88
|
}
|
|
81
|
-
return usage.options.build(deps);
|
|
89
|
+
return usage.options.build(systemScopedDeps(deps));
|
|
82
90
|
}
|
|
83
91
|
/** Optional tenantResolver — null when no provider registered. */
|
|
84
92
|
export async function resolveTenantResolver(deps) {
|
|
@@ -88,7 +96,7 @@ export async function resolveTenantResolver(deps) {
|
|
|
88
96
|
if (!isTenantResolverProvider(usage.options)) {
|
|
89
97
|
throw new Error("[auth-foundation] tenantResolver provider registered without a valid TenantResolverProvider");
|
|
90
98
|
}
|
|
91
|
-
const resolve = await usage.options.build(deps);
|
|
99
|
+
const resolve = await usage.options.build(systemScopedDeps(deps));
|
|
92
100
|
return { resolve, trust: usage.options.trust };
|
|
93
101
|
}
|
|
94
102
|
/** Optional tenantExistence — null when no provider registered. */
|
|
@@ -99,5 +107,5 @@ export async function resolveTenantExistence(deps) {
|
|
|
99
107
|
if (!isTenantExistenceProvider(usage.options)) {
|
|
100
108
|
throw new Error("[auth-foundation] tenantExistence provider registered without a valid TenantExistenceProvider");
|
|
101
109
|
}
|
|
102
|
-
return usage.options.build(deps);
|
|
110
|
+
return usage.options.build(systemScopedDeps(deps));
|
|
103
111
|
}
|
|
@@ -30,18 +30,18 @@ export function createDisableHandler(opts) {
|
|
|
30
30
|
if (!row)
|
|
31
31
|
return mfaNotEnabled();
|
|
32
32
|
// Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts
|
|
33
|
-
// without ctx.redis, verifyMfaFactor's TOTP-replay burn is skipped, and
|
|
33
|
+
// without ctx.services.redis, verifyMfaFactor's TOTP-replay burn is skipped, and
|
|
34
34
|
// a code observed once (phishing proxy/shoulder-surfing) could be
|
|
35
35
|
// replayed within the ±1-step window to disable MFA outright.
|
|
36
|
-
// "no ctx.redis means no one gets through MFA login" doesn't cover
|
|
36
|
+
// "no ctx.services.redis means no one gets through MFA login" doesn't cover
|
|
37
37
|
// this route — it's session-authed, and JWTs issued before Redis was
|
|
38
38
|
// unwired stay valid until they expire.
|
|
39
|
-
if (!ctx.redis) {
|
|
39
|
+
if (!ctx.services.redis) {
|
|
40
40
|
return writeFailure(new InternalError({
|
|
41
|
-
message: "disable (mfa) requires ctx.redis for TOTP replay protection",
|
|
41
|
+
message: "disable (mfa) requires ctx.services.redis for TOTP replay protection",
|
|
42
42
|
}));
|
|
43
43
|
}
|
|
44
|
-
const replay = { redis: ctx.redis, userId: event.user.id };
|
|
44
|
+
const replay = { redis: ctx.services.redis, userId: event.user.id };
|
|
45
45
|
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
46
46
|
if (!verify.ok)
|
|
47
47
|
return invalidTotpCode();
|
|
@@ -37,8 +37,8 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
37
37
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
38
38
|
escapeHatch: {
|
|
39
39
|
grants: ["systemIdentity", "unsafeRaw"],
|
|
40
|
-
reason: "Pre-auth MFA enrollment step has no session yet — re-checks status via ctx.queryAs(SYSTEM, " +
|
|
41
|
-
"user:findForAuth) and membership via ctx.resolveActiveMembership for the setup token's user. " +
|
|
40
|
+
reason: "Pre-auth MFA enrollment step has no session yet — re-checks status via ctx.dispatch.queryAs(SYSTEM, " +
|
|
41
|
+
"user:findForAuth) and membership via ctx.auth.resolveActiveMembership for the setup token's user. " +
|
|
42
42
|
"Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
|
|
43
43
|
"guest dispatch tenant.",
|
|
44
44
|
},
|
|
@@ -60,19 +60,19 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
60
60
|
// endpoint has NO gate before it (access: { roles: ["anonymous"] }, no JWT,
|
|
61
61
|
// no prior challenge-token from a successful login) — its only secret
|
|
62
62
|
// is a 6-digit TOTP code. Silently skipping the brute-force cap and
|
|
63
|
-
// single-use burn when ctx.redis is absent would let the setup token
|
|
63
|
+
// single-use burn when ctx.services.redis is absent would let the setup token
|
|
64
64
|
// be guessed against unboundedly until its TTL expires. The route's
|
|
65
65
|
// IP rate limiter is not a substitute: it's IP-scoped and resets on
|
|
66
66
|
// every success.
|
|
67
|
-
if (!ctx.redis) {
|
|
67
|
+
if (!ctx.services.redis) {
|
|
68
68
|
return writeFailure(new InternalError({
|
|
69
|
-
message: "enable-confirm-preauth requires ctx.redis for the brute-force cap and single-use burn",
|
|
69
|
+
message: "enable-confirm-preauth requires ctx.services.redis for the brute-force cap and single-use burn",
|
|
70
70
|
}));
|
|
71
71
|
}
|
|
72
72
|
// Same guessing surface as /auth/mfa/verify (a short TOTP code) and
|
|
73
73
|
// the same per-userId cap — deliberately shared counter, see
|
|
74
74
|
// mfa-verify-attempts.ts.
|
|
75
|
-
const state = await getMfaVerifyLockoutState(ctx.redis, userId);
|
|
75
|
+
const state = await getMfaVerifyLockoutState(ctx.services.redis, userId);
|
|
76
76
|
if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
|
|
77
77
|
const now = Date.now();
|
|
78
78
|
if (state.lockedUntil > now) {
|
|
@@ -82,17 +82,22 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
82
82
|
}
|
|
83
83
|
const secret = base32Decode(verify.payload.totpSecretBase32);
|
|
84
84
|
if (verifyTotp(secret, event.payload.code) === false) {
|
|
85
|
-
await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
|
|
85
|
+
await recordFailedMfaVerifyAttempt(ctx.services.redis, userId, maxAttempts, lockoutMinutes);
|
|
86
86
|
return invalidTotpCode();
|
|
87
87
|
}
|
|
88
88
|
// Burn the setup token on the first successful confirm — same
|
|
89
89
|
// single-use guarantee as enable-confirm.write.ts.
|
|
90
|
-
const burnResult = await burnToken(ctx.redis, "mfa-setup", userId, verify.expiresAtMs);
|
|
90
|
+
const burnResult = await burnToken(ctx.services.redis, "mfa-setup", userId, verify.expiresAtMs);
|
|
91
91
|
if (burnResult === "already-used")
|
|
92
92
|
return invalidSetupToken();
|
|
93
93
|
// "system" mode: no session exists yet, tenantId comes from the
|
|
94
94
|
// verified token, mirroring enable-start-preauth.write.ts.
|
|
95
|
-
const scopedDb = createTenantDb({
|
|
95
|
+
const scopedDb = createTenantDb({
|
|
96
|
+
db: ctx.db.unsafeRaw(),
|
|
97
|
+
tenantId,
|
|
98
|
+
mode: "system",
|
|
99
|
+
reason: "pre-auth MFA setup has no session, the tenant comes from the verified setup token",
|
|
100
|
+
});
|
|
96
101
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
97
102
|
const existing = await findUserMfaRow(scopedDb, scopedUser);
|
|
98
103
|
if (existing)
|
|
@@ -112,7 +117,7 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
112
117
|
// existing userId-unique-constraint backstop between the
|
|
113
118
|
// `existing` check above and executor.create() below.
|
|
114
119
|
const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
|
|
115
|
-
const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
120
|
+
const userRow = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
116
121
|
id: userId,
|
|
117
122
|
})); // @cast-boundary engine-payload
|
|
118
123
|
if (!userRow)
|
|
@@ -123,7 +128,7 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
123
128
|
return invalidSetupToken();
|
|
124
129
|
}
|
|
125
130
|
const globalRoles = parseRoles(userRow?.roles ?? null);
|
|
126
|
-
const active = await ctx.resolveActiveMembership(userId, tenantId);
|
|
131
|
+
const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
|
|
127
132
|
if (active.kind === "rejected")
|
|
128
133
|
return invalidSetupToken();
|
|
129
134
|
const result = await executor.create({
|
|
@@ -135,8 +140,8 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
135
140
|
}, scopedUser, scopedDb);
|
|
136
141
|
if (!result.isSuccess)
|
|
137
142
|
return result;
|
|
138
|
-
// ctx.redis is guaranteed by the fail-closed check above.
|
|
139
|
-
await clearMfaVerifyAttempts(ctx.redis, userId);
|
|
143
|
+
// ctx.services.redis is guaranteed by the fail-closed check above.
|
|
144
|
+
await clearMfaVerifyAttempts(ctx.services.redis, userId);
|
|
140
145
|
// Re-derive the full session the way verify.write.ts does — the
|
|
141
146
|
// setupToken only proves "password + new TOTP secret", not roles;
|
|
142
147
|
// roles come from the membership lookup above, same as every other
|
|
@@ -157,7 +162,7 @@ export function createEnableConfirmPreauthHandler(opts) {
|
|
|
157
162
|
...sessionTimezoneField(userRow?.timezone),
|
|
158
163
|
...sessionLocaleField(userRow?.locale),
|
|
159
164
|
};
|
|
160
|
-
const claims = await ctx.
|
|
165
|
+
const claims = await ctx.auth.resolveClaims(baseSession);
|
|
161
166
|
const session = Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
|
|
162
167
|
// Last step before returning success: every gate above (status,
|
|
163
168
|
// membership, entity-write) has passed, so this is the point of no
|
|
@@ -45,9 +45,9 @@ export function createEnableConfirmHandler(opts) {
|
|
|
45
45
|
// second seeing invalidSetupToken(). Checked before the TOTP verify
|
|
46
46
|
// for the same reason the other two handlers check it early: fail
|
|
47
47
|
// closed before doing any of the guarded work, not partway through.
|
|
48
|
-
if (!ctx.redis) {
|
|
48
|
+
if (!ctx.services.redis) {
|
|
49
49
|
return writeFailure(new InternalError({
|
|
50
|
-
message: "enable-confirm requires ctx.redis for the single-use setup-token burn",
|
|
50
|
+
message: "enable-confirm requires ctx.services.redis for the single-use setup-token burn",
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
53
|
const secret = base32Decode(verify.payload.totpSecretBase32);
|
|
@@ -65,7 +65,7 @@ export function createEnableConfirmHandler(opts) {
|
|
|
65
65
|
// concurrent mfa_already_enabled() write) is meant to leave the burn
|
|
66
66
|
// standing. Add unburnToken here if create-failure retries turn out
|
|
67
67
|
// to matter.
|
|
68
|
-
const burnResult = await burnToken(ctx.redis, "mfa-setup", event.user.id, verify.expiresAtMs);
|
|
68
|
+
const burnResult = await burnToken(ctx.services.redis, "mfa-setup", event.user.id, verify.expiresAtMs);
|
|
69
69
|
if (burnResult === "already-used")
|
|
70
70
|
return invalidSetupToken();
|
|
71
71
|
const existing = await findUserMfaRow(ctx.db, event.user);
|
|
@@ -44,7 +44,12 @@ export function createEnableStartPreauthHandler(opts) {
|
|
|
44
44
|
// "system" mode: the guest dispatch identity's own tenantId is
|
|
45
45
|
// meaningless here — the preauthSetupToken is the source of truth
|
|
46
46
|
// for which tenant's row to read, mirroring verify.write.ts.
|
|
47
|
-
const scopedDb = createTenantDb({
|
|
47
|
+
const scopedDb = createTenantDb({
|
|
48
|
+
db: ctx.db.unsafeRaw(),
|
|
49
|
+
tenantId,
|
|
50
|
+
mode: "system",
|
|
51
|
+
reason: "pre-auth MFA setup has no session, the tenant comes from the verified setup token",
|
|
52
|
+
});
|
|
48
53
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
49
54
|
const existing = await findUserMfaRow(scopedDb, scopedUser);
|
|
50
55
|
if (existing)
|
|
@@ -53,7 +53,7 @@ export function createEnableStartHandler(opts) {
|
|
|
53
53
|
if (!accountLabel) {
|
|
54
54
|
// The declarative secretMint screen has no client component left to
|
|
55
55
|
// read the session email off — derive the otpauth label server-side.
|
|
56
|
-
accountLabel = readableAccountLabel(await ctx.queryAs(event.user, UserQueries.me, {}));
|
|
56
|
+
accountLabel = readableAccountLabel(await ctx.dispatch.queryAs(event.user, UserQueries.me, {}));
|
|
57
57
|
}
|
|
58
58
|
const secret = generateTotpSecret();
|
|
59
59
|
const recoveryCodes = generateRecoveryCodes();
|
|
@@ -47,12 +47,12 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
47
47
|
// hoisted function declarations below capture this — pin the narrowed
|
|
48
48
|
// type explicitly so TS keeps it inside the closures.
|
|
49
49
|
const cipher = maybeCipher;
|
|
50
|
-
if (!ctx.masterKeyProvider) {
|
|
50
|
+
if (!ctx.services.masterKeyProvider) {
|
|
51
51
|
throw new InternalError({
|
|
52
|
-
message: "[auth-mfa:reencrypt] ctx.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
|
|
52
|
+
message: "[auth-mfa:reencrypt] ctx.services.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
const provider = ctx.masterKeyProvider;
|
|
55
|
+
const provider = ctx.services.masterKeyProvider;
|
|
56
56
|
// Undefined = per-subject crypto-shredding isn't configured for this app —
|
|
57
57
|
// both fields are then plain envelope strings, no PII layer to peel.
|
|
58
58
|
const piiKms = configuredPiiSubjectKms();
|
|
@@ -65,7 +65,12 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
65
65
|
function tdbFor(tenantId) {
|
|
66
66
|
let existing = tdbCache.get(tenantId);
|
|
67
67
|
if (!existing) {
|
|
68
|
-
existing = createTenantDb({
|
|
68
|
+
existing = createTenantDb({
|
|
69
|
+
db,
|
|
70
|
+
tenantId,
|
|
71
|
+
mode: "system",
|
|
72
|
+
reason: "MFA key re-encryption job rewrites enrollments of every tenant",
|
|
73
|
+
});
|
|
69
74
|
tdbCache.set(tenantId, existing);
|
|
70
75
|
}
|
|
71
76
|
return existing;
|
|
@@ -101,7 +106,7 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
101
106
|
const totpClass = classifyStoredEnvelope(envelopeValues.totpSecret, targetVersion);
|
|
102
107
|
const recoveryClass = classifyStoredEnvelope(envelopeValues.recoveryCodes, targetVersion);
|
|
103
108
|
if (totpClass === "unrecognized" || recoveryClass === "unrecognized") {
|
|
104
|
-
ctx.log?.warn?.(`[auth-mfa:reencrypt] row ${row.id} has a field that is not a current-cipher envelope, not re-encryptable`);
|
|
109
|
+
ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] row ${row.id} has a field that is not a current-cipher envelope, not re-encryptable`);
|
|
105
110
|
return "failed";
|
|
106
111
|
}
|
|
107
112
|
if (totpClass === "current" && recoveryClass === "current") {
|
|
@@ -128,7 +133,7 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
128
133
|
if (!result.isSuccess) {
|
|
129
134
|
if (result.error.code === "version_conflict")
|
|
130
135
|
return "skipped";
|
|
131
|
-
ctx.log?.warn?.(`[auth-mfa:reencrypt] executor rejected row ${row.id}`, {
|
|
136
|
+
ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] executor rejected row ${row.id}`, {
|
|
132
137
|
code: result.error.code,
|
|
133
138
|
});
|
|
134
139
|
return "failed";
|
|
@@ -140,9 +145,9 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
140
145
|
migrateRow,
|
|
141
146
|
maxFailures,
|
|
142
147
|
deadlineAt: deadline,
|
|
143
|
-
signal: ctx.signal,
|
|
148
|
+
signal: ctx.lifecycle.signal,
|
|
144
149
|
onRowError: (row, err) => {
|
|
145
|
-
ctx.log?.warn?.(`[auth-mfa:reencrypt] failed to re-encrypt row ${row.id}`, { err });
|
|
150
|
+
ctx.obs.log?.warn?.(`[auth-mfa:reencrypt] failed to re-encrypt row ${row.id}`, { err });
|
|
146
151
|
},
|
|
147
152
|
});
|
|
148
153
|
const result = {
|
|
@@ -152,5 +157,5 @@ export async function mfaReencryptJob(rawPayload, ctx, db) {
|
|
|
152
157
|
batchesProcessed: outcome.batchesProcessed,
|
|
153
158
|
stoppedReason: outcome.stoppedReason,
|
|
154
159
|
};
|
|
155
|
-
ctx.log?.info?.(`[auth-mfa:reencrypt] complete: ${JSON.stringify(result)}`);
|
|
160
|
+
ctx.obs.log?.info?.(`[auth-mfa:reencrypt] complete: ${JSON.stringify(result)}`);
|
|
156
161
|
}
|
|
@@ -37,16 +37,16 @@ export function createRegenerateRecoveryHandler(opts) {
|
|
|
37
37
|
// afterward.
|
|
38
38
|
//
|
|
39
39
|
// Fail closed, matching verify.write.ts/enable-confirm-preauth.write.ts:
|
|
40
|
-
// without ctx.redis, verifyMfaFactor's TOTP-replay burn is skipped,
|
|
40
|
+
// without ctx.services.redis, verifyMfaFactor's TOTP-replay burn is skipped,
|
|
41
41
|
// letting an observed code be replayed within the ±1-step window to
|
|
42
42
|
// regenerate (and thus invalidate) recovery codes — a session-authed
|
|
43
43
|
// route, so pre-unwiring JWTs stay a valid attack path.
|
|
44
|
-
if (!ctx.redis) {
|
|
44
|
+
if (!ctx.services.redis) {
|
|
45
45
|
return writeFailure(new InternalError({
|
|
46
|
-
message: "regenerate-recovery (mfa) requires ctx.redis for TOTP replay protection",
|
|
46
|
+
message: "regenerate-recovery (mfa) requires ctx.services.redis for TOTP replay protection",
|
|
47
47
|
}));
|
|
48
48
|
}
|
|
49
|
-
const replay = { redis: ctx.redis, userId: event.user.id };
|
|
49
|
+
const replay = { redis: ctx.services.redis, userId: event.user.id };
|
|
50
50
|
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
51
51
|
if (!verify.ok)
|
|
52
52
|
return invalidTotpCode();
|
|
@@ -29,8 +29,8 @@ export function createMfaVerifyHandler(opts) {
|
|
|
29
29
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
30
30
|
escapeHatch: {
|
|
31
31
|
grants: ["systemIdentity", "unsafeRaw"],
|
|
32
|
-
reason: "Pre-auth MFA step has no session yet — re-derives it via ctx.queryAs(SYSTEM, " +
|
|
33
|
-
"user:findForAuth) and ctx.resolveActiveMembership for the user the challenge token names. " +
|
|
32
|
+
reason: "Pre-auth MFA step has no session yet — re-derives it via ctx.dispatch.queryAs(SYSTEM, " +
|
|
33
|
+
"user:findForAuth) and ctx.auth.resolveActiveMembership for the user the challenge token names. " +
|
|
34
34
|
"Also reads the MFA enrollment of the tenant named in the signed login/setup token, not the " +
|
|
35
35
|
"guest dispatch tenant.",
|
|
36
36
|
},
|
|
@@ -46,19 +46,19 @@ export function createMfaVerifyHandler(opts) {
|
|
|
46
46
|
// this route sits behind a challenge token from a successful login,
|
|
47
47
|
// but its own secret is still just a short TOTP/recovery code —
|
|
48
48
|
// silently skipping the brute-force cap and single-use burn when
|
|
49
|
-
// ctx.redis is absent would let a stolen challenge token be guessed
|
|
49
|
+
// ctx.services.redis is absent would let a stolen challenge token be guessed
|
|
50
50
|
// against unboundedly, and let a recovery code be replayed via a
|
|
51
51
|
// reissued challenge (mfa-verify-attempts.ts's counter is deliberately
|
|
52
52
|
// NOT reset by reissuance, precisely to survive this).
|
|
53
|
-
if (!ctx.redis) {
|
|
53
|
+
if (!ctx.services.redis) {
|
|
54
54
|
return writeFailure(new InternalError({
|
|
55
|
-
message: "verify (mfa) requires ctx.redis for the brute-force cap and single-use burn",
|
|
55
|
+
message: "verify (mfa) requires ctx.services.redis for the brute-force cap and single-use burn",
|
|
56
56
|
}));
|
|
57
57
|
}
|
|
58
58
|
// Per-account brute-force cap — survives challenge-token reissuance
|
|
59
59
|
// on purpose (see mfa-verify-attempts.ts header). Checked BEFORE the
|
|
60
60
|
// TOTP/recovery verify so a locked account can't be guessed against.
|
|
61
|
-
const state = await getMfaVerifyLockoutState(ctx.redis, userId);
|
|
61
|
+
const state = await getMfaVerifyLockoutState(ctx.services.redis, userId);
|
|
62
62
|
if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
|
|
63
63
|
const now = Date.now();
|
|
64
64
|
if (state.lockedUntil > now) {
|
|
@@ -69,7 +69,12 @@ export function createMfaVerifyHandler(opts) {
|
|
|
69
69
|
// "system" mode: this handler runs with a guest identity whose own
|
|
70
70
|
// tenantId is meaningless here — the challenge token is the source
|
|
71
71
|
// of truth for which tenant's row to read.
|
|
72
|
-
const scopedDb = createTenantDb({
|
|
72
|
+
const scopedDb = createTenantDb({
|
|
73
|
+
db: ctx.db.unsafeRaw(),
|
|
74
|
+
tenantId,
|
|
75
|
+
mode: "system",
|
|
76
|
+
reason: "MFA challenge has no session, the tenant comes from the verified challenge token",
|
|
77
|
+
});
|
|
73
78
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
74
79
|
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
75
80
|
// MFA got disabled between login and verify (race, or a stale
|
|
@@ -77,10 +82,10 @@ export function createMfaVerifyHandler(opts) {
|
|
|
77
82
|
// bad code, no detail leak about account state.
|
|
78
83
|
if (!row)
|
|
79
84
|
return invalidChallengeToken();
|
|
80
|
-
const replay = { redis: ctx.redis, userId };
|
|
85
|
+
const replay = { redis: ctx.services.redis, userId };
|
|
81
86
|
const verify = await verifyMfaFactor(row, event.payload.code, replay);
|
|
82
87
|
if (!verify.ok) {
|
|
83
|
-
await recordFailedMfaVerifyAttempt(ctx.redis, userId, maxAttempts, lockoutMinutes);
|
|
88
|
+
await recordFailedMfaVerifyAttempt(ctx.services.redis, userId, maxAttempts, lockoutMinutes);
|
|
84
89
|
return invalidTotpCode();
|
|
85
90
|
}
|
|
86
91
|
// Burn the challenge token only after a successful code check — a
|
|
@@ -91,7 +96,7 @@ export function createMfaVerifyHandler(opts) {
|
|
|
91
96
|
// is reached, so this burn only needs to guard a DIFFERENT code
|
|
92
97
|
// (recovery, or a still-valid-but-not-yet-burned TOTP step) reused
|
|
93
98
|
// against the same challenge token.
|
|
94
|
-
const burnResult = await burnToken(ctx.redis, "mfa-challenge", userId, verified.expiresAtMs);
|
|
99
|
+
const burnResult = await burnToken(ctx.services.redis, "mfa-challenge", userId, verified.expiresAtMs);
|
|
95
100
|
if (burnResult === "already-used")
|
|
96
101
|
return invalidChallengeToken();
|
|
97
102
|
// Recovery-code use consumes the code — persist the reduced hash-list
|
|
@@ -106,12 +111,12 @@ export function createMfaVerifyHandler(opts) {
|
|
|
106
111
|
if (!updateResult.isSuccess)
|
|
107
112
|
return updateResult;
|
|
108
113
|
}
|
|
109
|
-
await clearMfaVerifyAttempts(ctx.redis, userId);
|
|
114
|
+
await clearMfaVerifyAttempts(ctx.services.redis, userId);
|
|
110
115
|
// Re-derive the full session the way login.write.ts would have, for
|
|
111
116
|
// the SAME tenant the challenge token already committed to (no
|
|
112
117
|
// "pick a tenant" step here — that already happened at login time).
|
|
113
118
|
const systemUser = createSystemUser(tenantId, ["SystemAdmin"]);
|
|
114
|
-
const userRow = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
119
|
+
const userRow = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
115
120
|
id: userId,
|
|
116
121
|
})); // @cast-boundary engine-payload
|
|
117
122
|
// Re-check status + membership the way login.write.ts does after its
|
|
@@ -130,7 +135,7 @@ export function createMfaVerifyHandler(opts) {
|
|
|
130
135
|
const globalRoles = parseRoles(userRow?.roles ?? null);
|
|
131
136
|
// Membership revoked or principal blocked between login and verify —
|
|
132
137
|
// mirror login.write.ts's refusal instead of falling back to globalRoles only.
|
|
133
|
-
const active = await ctx.resolveActiveMembership(userId, tenantId);
|
|
138
|
+
const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
|
|
134
139
|
if (active.kind === "rejected")
|
|
135
140
|
return invalidChallengeToken();
|
|
136
141
|
// buildSessionRoles calls stripForbiddenMembershipRoles to strip reserved
|
|
@@ -144,7 +149,7 @@ export function createMfaVerifyHandler(opts) {
|
|
|
144
149
|
...sessionTimezoneField(userRow?.timezone),
|
|
145
150
|
...sessionLocaleField(userRow?.locale),
|
|
146
151
|
};
|
|
147
|
-
const claims = await ctx.
|
|
152
|
+
const claims = await ctx.auth.resolveClaims(baseSession);
|
|
148
153
|
const session = Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
|
|
149
154
|
return { isSuccess: true, data: { kind: "mfa-verify-success", session } };
|
|
150
155
|
},
|
|
@@ -14,15 +14,20 @@ export function createMfaCodeVerifier() {
|
|
|
14
14
|
declareEscapeHatch({
|
|
15
15
|
reason: "reads the MFA enrollment of the user being re-authenticated on the caller's handler context; the calling handler declares its own escapeHatch",
|
|
16
16
|
});
|
|
17
|
-
const scopedDb = createTenantDb({
|
|
17
|
+
const scopedDb = createTenantDb({
|
|
18
|
+
db: ctx.db.unsafeRaw(),
|
|
19
|
+
tenantId,
|
|
20
|
+
mode: "system",
|
|
21
|
+
reason: "re-authentication reads the MFA enrollment of the user's own tenant, not the handler's dispatch tenant",
|
|
22
|
+
});
|
|
18
23
|
const row = await findUserMfaRow(scopedDb, { id: userId, tenantId, roles: [] });
|
|
19
24
|
if (!row)
|
|
20
25
|
return { enrolled: false, ok: false };
|
|
21
|
-
// Fail closed without ctx.redis, matching disable.write.ts/verify.write.ts —
|
|
26
|
+
// Fail closed without ctx.services.redis, matching disable.write.ts/verify.write.ts —
|
|
22
27
|
// skipping the TOTP-replay burn would let an observed code be replayed.
|
|
23
|
-
if (!code || !ctx.redis)
|
|
28
|
+
if (!code || !ctx.services.redis)
|
|
24
29
|
return { enrolled: true, ok: false };
|
|
25
|
-
const verify = await verifyMfaFactor(row, code, { redis: ctx.redis, userId });
|
|
30
|
+
const verify = await verifyMfaFactor(row, code, { redis: ctx.services.redis, userId });
|
|
26
31
|
return { enrolled: true, ok: verify.ok && verify.method === "totp" };
|
|
27
32
|
};
|
|
28
33
|
}
|
|
@@ -11,7 +11,12 @@ export function createMfaStatusChecker(opts) {
|
|
|
11
11
|
declareEscapeHatch({
|
|
12
12
|
reason: "reads the MFA enrollment of the tenant named in the signed login/setup token, not the guest dispatch tenant, on the caller's handler context; the calling handler declares its own escapeHatch",
|
|
13
13
|
});
|
|
14
|
-
const scopedDb = createTenantDb({
|
|
14
|
+
const scopedDb = createTenantDb({
|
|
15
|
+
db: ctx.db.unsafeRaw(),
|
|
16
|
+
tenantId,
|
|
17
|
+
mode: "system",
|
|
18
|
+
reason: "login/setup token names the tenant to check, the guest dispatch tenant is meaningless",
|
|
19
|
+
});
|
|
15
20
|
const scopedUser = { id: userId, tenantId, roles: ["User"] };
|
|
16
21
|
const row = await findUserMfaRow(scopedDb, scopedUser);
|
|
17
22
|
if (row) {
|
|
@@ -22,12 +27,12 @@ export function createMfaStatusChecker(opts) {
|
|
|
22
27
|
return { required: true, challengeToken: token };
|
|
23
28
|
}
|
|
24
29
|
// ctx.config is bound to the CALLING user's tenant (the anonymous
|
|
25
|
-
// identity's tenant for login) — wrong tenant here. ctx.configResolver takes tenantId
|
|
30
|
+
// identity's tenant for login) — wrong tenant here. ctx.services.configResolver takes tenantId
|
|
26
31
|
// explicitly, which is what a pre-session check against the LOGGING-IN
|
|
27
32
|
// user's tenant needs.
|
|
28
33
|
// @cast-boundary engine-payload — "select" config values are validated
|
|
29
34
|
// against the declared `options` at write-time by the config feature.
|
|
30
|
-
const policy = ((await ctx.configResolver?.get(mfaRequiredConfigHandle.name, mfaRequiredConfigKey(opts.requiredPolicy), tenantId, userId, scopedDb)) ?? "optional");
|
|
35
|
+
const policy = ((await ctx.services.configResolver?.get(mfaRequiredConfigHandle.name, mfaRequiredConfigKey(opts.requiredPolicy), tenantId, userId, scopedDb)) ?? "optional");
|
|
31
36
|
if (policy === "optional")
|
|
32
37
|
return { required: false };
|
|
33
38
|
const buildSetupToken = () => signMfaPreauthSetupToken({ userId, tenantId }, MFA_PREAUTH_SETUP_TOKEN_TTL_MINUTES, opts.challengeTokenSecret).token;
|
|
@@ -13,7 +13,7 @@ import { STEP_SECONDS, verifyTotp } from "./totp.js";
|
|
|
13
13
|
// must not verify again within its ±1-step window — otherwise a code an
|
|
14
14
|
// attacker observed once, via phishing proxy or shoulder-surfing, stays
|
|
15
15
|
// usable for ~90s of parallel logins). Omitted where a caller has no
|
|
16
|
-
// ctx.redis available; the code-verify itself still works, just without
|
|
16
|
+
// ctx.services.redis available; the code-verify itself still works, just without
|
|
17
17
|
// replay protection for that call.
|
|
18
18
|
export async function verifyMfaFactor(row, code, replay) {
|
|
19
19
|
const secret = base32Decode(row.totpSecret);
|
|
@@ -39,8 +39,8 @@ export function createBillingInfoQueryConfig(deps) {
|
|
|
39
39
|
const billingLive = ctx.config
|
|
40
40
|
? (await ctx.config(STRIPE_BILLING_LIVE_CONFIG_QN)) === true
|
|
41
41
|
: false;
|
|
42
|
-
const apiKeySet = ctx.secrets
|
|
43
|
-
? await ctx.secrets.has(SYSTEM_TENANT_ID, STRIPE_API_KEY_CONFIG_QN)
|
|
42
|
+
const apiKeySet = ctx.services.secrets
|
|
43
|
+
? await ctx.services.secrets.has(SYSTEM_TENANT_ID, STRIPE_API_KEY_CONFIG_QN)
|
|
44
44
|
: false;
|
|
45
45
|
const sub = await ctx.db.fetchOne(subscriptionsProjectionTable, { id: subscriptionAggregateId(ctx.user.tenantId) });
|
|
46
46
|
return {
|
|
@@ -112,7 +112,7 @@ export async function recordConsent(ctx, prepared, details) {
|
|
|
112
112
|
};
|
|
113
113
|
const tenantId = ctx.user.tenantId;
|
|
114
114
|
const isSubscription = prepared.mode === "subscription";
|
|
115
|
-
await ctx.
|
|
115
|
+
await ctx.events.unsafeAppend({
|
|
116
116
|
aggregateId: isSubscription ? subscriptionAggregateId(tenantId) : paymentAggregateId(tenantId),
|
|
117
117
|
aggregateType: isSubscription ? SUBSCRIPTION_AGGREGATE_TYPE : PAYMENT_AGGREGATE_TYPE,
|
|
118
118
|
type: CHECKOUT_CONSENT_RECORDED_EVENT_QN,
|