@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
package/dist/audit/feature.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Narrow cross-feature query results (ctx.queryAs → Promise<unknown>) into
|
|
1
|
+
// Narrow cross-feature query results (ctx.dispatch.queryAs → Promise<unknown>) into
|
|
2
2
|
// the shape the auth handlers actually read. Replaces a bare
|
|
3
3
|
// `as AuthUserRow | null` at the system boundary — the coding standard
|
|
4
4
|
// requires a TypeGuard in place of unchecked casts from unknown.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { makeDispatchSystemWrite, makeDispatchWriteAs, } from "@cosmicdrift/kumiko-framework/api";
|
|
8
8
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
9
9
|
import { ConfigScopes, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
+
import { runInSystemRowLevelSecurityScope } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
|
|
10
11
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
11
12
|
// kumiko-lint-ignore cross-feature-import bootstrap sets per-tenant config through the config feature's write handler
|
|
12
13
|
import { ConfigHandlers } from "../config/web/constants.js";
|
|
@@ -139,7 +140,11 @@ async function decideInvite(deps, invite) {
|
|
|
139
140
|
function isMailingOutcome(outcome) {
|
|
140
141
|
return outcome === "invited" || outcome === "resent";
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
+
// A one-shot operator process, not a tenant request: it looks up users, memberships and invitations across tenants.
|
|
144
|
+
export function bootstrapTenants(deps, plan) {
|
|
145
|
+
return runInSystemRowLevelSecurityScope("boot tenant bootstrap creates tenants and memberships before any tenant exists", () => bootstrapTenantsInScope(deps, plan));
|
|
146
|
+
}
|
|
147
|
+
async function bootstrapTenantsInScope(deps, plan) {
|
|
143
148
|
const plannedInvites = planInvites(plan);
|
|
144
149
|
const dispatchSystemWrite = makeDispatchSystemWrite(deps.dispatcher);
|
|
145
150
|
const dispatchWriteAsRoles = makeDispatchWriteAs(deps.dispatcher);
|
|
@@ -48,7 +48,7 @@ export const authEmailPasswordEnvSchema = z.object({
|
|
|
48
48
|
.describe("Optional `iss` claim pinned on every minted JWT."),
|
|
49
49
|
});
|
|
50
50
|
// Auth feature — email+password login. Depends on the user feature for
|
|
51
|
-
// identity lookups (via ctx.queryAs) and on the tenant feature for
|
|
51
|
+
// identity lookups (via ctx.dispatch.queryAs) and on the tenant feature for
|
|
52
52
|
// membership resolution. No direct imports of foreign tables.
|
|
53
53
|
export function createAuthEmailPasswordFeature(opts = {}) {
|
|
54
54
|
// ≥32 chars, symmetric to the JWT_SECRET env check: a short HMAC secret
|
|
@@ -5,7 +5,7 @@ import { UserHandlers, UserQueries } from "../../user/index.js";
|
|
|
5
5
|
import { invalidCredentials } from "../errors.js";
|
|
6
6
|
import { passwordSchema } from "../password-policy.js";
|
|
7
7
|
// Change-password — authenticated. The user supplies their current password
|
|
8
|
-
// (re-auth) and the new one. The new hash is written via ctx.writeAs(system)
|
|
8
|
+
// (re-auth) and the new one. The new hash is written via ctx.dispatch.writeAs(system)
|
|
9
9
|
// against the user feature's update handler; field-access on passwordHash
|
|
10
10
|
// (privileged-only) lets the system identity through.
|
|
11
11
|
export const changePasswordWrite = defineWriteHandler({
|
|
@@ -21,8 +21,8 @@ export const changePasswordWrite = defineWriteHandler({
|
|
|
21
21
|
},
|
|
22
22
|
escapeHatch: {
|
|
23
23
|
grants: ["systemIdentity"],
|
|
24
|
-
reason: "Reads the caller's own passwordHash (privileged-only field) via ctx.queryAs(SYSTEM, ...) " +
|
|
25
|
-
"and writes the new hash via ctx.writeAs(SYSTEM, user:update) — the caller's own " +
|
|
24
|
+
reason: "Reads the caller's own passwordHash (privileged-only field) via ctx.dispatch.queryAs(SYSTEM, ...) " +
|
|
25
|
+
"and writes the new hash via ctx.dispatch.writeAs(SYSTEM, user:update) — the caller's own " +
|
|
26
26
|
"identity can't reach either, field-access on passwordHash is privileged-only.",
|
|
27
27
|
},
|
|
28
28
|
// Payload carries the old and new password.
|
|
@@ -31,7 +31,7 @@ export const changePasswordWrite = defineWriteHandler({
|
|
|
31
31
|
handler: async (event, ctx) => {
|
|
32
32
|
const systemUser = createSystemUser(event.user.tenantId);
|
|
33
33
|
// Load self with passwordHash — only visible to the privileged caller.
|
|
34
|
-
const me = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
34
|
+
const me = (await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
35
35
|
id: event.user.id,
|
|
36
36
|
})); // @cast-boundary db-runner
|
|
37
37
|
if (!me?.passwordHash) {
|
|
@@ -43,13 +43,13 @@ export const changePasswordWrite = defineWriteHandler({
|
|
|
43
43
|
}
|
|
44
44
|
const newHash = await hashPassword(event.payload.newPassword);
|
|
45
45
|
// The user aggregate is systemStream: its event stream lives on
|
|
46
|
-
// SYSTEM_TENANT_ID deterministically. Previously scattered streams
|
|
47
|
-
//
|
|
46
|
+
// SYSTEM_TENANT_ID deterministically. Previously scattered streams are moved by the
|
|
47
|
+
// boot-time stream-tenant backfill (runStreamTenantBackfillOnBoot).
|
|
48
48
|
const writer = createSystemUser(SYSTEM_TENANT_ID);
|
|
49
49
|
// Apply via user feature's update handler — writeAs(system) satisfies
|
|
50
50
|
// the privileged-only write rule on passwordHash. Pass the current version
|
|
51
51
|
// through so optimistic locking still applies end-to-end.
|
|
52
|
-
const writeRes = await ctx.writeAs(writer, UserHandlers.update, {
|
|
52
|
+
const writeRes = await ctx.dispatch.writeAs(writer, UserHandlers.update, {
|
|
53
53
|
id: me.id,
|
|
54
54
|
version: me.version,
|
|
55
55
|
changes: { passwordHash: newHash },
|
|
@@ -29,9 +29,9 @@ export function createConfirmAccountUnlockHandler(opts) {
|
|
|
29
29
|
i18nKey: "auth.errors.unlockNotConfigured",
|
|
30
30
|
}));
|
|
31
31
|
}
|
|
32
|
-
if (!ctx.redis) {
|
|
32
|
+
if (!ctx.services.redis) {
|
|
33
33
|
return writeFailure(new InternalError({
|
|
34
|
-
message: "account-unlock requires ctx.redis to clear lockout state",
|
|
34
|
+
message: "account-unlock requires ctx.services.redis to clear lockout state",
|
|
35
35
|
}));
|
|
36
36
|
}
|
|
37
37
|
// All verify failures (malformed / bad_signature / expired) fold into
|
|
@@ -40,7 +40,7 @@ export function createConfirmAccountUnlockHandler(opts) {
|
|
|
40
40
|
const verify = verifyUnlockToken(event.payload.token, opts.hmacSecret);
|
|
41
41
|
if (!verify.ok)
|
|
42
42
|
return invalidUnlockToken();
|
|
43
|
-
await clearLockoutState(ctx.redis, verify.userId);
|
|
43
|
+
await clearLockoutState(ctx.services.redis, verify.userId);
|
|
44
44
|
return { isSuccess: true, data: { kind: "account-unlocked" } };
|
|
45
45
|
},
|
|
46
46
|
});
|
|
@@ -30,10 +30,10 @@ export async function runConfirmTokenFlow(ctx, userId, expiresAtMs, spec) {
|
|
|
30
30
|
declareEscapeHatch({
|
|
31
31
|
reason: "writes the confirming user's row via UserHandlers.update on the caller's handler context, using a SYSTEM_TENANT-scoped identity; the calling handler declares its own escapeHatch",
|
|
32
32
|
});
|
|
33
|
-
if (!ctx.redis) {
|
|
33
|
+
if (!ctx.services.redis) {
|
|
34
34
|
return writeFailure(new InternalError({ message: spec.redisRequiredMessage }));
|
|
35
35
|
}
|
|
36
|
-
const redis = ctx.redis;
|
|
36
|
+
const redis = ctx.services.redis;
|
|
37
37
|
const burn = await burnToken(redis, spec.purpose, userId, expiresAtMs);
|
|
38
38
|
if (burn === "already-used")
|
|
39
39
|
return spec.invalidToken();
|
|
@@ -52,7 +52,7 @@ export async function runConfirmTokenFlow(ctx, userId, expiresAtMs, spec) {
|
|
|
52
52
|
return { isSuccess: true, data: spec.alreadyDone.data };
|
|
53
53
|
}
|
|
54
54
|
const changes = await spec.buildChanges(me);
|
|
55
|
-
const writeRes = await ctx.writeAs(systemUser, UserHandlers.update, {
|
|
55
|
+
const writeRes = await ctx.dispatch.writeAs(systemUser, UserHandlers.update, {
|
|
56
56
|
id: me.id,
|
|
57
57
|
version: me.version,
|
|
58
58
|
changes,
|
|
@@ -89,7 +89,7 @@ async function loadValidatedUser(ctx, systemUser, userId) {
|
|
|
89
89
|
declareEscapeHatch({
|
|
90
90
|
reason: "reads the confirming user's row via UserQueries.findForAuth on the caller's handler context, using a SYSTEM_TENANT-scoped identity; the calling handler declares its own escapeHatch",
|
|
91
91
|
});
|
|
92
|
-
const me = parseAuthUserRow(await ctx.queryAs(systemUser, UserQueries.findForAuth, { id: userId }));
|
|
92
|
+
const me = parseAuthUserRow(await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, { id: userId }));
|
|
93
93
|
if (!me || me.isDeleted || me.version === undefined || me.version < 1)
|
|
94
94
|
return null;
|
|
95
95
|
return { ...me, version: me.version };
|
|
@@ -64,13 +64,13 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
64
64
|
agent: { expose: false },
|
|
65
65
|
// kumiko-lint-ignore complexity-budget reuses login.write.ts's gate chain (lockout/password/email/status/membership/mfa) plus invite-specific branches (email match, already-member check, invitation update, unburn-on-failure) — splitting would scatter gate order across functions without reducing risk
|
|
66
66
|
handler: async (event, ctx) => {
|
|
67
|
-
if (!ctx.redis) {
|
|
68
|
-
return writeFailure(new InternalError({ message: "invite-accept-with-login requires ctx.redis" }));
|
|
67
|
+
if (!ctx.services.redis) {
|
|
68
|
+
return writeFailure(new InternalError({ message: "invite-accept-with-login requires ctx.services.redis" }));
|
|
69
69
|
}
|
|
70
|
-
const invitationId = await getInvitationIdForToken(ctx.redis, event.payload.token);
|
|
70
|
+
const invitationId = await getInvitationIdForToken(ctx.services.redis, event.payload.token);
|
|
71
71
|
if (!invitationId)
|
|
72
72
|
return invalidInviteToken();
|
|
73
|
-
const burn = await burnInviteToken(ctx.redis, event.payload.token);
|
|
73
|
+
const burn = await burnInviteToken(ctx.services.redis, event.payload.token);
|
|
74
74
|
if (burn === "already-used")
|
|
75
75
|
return invalidInviteToken();
|
|
76
76
|
let committed = false;
|
|
@@ -131,6 +131,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
131
131
|
db: dbConn,
|
|
132
132
|
tenantId: invitationTenantId,
|
|
133
133
|
mode: "system",
|
|
134
|
+
reason: "invitation tenant differs from the caller's tenant, the accept step updates the invitation row there",
|
|
134
135
|
});
|
|
135
136
|
const updateResult = await invitationExecutor.update({
|
|
136
137
|
id: invitationId,
|
|
@@ -139,7 +140,7 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
139
140
|
}, createSystemUser(invitationTenantId), invitationTdb);
|
|
140
141
|
if (!updateResult.isSuccess)
|
|
141
142
|
return updateResult;
|
|
142
|
-
await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
|
|
143
|
+
await deleteInviteToken(ctx.services.redis, { invitationId, token: event.payload.token });
|
|
143
144
|
// buildSessionRoles calls stripForbiddenMembershipRoles internally —
|
|
144
145
|
// a reserved role on an existing membership must never reach the session.
|
|
145
146
|
const mergedRoles = buildSessionRoles(globalGrant.data.roles, grant.data.roles);
|
|
@@ -172,8 +173,8 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
finally {
|
|
175
|
-
if (!committed && ctx.redis) {
|
|
176
|
-
await unburnInviteToken(ctx.redis, event.payload.token);
|
|
176
|
+
if (!committed && ctx.services.redis) {
|
|
177
|
+
await unburnInviteToken(ctx.services.redis, event.payload.token);
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
},
|
|
@@ -55,13 +55,15 @@ export function createInviteAcceptHandler() {
|
|
|
55
55
|
},
|
|
56
56
|
// kumiko-lint-ignore complexity-budget invite branches (auth/anon/burn) stay in one handler
|
|
57
57
|
handler: async (event, ctx) => {
|
|
58
|
-
if (!ctx.redis) {
|
|
59
|
-
return writeFailure(new InternalError({
|
|
58
|
+
if (!ctx.services.redis) {
|
|
59
|
+
return writeFailure(new InternalError({
|
|
60
|
+
message: "invite-accept requires ctx.services.redis for token consumption",
|
|
61
|
+
}));
|
|
60
62
|
}
|
|
61
|
-
const invitationId = await getInvitationIdForToken(ctx.redis, event.payload.token);
|
|
63
|
+
const invitationId = await getInvitationIdForToken(ctx.services.redis, event.payload.token);
|
|
62
64
|
if (!invitationId)
|
|
63
65
|
return invalidInviteToken();
|
|
64
|
-
const burn = await burnInviteToken(ctx.redis, event.payload.token);
|
|
66
|
+
const burn = await burnInviteToken(ctx.services.redis, event.payload.token);
|
|
65
67
|
if (burn === "already-used")
|
|
66
68
|
return invalidInviteToken();
|
|
67
69
|
let committed = false;
|
|
@@ -111,6 +113,7 @@ export function createInviteAcceptHandler() {
|
|
|
111
113
|
db: dbConn,
|
|
112
114
|
tenantId: invitationTenantId,
|
|
113
115
|
mode: "system",
|
|
116
|
+
reason: "invitation tenant differs from the caller's tenant, the accept step updates the invitation row there",
|
|
114
117
|
});
|
|
115
118
|
const updateResult = await invitationExecutor.update({
|
|
116
119
|
id: invitationId,
|
|
@@ -119,7 +122,7 @@ export function createInviteAcceptHandler() {
|
|
|
119
122
|
}, createSystemUser(invitationTenantId), invitationTdb);
|
|
120
123
|
if (!updateResult.isSuccess)
|
|
121
124
|
return updateResult;
|
|
122
|
-
await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
|
|
125
|
+
await deleteInviteToken(ctx.services.redis, { invitationId, token: event.payload.token });
|
|
123
126
|
committed = true;
|
|
124
127
|
return {
|
|
125
128
|
isSuccess: true,
|
|
@@ -132,8 +135,8 @@ export function createInviteAcceptHandler() {
|
|
|
132
135
|
};
|
|
133
136
|
}
|
|
134
137
|
finally {
|
|
135
|
-
if (!committed && ctx.redis) {
|
|
136
|
-
await unburnInviteToken(ctx.redis, event.payload.token);
|
|
138
|
+
if (!committed && ctx.services.redis) {
|
|
139
|
+
await unburnInviteToken(ctx.services.redis, event.payload.token);
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
},
|
|
@@ -76,8 +76,8 @@ async function currentMembershipVersion(ctx, tenantId, email) {
|
|
|
76
76
|
return membershipVersionOf(ctx.db, tenantId, user.id);
|
|
77
77
|
}
|
|
78
78
|
async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAccepted = false) {
|
|
79
|
-
if (!ctx.redis) {
|
|
80
|
-
return writeFailure(new InternalError({ message: "invite-create requires ctx.redis for token store" }));
|
|
79
|
+
if (!ctx.services.redis) {
|
|
80
|
+
return writeFailure(new InternalError({ message: "invite-create requires ctx.services.redis for token store" }));
|
|
81
81
|
}
|
|
82
82
|
const email = request.email.toLowerCase();
|
|
83
83
|
const tenantId = inviter.tenantId;
|
|
@@ -100,7 +100,7 @@ async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAc
|
|
|
100
100
|
const existingVersion = existing["version"]; // @cast-boundary db-row
|
|
101
101
|
// At most one live invite token per invitation: invalidate
|
|
102
102
|
// whatever's there before minting the new one.
|
|
103
|
-
await invalidateExistingInviteToken(ctx.redis, invitationId);
|
|
103
|
+
await invalidateExistingInviteToken(ctx.services.redis, invitationId);
|
|
104
104
|
const updateResult = await executor.update({
|
|
105
105
|
id: invitationId,
|
|
106
106
|
version: existingVersion,
|
|
@@ -133,7 +133,7 @@ async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAc
|
|
|
133
133
|
invitationId = createResult.data.id; // @cast-boundary engine-payload
|
|
134
134
|
}
|
|
135
135
|
const token = generateToken();
|
|
136
|
-
await storeInviteToken(ctx.redis, { invitationId, token, ttlSeconds });
|
|
136
|
+
await storeInviteToken(ctx.services.redis, { invitationId, token, ttlSeconds });
|
|
137
137
|
const locale = request.locale ?? resolveHandlerMailLocale(ctx, opts.locale);
|
|
138
138
|
await dispatchMagicLinkMail(ctx.notify, {
|
|
139
139
|
handlerName: "invite-create",
|
|
@@ -40,9 +40,9 @@ export const inviteInfoQuery = defineQueryHandler({
|
|
|
40
40
|
description: "Anonymous, read-only invite lookup for the invite-acceptance page: reveals the invited " +
|
|
41
41
|
"email and whether an account already exists for it, without consuming the token.",
|
|
42
42
|
handler: async (query, ctx) => {
|
|
43
|
-
if (!ctx.redis)
|
|
43
|
+
if (!ctx.services.redis)
|
|
44
44
|
throwInvalidInviteToken();
|
|
45
|
-
const invitationId = await getInvitationIdForToken(ctx.redis, query.payload.token);
|
|
45
|
+
const invitationId = await getInvitationIdForToken(ctx.services.redis, query.payload.token);
|
|
46
46
|
if (!invitationId)
|
|
47
47
|
throwInvalidInviteToken();
|
|
48
48
|
const invitation = await fetchOne(ctx.db.unsafeRaw(), tenantInvitationsTable, {
|
|
@@ -55,13 +55,13 @@ export function createInviteSignupCompleteHandler(opts = {}) {
|
|
|
55
55
|
reason: INVITE_SIGNUP_COMPLETE_ESCAPE_HATCH_REASON,
|
|
56
56
|
},
|
|
57
57
|
handler: async (event, ctx) => {
|
|
58
|
-
if (!ctx.redis) {
|
|
59
|
-
return writeFailure(new InternalError({ message: "invite-signup-complete requires ctx.redis" }));
|
|
58
|
+
if (!ctx.services.redis) {
|
|
59
|
+
return writeFailure(new InternalError({ message: "invite-signup-complete requires ctx.services.redis" }));
|
|
60
60
|
}
|
|
61
|
-
const invitationId = await getInvitationIdForToken(ctx.redis, event.payload.token);
|
|
61
|
+
const invitationId = await getInvitationIdForToken(ctx.services.redis, event.payload.token);
|
|
62
62
|
if (!invitationId)
|
|
63
63
|
return invalidInviteToken();
|
|
64
|
-
const burn = await burnInviteToken(ctx.redis, event.payload.token);
|
|
64
|
+
const burn = await burnInviteToken(ctx.services.redis, event.payload.token);
|
|
65
65
|
if (burn === "already-used")
|
|
66
66
|
return invalidInviteToken();
|
|
67
67
|
let committed = false;
|
|
@@ -115,6 +115,7 @@ export function createInviteSignupCompleteHandler(opts = {}) {
|
|
|
115
115
|
db: dbConn,
|
|
116
116
|
tenantId: invitationTenantId,
|
|
117
117
|
mode: "system",
|
|
118
|
+
reason: "invitation tenant differs from the caller's tenant, the accept step updates the invitation row there",
|
|
118
119
|
});
|
|
119
120
|
const updateResult = await invitationExecutor.update({
|
|
120
121
|
id: invitationId,
|
|
@@ -130,7 +131,7 @@ export function createInviteSignupCompleteHandler(opts = {}) {
|
|
|
130
131
|
// call, but an MFA-gated role gets the MFA step instead of a session.
|
|
131
132
|
// It runs before the token is deleted so a throwing check leaves the link retryable.
|
|
132
133
|
const mfaGate = await gateEnforceMfa(ctx, { mfaStatusChecker: opts.mfaStatusChecker }, userId, invitationTenantId, mergedRoles);
|
|
133
|
-
await deleteInviteToken(ctx.redis, { invitationId, token: event.payload.token });
|
|
134
|
+
await deleteInviteToken(ctx.services.redis, { invitationId, token: event.payload.token });
|
|
134
135
|
if (mfaGate !== undefined) {
|
|
135
136
|
committed = true;
|
|
136
137
|
return { isSuccess: true, data: mfaGate };
|
|
@@ -153,8 +154,8 @@ export function createInviteSignupCompleteHandler(opts = {}) {
|
|
|
153
154
|
};
|
|
154
155
|
}
|
|
155
156
|
finally {
|
|
156
|
-
if (!committed && ctx.redis) {
|
|
157
|
-
await unburnInviteToken(ctx.redis, event.payload.token);
|
|
157
|
+
if (!committed && ctx.services.redis) {
|
|
158
|
+
await unburnInviteToken(ctx.services.redis, event.payload.token);
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
},
|
|
@@ -34,12 +34,12 @@ function unknownEmailLockoutSubject(ctx, email) {
|
|
|
34
34
|
}
|
|
35
35
|
export async function gateCountUnknownUser(ctx, email, maxFailedAttempts, lockoutDurationMinutes) {
|
|
36
36
|
const subject = unknownEmailLockoutSubject(ctx, email);
|
|
37
|
-
if (!ctx.redis || subject === undefined)
|
|
37
|
+
if (!ctx.services.redis || subject === undefined)
|
|
38
38
|
return ok(undefined);
|
|
39
39
|
const locked = await gateEnforceLockout(ctx, subject);
|
|
40
40
|
if (!locked.ok)
|
|
41
41
|
return locked;
|
|
42
|
-
await recordFailedAttempt(ctx.redis, subject, maxFailedAttempts, lockoutDurationMinutes);
|
|
42
|
+
await recordFailedAttempt(ctx.services.redis, subject, maxFailedAttempts, lockoutDurationMinutes);
|
|
43
43
|
return ok(undefined);
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
@@ -47,9 +47,9 @@ export async function gateCountUnknownUser(ctx, email, maxFailedAttempts, lockou
|
|
|
47
47
|
* Fail-open without Redis (IP rate-limit still covers partially).
|
|
48
48
|
*/
|
|
49
49
|
export async function gateEnforceLockout(ctx, userId) {
|
|
50
|
-
if (!ctx.redis)
|
|
50
|
+
if (!ctx.services.redis)
|
|
51
51
|
return ok(undefined);
|
|
52
|
-
const state = await getLockoutState(ctx.redis, userId);
|
|
52
|
+
const state = await getLockoutState(ctx.services.redis, userId);
|
|
53
53
|
if (state?.lockedUntil !== null && state?.lockedUntil !== undefined) {
|
|
54
54
|
const now = Date.now();
|
|
55
55
|
if (state.lockedUntil > now) {
|
|
@@ -63,15 +63,15 @@ export async function gateEnforceLockout(ctx, userId) {
|
|
|
63
63
|
export async function gateVerifyPassword(ctx, found, password, maxFailedAttempts, lockoutDurationMinutes) {
|
|
64
64
|
const passwordOk = await verifyPassword(found.passwordHash, password);
|
|
65
65
|
if (!passwordOk) {
|
|
66
|
-
if (ctx.redis) {
|
|
67
|
-
await recordFailedAttempt(ctx.redis, found.id, maxFailedAttempts, lockoutDurationMinutes);
|
|
66
|
+
if (ctx.services.redis) {
|
|
67
|
+
await recordFailedAttempt(ctx.services.redis, found.id, maxFailedAttempts, lockoutDurationMinutes);
|
|
68
68
|
}
|
|
69
69
|
return reject(invalidCredentials());
|
|
70
70
|
}
|
|
71
71
|
// Clear before MFA — MFA has its own attempt-cap; don't password-lock
|
|
72
72
|
// users who occasionally mistype across otherwise-successful logins.
|
|
73
|
-
if (ctx.redis) {
|
|
74
|
-
await clearLockoutState(ctx.redis, found.id);
|
|
73
|
+
if (ctx.services.redis) {
|
|
74
|
+
await clearLockoutState(ctx.services.redis, found.id);
|
|
75
75
|
}
|
|
76
76
|
return ok(undefined);
|
|
77
77
|
}
|
|
@@ -105,7 +105,7 @@ export async function gateResolveMembership(ctx, found, memberships) {
|
|
|
105
105
|
: memberships;
|
|
106
106
|
const globalRoles = parseRoles(found.roles ?? null);
|
|
107
107
|
for (const candidate of candidates) {
|
|
108
|
-
const active = await ctx.resolveActiveMembership(found.id, candidate.tenantId);
|
|
108
|
+
const active = await ctx.auth.resolveActiveMembership(found.id, candidate.tenantId);
|
|
109
109
|
if (active.kind === "rejected") {
|
|
110
110
|
// A blocked principal is blocked for every tenant, so stop here;
|
|
111
111
|
// not_a_member/tenant_teardown are per-tenant — try the next candidate.
|
|
@@ -149,14 +149,14 @@ export async function gateBuildSession(ctx, userId, tenantId, mergedRoles, timez
|
|
|
149
149
|
...(timezone !== null && timezone !== undefined && { timezone }),
|
|
150
150
|
...(locale !== null && locale !== undefined && { locale }),
|
|
151
151
|
};
|
|
152
|
-
const claims = await ctx.
|
|
152
|
+
const claims = await ctx.auth.resolveClaims(baseSession);
|
|
153
153
|
const session = Object.keys(claims).length > 0 ? { ...baseSession, claims } : baseSession;
|
|
154
154
|
return { kind: "auth-session", session };
|
|
155
155
|
}
|
|
156
156
|
// Login — unauthenticated entry point. The route is wired public (no JWT
|
|
157
157
|
// middleware), dispatching with the framework's anonymous identity for the
|
|
158
158
|
// handler's access check. Everything inside the handler goes through
|
|
159
|
-
// ctx.queryAs(system, ...) so the user feature stays the single owner of its
|
|
159
|
+
// ctx.dispatch.queryAs(system, ...) so the user feature stays the single owner of its
|
|
160
160
|
// table.
|
|
161
161
|
export function createLoginHandler(opts = {}) {
|
|
162
162
|
const strictVerification = opts.strictEmailVerification === true;
|
|
@@ -173,8 +173,8 @@ export function createLoginHandler(opts = {}) {
|
|
|
173
173
|
escapeHatch: {
|
|
174
174
|
grants: ["systemIdentity", "unsafeRaw"],
|
|
175
175
|
reason: "Unauthenticated login has no caller identity yet — it looks up the user row by " +
|
|
176
|
-
"email via ctx.queryAs(SYSTEM, ...) and resolves tenant membership via " +
|
|
177
|
-
"ctx.resolveActiveMembership before a session exists. It also reads the MFA " +
|
|
176
|
+
"email via ctx.dispatch.queryAs(SYSTEM, ...) and resolves tenant membership via " +
|
|
177
|
+
"ctx.auth.resolveActiveMembership before a session exists. It also reads the MFA " +
|
|
178
178
|
"enrollment of the tenant named in the signed login/setup token, not the guest " +
|
|
179
179
|
"dispatch tenant, via the mfaStatusChecker callback.",
|
|
180
180
|
},
|
|
@@ -183,7 +183,9 @@ export function createLoginHandler(opts = {}) {
|
|
|
183
183
|
description: "Signs a user in with email and password, running the lockout, email-verification, account-status, tenant-membership and MFA gates, and answering with a session or with an MFA challenge or setup requirement.",
|
|
184
184
|
handler: async (event, ctx) => {
|
|
185
185
|
const systemUser = createSystemUser(SYSTEM_USER_ID);
|
|
186
|
-
const foundRow = parseAuthUserRow(await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
186
|
+
const foundRow = parseAuthUserRow(await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
187
|
+
email: event.payload.email,
|
|
188
|
+
}));
|
|
187
189
|
if (!foundRow?.passwordHash || foundRow.isDeleted) {
|
|
188
190
|
const unknown = await gateCountUnknownUser(ctx, event.payload.email, maxFailedAttempts, lockoutDurationMinutes);
|
|
189
191
|
if (!unknown.ok)
|
|
@@ -206,7 +208,7 @@ export function createLoginHandler(opts = {}) {
|
|
|
206
208
|
if (!statusGate.ok)
|
|
207
209
|
return statusGate.result;
|
|
208
210
|
// Still needed for candidate ORDER (preferred tenant first) in gateResolveMembership.
|
|
209
|
-
const memberships = (await ctx.queryAs(systemUser, TENANT_MEMBERSHIPS_QUERY, {
|
|
211
|
+
const memberships = (await ctx.dispatch.queryAs(systemUser, TENANT_MEMBERSHIPS_QUERY, {
|
|
210
212
|
userId: found.id,
|
|
211
213
|
})); // @cast-boundary db-runner
|
|
212
214
|
const membershipGate = await gateResolveMembership(ctx, found, memberships);
|
|
@@ -25,7 +25,7 @@ export function createResetPasswordHandler(opts) {
|
|
|
25
25
|
escapeHatch: {
|
|
26
26
|
grants: ["systemIdentity"],
|
|
27
27
|
reason: "Anonymous token holder has no session — runConfirmTokenFlow loads and updates the " +
|
|
28
|
-
"target user via ctx.writeAs(SYSTEM, user:update) to set the new password hash.",
|
|
28
|
+
"target user via ctx.dispatch.writeAs(SYSTEM, user:update) to set the new password hash.",
|
|
29
29
|
},
|
|
30
30
|
agent: { expose: false },
|
|
31
31
|
handler: async (event, ctx) => {
|
|
@@ -42,7 +42,7 @@ export function createResetPasswordHandler(opts) {
|
|
|
42
42
|
return invalidResetToken();
|
|
43
43
|
return runConfirmTokenFlow(ctx, verify.userId, verify.expiresAtMs, {
|
|
44
44
|
purpose: "reset",
|
|
45
|
-
redisRequiredMessage: "password-reset requires ctx.redis to enforce token single-use",
|
|
45
|
+
redisRequiredMessage: "password-reset requires ctx.services.redis to enforce token single-use",
|
|
46
46
|
invalidToken: invalidResetToken,
|
|
47
47
|
buildChanges: async () => ({
|
|
48
48
|
passwordHash: await hashPassword(event.payload.newPassword),
|
|
@@ -55,16 +55,16 @@ function isBenignClaimRejection(error) {
|
|
|
55
55
|
async function claimBoundHandover(ctx, session, binding) {
|
|
56
56
|
const provider = findSignupHandoverProvider(ctx.registry);
|
|
57
57
|
if (!provider) {
|
|
58
|
-
ctx.log?.warn("signup-confirm: handover binding present but no signupHandover provider mounted");
|
|
58
|
+
ctx.obs.log?.warn("signup-confirm: handover binding present but no signupHandover provider mounted");
|
|
59
59
|
return undefined;
|
|
60
60
|
}
|
|
61
61
|
const claim = provider.mintClaim(binding);
|
|
62
|
-
const claimResult = await ctx.writeAs(session, claim.qualifiedName, claim.payload);
|
|
62
|
+
const claimResult = await ctx.dispatch.writeAs(session, claim.qualifiedName, claim.payload);
|
|
63
63
|
if (claimResult.isSuccess)
|
|
64
64
|
return { entityType: binding.entityType, id: binding.rowId };
|
|
65
65
|
if (!isBenignClaimRejection(claimResult.error))
|
|
66
66
|
throw reraiseAsKumikoError(claimResult.error);
|
|
67
|
-
ctx.log?.warn("signup-confirm: handover grant did not redeem", {
|
|
67
|
+
ctx.obs.log?.warn("signup-confirm: handover grant did not redeem", {
|
|
68
68
|
entityType: binding.entityType,
|
|
69
69
|
});
|
|
70
70
|
return undefined;
|
|
@@ -81,19 +81,19 @@ export function createSignupConfirmHandler(opts = {}) {
|
|
|
81
81
|
reason: SIGNUP_CONFIRM_PROVISION_REASON,
|
|
82
82
|
},
|
|
83
83
|
handler: async (event, ctx) => {
|
|
84
|
-
if (!ctx.redis) {
|
|
84
|
+
if (!ctx.services.redis) {
|
|
85
85
|
return writeFailure(new InternalError({
|
|
86
|
-
message: "signup-confirm requires ctx.redis for token consumption",
|
|
86
|
+
message: "signup-confirm requires ctx.services.redis for token consumption",
|
|
87
87
|
}));
|
|
88
88
|
}
|
|
89
89
|
// Token lookup: nonexistent, expired, and already-consumed tokens all
|
|
90
90
|
// collapse to invalid_signup_token to prevent enumeration.
|
|
91
|
-
const email = await getEmailForSignupToken(ctx.redis, event.payload.token);
|
|
91
|
+
const email = await getEmailForSignupToken(ctx.services.redis, event.payload.token);
|
|
92
92
|
if (!email)
|
|
93
93
|
return invalidSignupToken();
|
|
94
94
|
// Single-use burn: of two parallel confirmations from different tabs,
|
|
95
95
|
// one succeeds and the other receives invalid_signup_token.
|
|
96
|
-
const burn = await burnSignupToken(ctx.redis, event.payload.token);
|
|
96
|
+
const burn = await burnSignupToken(ctx.services.redis, event.payload.token);
|
|
97
97
|
if (burn === "already-used")
|
|
98
98
|
return invalidSignupToken();
|
|
99
99
|
let committed = false;
|
|
@@ -154,7 +154,7 @@ export function createSignupConfirmHandler(opts = {}) {
|
|
|
154
154
|
// Claim BEFORE spending the signup token or the binding below and
|
|
155
155
|
// before `committed` flips: if the claim throws, the `finally` still
|
|
156
156
|
// unburns and the activation link stays retryable.
|
|
157
|
-
const handoverBinding = await getSignupHandover(ctx.redis, event.payload.token);
|
|
157
|
+
const handoverBinding = await getSignupHandover(ctx.services.redis, event.payload.token);
|
|
158
158
|
const handover = handoverBinding
|
|
159
159
|
? await claimBoundHandover(ctx, session, handoverBinding)
|
|
160
160
|
: undefined;
|
|
@@ -162,9 +162,9 @@ export function createSignupConfirmHandler(opts = {}) {
|
|
|
162
162
|
const mfaGate = await gateEnforceMfa(ctx, { mfaStatusChecker: opts.mfaStatusChecker }, provisioned.userId, provisioned.tenantId, session.roles);
|
|
163
163
|
// Drop both token lookup keys; the burn key stays for its remaining
|
|
164
164
|
// TTL as replay protection.
|
|
165
|
-
await deleteSignupToken(ctx.redis, { email, token: event.payload.token });
|
|
165
|
+
await deleteSignupToken(ctx.services.redis, { email, token: event.payload.token });
|
|
166
166
|
if (handoverBinding)
|
|
167
|
-
await deleteSignupHandover(ctx.redis, event.payload.token);
|
|
167
|
+
await deleteSignupHandover(ctx.services.redis, event.payload.token);
|
|
168
168
|
if (mfaGate !== undefined) {
|
|
169
169
|
committed = true;
|
|
170
170
|
return {
|
|
@@ -192,10 +192,10 @@ export function createSignupConfirmHandler(opts = {}) {
|
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
finally {
|
|
195
|
-
if (!committed && ctx.redis) {
|
|
195
|
+
if (!committed && ctx.services.redis) {
|
|
196
196
|
// Release the burn so a retry after a DB hiccup is not blocked.
|
|
197
197
|
// Keep the token lookup keys so the user can click the email link again.
|
|
198
|
-
await unburnSignupToken(ctx.redis, event.payload.token);
|
|
198
|
+
await unburnSignupToken(ctx.services.redis, event.payload.token);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
},
|
|
@@ -73,12 +73,12 @@ export function createSignupRequestHandler(opts) {
|
|
|
73
73
|
// No email address here — this fires on every no-op request, PII
|
|
74
74
|
// must not accumulate in logs just for the "was this toggled off"
|
|
75
75
|
// signal a support ticket needs.
|
|
76
|
-
ctx.log?.info("signup-request skipped: self-registration disabled");
|
|
76
|
+
ctx.obs.log?.info("signup-request skipped: self-registration disabled");
|
|
77
77
|
return { isSuccess: true, data: { kind: "no-op" } };
|
|
78
78
|
}
|
|
79
|
-
if (!ctx.redis) {
|
|
79
|
+
if (!ctx.services.redis) {
|
|
80
80
|
return writeFailure(new InternalError({
|
|
81
|
-
message: "signup-request requires ctx.redis for the activation-token store",
|
|
81
|
+
message: "signup-request requires ctx.services.redis for the activation-token store",
|
|
82
82
|
}));
|
|
83
83
|
}
|
|
84
84
|
// Email normalization lives in the store (signup-token-store). The
|
|
@@ -89,9 +89,9 @@ export function createSignupRequestHandler(opts) {
|
|
|
89
89
|
// resend without a fresh `handover` in the payload (the common case:
|
|
90
90
|
// the visitor just retyped their email) must still carry the earlier
|
|
91
91
|
// verified grant over to the new token instead of silently dropping it.
|
|
92
|
-
const priorBinding = await takeOverSignupHandoverForResend(ctx.redis, email);
|
|
92
|
+
const priorBinding = await takeOverSignupHandoverForResend(ctx.services.redis, email);
|
|
93
93
|
const locale = resolveHandlerMailLocale(ctx, opts.locale);
|
|
94
|
-
const redis = ctx.redis;
|
|
94
|
+
const redis = ctx.services.redis;
|
|
95
95
|
const issued = await issueSignupActivation({
|
|
96
96
|
redis,
|
|
97
97
|
notify: ctx.notify,
|
|
@@ -115,7 +115,7 @@ export function createSignupRequestHandler(opts) {
|
|
|
115
115
|
const { entityType, token: grantToken } = event.payload.handover;
|
|
116
116
|
const provider = findSignupHandoverProvider(ctx.registry);
|
|
117
117
|
if (!provider) {
|
|
118
|
-
ctx.log?.warn("signup-request: handover payload present but no signupHandover provider mounted");
|
|
118
|
+
ctx.obs.log?.warn("signup-request: handover payload present but no signupHandover provider mounted");
|
|
119
119
|
}
|
|
120
120
|
else {
|
|
121
121
|
try {
|
|
@@ -129,11 +129,15 @@ export function createSignupRequestHandler(opts) {
|
|
|
129
129
|
binding = verified;
|
|
130
130
|
}
|
|
131
131
|
else {
|
|
132
|
-
ctx.log?.warn("signup-request: handover grant did not verify", {
|
|
132
|
+
ctx.obs.log?.warn("signup-request: handover grant did not verify", {
|
|
133
|
+
entityType,
|
|
134
|
+
});
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
137
|
catch {
|
|
136
|
-
ctx.log?.warn("signup-request: handover grant verification threw", {
|
|
138
|
+
ctx.obs.log?.warn("signup-request: handover grant verification threw", {
|
|
139
|
+
entityType,
|
|
140
|
+
});
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
}
|
|
@@ -24,10 +24,12 @@ export function createSwitchTenantMfaGateHandler(opts) {
|
|
|
24
24
|
description: "Runs the MFA gate of the login for a tenant switch and answers with an MFA challenge, a setup requirement, or clearance.",
|
|
25
25
|
handler: async (event, ctx) => {
|
|
26
26
|
const { userId, tenantId } = event.payload;
|
|
27
|
-
const active = await ctx.resolveActiveMembership(userId, tenantId);
|
|
27
|
+
const active = await ctx.auth.resolveActiveMembership(userId, tenantId);
|
|
28
28
|
if (active.kind === "rejected")
|
|
29
29
|
return noMembership();
|
|
30
|
-
const userRow = parseAuthUserRow(await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
30
|
+
const userRow = parseAuthUserRow(await ctx.dispatch.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
|
|
31
|
+
id: userId,
|
|
32
|
+
}));
|
|
31
33
|
const roles = buildSessionRoles(parseRoles(userRow?.roles ?? null), active.membership.roles);
|
|
32
34
|
const mfaGate = await gateEnforceMfa(ctx, opts, userId, tenantId, roles);
|
|
33
35
|
return { isSuccess: true, data: mfaGate ?? { kind: "mfa-gate-clear", roles } };
|
|
@@ -41,7 +41,7 @@ export function createTokenRequestHandler(spec, opts) {
|
|
|
41
41
|
escapeHatch: {
|
|
42
42
|
grants: ["systemIdentity"],
|
|
43
43
|
reason: "Anonymous requester has no session — looks up the target user by email via " +
|
|
44
|
-
"ctx.queryAs(SYSTEM, user:findForAuth) to mint the out-of-band token.",
|
|
44
|
+
"ctx.dispatch.queryAs(SYSTEM, user:findForAuth) to mint the out-of-band token.",
|
|
45
45
|
},
|
|
46
46
|
description: spec.description,
|
|
47
47
|
handler: async (event, ctx) => {
|
|
@@ -53,7 +53,7 @@ export function createTokenRequestHandler(spec, opts) {
|
|
|
53
53
|
}));
|
|
54
54
|
}
|
|
55
55
|
const systemUser = createSystemUser(event.user.tenantId);
|
|
56
|
-
const user = parseAuthUserRow(await ctx.queryAs(systemUser, UserQueries.findForAuth, {
|
|
56
|
+
const user = parseAuthUserRow(await ctx.dispatch.queryAs(systemUser, UserQueries.findForAuth, {
|
|
57
57
|
email: event.payload.email,
|
|
58
58
|
}));
|
|
59
59
|
// Silent-success branches all return the SAME shape with kind="no-op".
|