@cosmicdrift/kumiko-samples 0.363.0 → 0.365.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/LICENSE +1 -1
- package/package.json +1 -1
- package/packages/bundled-features/package.json +7 -1
- package/packages/bundled-features/src/audit/changes.json +18 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
- package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +18 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
- package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +19 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
- package/packages/bundled-features/src/channel-email/changes.json +18 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +47 -24
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +54 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
- package/packages/bundled-features/src/delivery/changes.json +30 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/delivery/index.ts +2 -0
- package/packages/bundled-features/src/delivery/jobs.ts +51 -14
- package/packages/bundled-features/src/delivery/types.ts +7 -1
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/packages/bundled-features/src/jobs/changes.json +31 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +56 -18
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/metrics/changes.json +6 -0
- package/packages/bundled-features/src/metrics/filters.ts +28 -12
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
- package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +12 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +8 -0
- package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
- package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/template-resolver/README.md +4 -2
- package/packages/bundled-features/src/template-resolver/api.ts +3 -2
- package/packages/bundled-features/src/template-resolver/changes.json +149 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +121 -53
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +18 -2
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/table.ts +13 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
- package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
- package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
- package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
- package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
- package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
- package/packages/bundled-features/src/tenant/seeding.ts +33 -27
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
- package/packages/bundled-features/src/tier-engine/changes.json +18 -0
- package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
- package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
- package/packages/bundled-features/src/tier-engine/index.ts +2 -0
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
- package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
- package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
- package/packages/bundled-features/src/waitlist/lib.ts +20 -0
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +9 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
- package/samples/recipes/auth-mfa/README.md +2 -1
- package/samples/recipes/auth-mfa/src/feature.ts +1 -0
- package/samples/recipes/custom-handlers/README.md +4 -3
- package/samples/recipes/custom-handlers/src/feature.ts +21 -12
- package/samples/recipes/dashboard-stats/README.md +38 -0
- package/samples/recipes/dashboard-stats/package.json +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/pdf-audit-export/README.md +2 -1
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
- package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
- package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
- package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
- package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.0",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"./shared/single-use-token-store": "./src/shared/single-use-token-store.ts",
|
|
107
107
|
"./shared/signed-token": "./src/shared/signed-token.ts",
|
|
108
108
|
"./shared/row-bound-grant": "./src/shared/row-bound-grant.ts",
|
|
109
|
+
"./shared/mail-template-body": "./src/shared/mail-template-body.ts",
|
|
109
110
|
"./delivery": "./src/delivery/index.ts",
|
|
110
111
|
"./delivery/web": "./src/delivery/web/index.ts",
|
|
111
112
|
"./channel-in-app": "./src/channel-in-app/index.ts",
|
|
@@ -180,6 +181,7 @@
|
|
|
180
181
|
"pdf-lib": "^1.17.1",
|
|
181
182
|
"qrcode": "^1.5.4",
|
|
182
183
|
"react": "^19.2.6",
|
|
184
|
+
"react-dom": "^19.2.6",
|
|
183
185
|
"sharp": "^0.35.5",
|
|
184
186
|
"stripe": "^22.6.2",
|
|
185
187
|
"tailwind-merge": "^3.6.0",
|
|
@@ -527,6 +529,10 @@
|
|
|
527
529
|
"types": "./dist/shared/row-bound-grant.d.ts",
|
|
528
530
|
"default": "./dist/shared/row-bound-grant.js"
|
|
529
531
|
},
|
|
532
|
+
"./shared/mail-template-body": {
|
|
533
|
+
"types": "./dist/shared/mail-template-body.d.ts",
|
|
534
|
+
"default": "./dist/shared/mail-template-body.js"
|
|
535
|
+
},
|
|
530
536
|
"./delivery": {
|
|
531
537
|
"types": "./dist/delivery/index.d.ts",
|
|
532
538
|
"default": "./dist/delivery/index.js"
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Escape-hatch retention keeps the operator floor and deletes in bounded batches",
|
|
6
|
+
"detail": "An explicitly configured retention key now acts as a lower bound for tenant compliance-profile cutoffs, so a tenant profile can no longer delete events younger than the operator allows. A key left at its default does not count. Pruning runs in batches of 5000 events, each in its own transaction, until a batch comes back short."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Audit retention events always use the system user",
|
|
12
|
+
"detail": "Events written by the audit feature no longer fall back to the calling user id and always record the system user."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.364.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Audit list pages by id unless sorted by type",
|
|
18
|
+
"detail": "An explicit createdAt sort pages in id order so keyset pagination no longer skips rows."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.361.0",
|
|
4
22
|
"type": "fix",
|
|
@@ -117,11 +117,16 @@ export const listQuery = defineQueryHandler({
|
|
|
117
117
|
createdAt: unknown;
|
|
118
118
|
createdBy: string;
|
|
119
119
|
}>(ctx.db, eventsTable, where, {
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
// The id cursor only resumes an id-ordered listing. created_at comes from the
|
|
121
|
+
// transaction start and can run against insert order, so ordering by it would let
|
|
122
|
+
// a page boundary skip rows; only the type sort leaves id order (and is one page).
|
|
123
|
+
orderBy:
|
|
124
|
+
p.sort === "type"
|
|
125
|
+
? [
|
|
126
|
+
{ col: "type", direction },
|
|
127
|
+
{ col: "id", direction },
|
|
128
|
+
]
|
|
129
|
+
: [{ col: "id", direction }],
|
|
125
130
|
limit: p.limit,
|
|
126
131
|
});
|
|
127
132
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
aggregateWhere,
|
|
3
|
+
runnerCanBeginTransaction,
|
|
4
|
+
selectMany,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
6
|
import {
|
|
3
7
|
COMPLIANCE_PROFILES,
|
|
4
8
|
subtractRetentionSpec,
|
|
@@ -27,7 +31,7 @@ import {
|
|
|
27
31
|
|
|
28
32
|
// pruneEvents opens its own transaction, so a runner that cannot begin one (a tx handle) is rejected instead of nested.
|
|
29
33
|
export function isDbConnection(runner: DbRunner): runner is DbConnection {
|
|
30
|
-
return
|
|
34
|
+
return runnerCanBeginTransaction(runner);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
async function resolveRetentionDays(args: {
|
|
@@ -48,9 +52,30 @@ async function resolveRetentionDays(args: {
|
|
|
48
52
|
return typeof raw === "number" && raw >= 1 ? raw : DEFAULT_ESCAPE_HATCH_RETENTION_DAYS;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
// Only an operator-set value counts: the key's declared default must not override the tenant profiles.
|
|
56
|
+
async function resolveExplicitRetentionDays(args: {
|
|
57
|
+
readonly registry: Registry;
|
|
58
|
+
readonly configResolver: ConfigResolver | undefined;
|
|
59
|
+
readonly db: DbConnection;
|
|
60
|
+
readonly userId: string;
|
|
61
|
+
}): Promise<number | undefined> {
|
|
62
|
+
const keyDef = args.registry.getConfigKey(ESCAPE_HATCH_RETENTION_DAYS_KEY);
|
|
63
|
+
if (!args.configResolver || !keyDef) return undefined;
|
|
64
|
+
const { value, source } = await args.configResolver.getWithSource(
|
|
65
|
+
ESCAPE_HATCH_RETENTION_DAYS_KEY,
|
|
66
|
+
keyDef,
|
|
67
|
+
SYSTEM_TENANT_ID,
|
|
68
|
+
args.userId,
|
|
69
|
+
args.db,
|
|
70
|
+
);
|
|
71
|
+
const isOperatorSet = source !== "default" && source !== "missing";
|
|
72
|
+
return isOperatorSet && typeof value === "number" && value >= 1 ? value : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
// Soft dependency: audit must stay mountable without compliance-profiles, so we probe its entity.
|
|
52
76
|
const COMPLIANCE_PROFILE_ENTITY = "tenant-compliance-profile";
|
|
53
77
|
const PRUNE_CHUNK_SIZE = 1000;
|
|
78
|
+
const PRUNE_BATCH_SIZE = 5000;
|
|
54
79
|
|
|
55
80
|
type AuditEventRow = {
|
|
56
81
|
readonly aggregateId: string;
|
|
@@ -68,11 +93,13 @@ async function resolveTenantCutoff(
|
|
|
68
93
|
db: DbConnection,
|
|
69
94
|
tenantId: TenantId,
|
|
70
95
|
now: Temporal.Instant,
|
|
96
|
+
operatorFloor: Temporal.Instant | undefined,
|
|
71
97
|
): Promise<Temporal.Instant> {
|
|
72
98
|
const { profile } = await resolveProfileForTenant({ db, tenantId });
|
|
73
99
|
const effective = subtractRetentionSpec(now, profile.auditLog.retention);
|
|
74
100
|
const base = subtractRetentionSpec(now, COMPLIANCE_PROFILES[profile.key].auditLog.retention);
|
|
75
|
-
|
|
101
|
+
const profileCutoff = earlierInstant(effective, base);
|
|
102
|
+
return operatorFloor ? earlierInstant(profileCutoff, operatorFloor) : profileCutoff;
|
|
76
103
|
}
|
|
77
104
|
|
|
78
105
|
// A cross-tenant audit (e.g. identity-switch) lives under the actor's tenant but must also
|
|
@@ -95,13 +122,20 @@ async function storingTenantIds(db: DbConnection): Promise<readonly TenantId[]>
|
|
|
95
122
|
});
|
|
96
123
|
}
|
|
97
124
|
|
|
98
|
-
async function pruneByTenantProfiles(
|
|
125
|
+
async function pruneByTenantProfiles(
|
|
126
|
+
db: DbConnection,
|
|
127
|
+
operatorRetentionDays: number | undefined,
|
|
128
|
+
): Promise<number> {
|
|
99
129
|
const now = getTemporal().Now.instant();
|
|
130
|
+
const operatorFloor =
|
|
131
|
+
operatorRetentionDays === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: now.subtract({ hours: operatorRetentionDays * 24 });
|
|
100
134
|
const cutoffByTenant = new Map<TenantId, Temporal.Instant>();
|
|
101
135
|
const cutoffOf = async (tenantId: TenantId): Promise<Temporal.Instant> => {
|
|
102
136
|
const cached = cutoffByTenant.get(tenantId);
|
|
103
137
|
if (cached) return cached;
|
|
104
|
-
const cutoff = await resolveTenantCutoff(db, tenantId, now);
|
|
138
|
+
const cutoff = await resolveTenantCutoff(db, tenantId, now, operatorFloor);
|
|
105
139
|
cutoffByTenant.set(tenantId, cutoff);
|
|
106
140
|
return cutoff;
|
|
107
141
|
};
|
|
@@ -139,11 +173,32 @@ async function pruneByTenantProfiles(db: DbConnection): Promise<number> {
|
|
|
139
173
|
return deletedCount;
|
|
140
174
|
}
|
|
141
175
|
|
|
176
|
+
async function pruneInBatches(
|
|
177
|
+
db: DbConnection,
|
|
178
|
+
olderThanDays: number,
|
|
179
|
+
batchSize = PRUNE_BATCH_SIZE,
|
|
180
|
+
): Promise<number> {
|
|
181
|
+
const olderThan = getTemporal()
|
|
182
|
+
.Now.instant()
|
|
183
|
+
.subtract({ hours: olderThanDays * 24 });
|
|
184
|
+
let deletedCount = 0;
|
|
185
|
+
for (;;) {
|
|
186
|
+
const batch = await pruneEvents(db, {
|
|
187
|
+
aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
|
|
188
|
+
olderThan,
|
|
189
|
+
batchSize,
|
|
190
|
+
});
|
|
191
|
+
deletedCount += batch.deletedCount;
|
|
192
|
+
if (batch.deletedCount < batchSize) return deletedCount;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
142
196
|
export async function runEscapeHatchRetention(args: {
|
|
143
197
|
readonly db: DbRunner;
|
|
144
198
|
readonly registry: Registry;
|
|
145
199
|
readonly configResolver: ConfigResolver | undefined;
|
|
146
200
|
readonly userId: string;
|
|
201
|
+
readonly batchSize?: number;
|
|
147
202
|
}): Promise<{ readonly deletedCount: number }> {
|
|
148
203
|
const { db } = args;
|
|
149
204
|
if (!isDbConnection(db)) {
|
|
@@ -152,12 +207,9 @@ export async function runEscapeHatchRetention(args: {
|
|
|
152
207
|
|
|
153
208
|
if (!args.registry.getEntity(COMPLIANCE_PROFILE_ENTITY)) {
|
|
154
209
|
const olderThanDays = await resolveRetentionDays({ ...args, db });
|
|
155
|
-
|
|
156
|
-
aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
|
|
157
|
-
olderThanDays,
|
|
158
|
-
});
|
|
159
|
-
return { deletedCount };
|
|
210
|
+
return { deletedCount: await pruneInBatches(db, olderThanDays, args.batchSize) };
|
|
160
211
|
}
|
|
161
212
|
|
|
162
|
-
|
|
213
|
+
const operatorRetentionDays = await resolveExplicitRetentionDays({ ...args, db });
|
|
214
|
+
return { deletedCount: await pruneByTenantProfiles(db, operatorRetentionDays) };
|
|
163
215
|
}
|
|
@@ -7,23 +7,26 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
makeDispatchSystemWrite,
|
|
10
|
-
|
|
10
|
+
makeDispatchWriteAs,
|
|
11
|
+
type RoleDispatchArgs,
|
|
11
12
|
} from "@cosmicdrift/kumiko-framework/api";
|
|
12
13
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
13
14
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
14
15
|
import {
|
|
15
16
|
ConfigScopes,
|
|
16
17
|
createSystemUser,
|
|
18
|
+
type Registry,
|
|
17
19
|
type TenantId,
|
|
18
20
|
type WriteResult,
|
|
19
21
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
22
|
import type { Dispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
23
|
+
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
21
24
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
22
25
|
import type { Redis } from "ioredis";
|
|
23
26
|
// kumiko-lint-ignore cross-feature-import bootstrap sets per-tenant config through the config feature's write handler
|
|
24
27
|
import { ConfigHandlers } from "../config/constants.js";
|
|
25
28
|
// kumiko-lint-ignore cross-feature-import bootstrap compares against the stored config row to stay idempotent
|
|
26
|
-
import { findConfigRow } from "../config/write-helpers.js";
|
|
29
|
+
import { configRowHoldsValue, findConfigRow } from "../config/write-helpers.js";
|
|
27
30
|
// kumiko-lint-ignore cross-feature-import bootstrap provisions tenant + invitation rows owned by the tenant feature
|
|
28
31
|
import { TenantHandlers } from "../tenant/constants.js";
|
|
29
32
|
// kumiko-lint-ignore cross-feature-import bootstrap provisions tenant + invitation rows owned by the tenant feature
|
|
@@ -33,14 +36,12 @@ import { tenantMembershipsTable } from "../tenant/membership-table.js";
|
|
|
33
36
|
// kumiko-lint-ignore cross-feature-import bootstrap checks for an existing tenant
|
|
34
37
|
import { tenantTable } from "../tenant/schema/tenant.js";
|
|
35
38
|
// kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
|
|
36
|
-
import type {
|
|
39
|
+
import type { GlobalRole } from "../user/constants.js";
|
|
37
40
|
// kumiko-lint-ignore cross-feature-import bootstrap skips already activated users
|
|
38
41
|
import { userTable } from "../user/schema/user.js";
|
|
39
42
|
import { AuthHandlers } from "./constants.js";
|
|
40
43
|
import { hasLiveInviteToken } from "./invite-token-store.js";
|
|
41
44
|
|
|
42
|
-
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
43
|
-
|
|
44
45
|
export type BootstrapInvite = {
|
|
45
46
|
readonly email: string;
|
|
46
47
|
/** Membership role in the tenant, validated like a tenant-admin invite
|
|
@@ -55,9 +56,7 @@ export type BootstrapSeedDeps = {
|
|
|
55
56
|
/** Writes as the system user with the given tenant roles in the seeded
|
|
56
57
|
* tenant; use it when the app gates tenant data on its own roles rather
|
|
57
58
|
* than SystemAdmin. */
|
|
58
|
-
readonly dispatchWriteAs: (
|
|
59
|
-
args: Pick<SystemDispatchArgs, "handlerQn" | "payload"> & { readonly roles: readonly string[] },
|
|
60
|
-
) => Promise<WriteResult>;
|
|
59
|
+
readonly dispatchWriteAs: (args: Omit<RoleDispatchArgs, "tenantId">) => Promise<WriteResult>;
|
|
61
60
|
};
|
|
62
61
|
|
|
63
62
|
export type BootstrapConfigValue = string | number | boolean;
|
|
@@ -96,6 +95,10 @@ export type BootstrapDeps = {
|
|
|
96
95
|
readonly db: DbConnection;
|
|
97
96
|
readonly redis: Redis;
|
|
98
97
|
readonly dispatcher: Dispatcher;
|
|
98
|
+
/** With these, a rerun can compare `encrypted`/`piiEncrypted` config keys by
|
|
99
|
+
* their decrypted value; without them such keys are rewritten on every run. */
|
|
100
|
+
readonly registry?: Registry;
|
|
101
|
+
readonly configCipher?: EnvelopeCipher;
|
|
99
102
|
};
|
|
100
103
|
|
|
101
104
|
export type BootstrapTenantOutcome = "created" | "exists";
|
|
@@ -216,7 +219,18 @@ async function applyTenantConfig(
|
|
|
216
219
|
const applied: string[] = [];
|
|
217
220
|
for (const [key, value] of Object.entries(tenant.config ?? {})) {
|
|
218
221
|
const existing = await findConfigRow(deps.db, key, tenant.id, null);
|
|
219
|
-
|
|
222
|
+
const serialized = JSON.stringify(value);
|
|
223
|
+
if (
|
|
224
|
+
await configRowHoldsValue(
|
|
225
|
+
existing,
|
|
226
|
+
deps.registry?.getConfigKey(key),
|
|
227
|
+
deps.configCipher,
|
|
228
|
+
key,
|
|
229
|
+
serialized,
|
|
230
|
+
)
|
|
231
|
+
) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
220
234
|
const result = await deps.dispatcher.write(
|
|
221
235
|
ConfigHandlers.set,
|
|
222
236
|
{ key, value, scope: ConfigScopes.tenant },
|
|
@@ -266,6 +280,11 @@ async function decideInvite(
|
|
|
266
280
|
return "role-mismatch";
|
|
267
281
|
}
|
|
268
282
|
if (invitation.status === INVITATION_STATUS.accepted) return "active";
|
|
283
|
+
// The plan dropped a global role (e.g. an admin moved to tenants[].invites):
|
|
284
|
+
// the stored invitation would still grant it on accept, so it is re-issued.
|
|
285
|
+
if (storedGlobalRoles.some((role) => !invite.globalRoles.some((planned) => planned === role))) {
|
|
286
|
+
return "resent";
|
|
287
|
+
}
|
|
269
288
|
if (await hasLiveInviteToken(deps.redis, invitation.id)) return "pending";
|
|
270
289
|
return "resent";
|
|
271
290
|
}
|
|
@@ -280,6 +299,7 @@ export async function bootstrapTenants(
|
|
|
280
299
|
): Promise<BootstrapReport> {
|
|
281
300
|
const plannedInvites = planInvites(plan);
|
|
282
301
|
const dispatchSystemWrite = makeDispatchSystemWrite(deps.dispatcher);
|
|
302
|
+
const dispatchWriteAsRoles = makeDispatchWriteAs(deps.dispatcher);
|
|
283
303
|
|
|
284
304
|
const tenants: Array<BootstrapReport["tenants"][number]> = [];
|
|
285
305
|
for (const tenant of plan.tenants) {
|
|
@@ -290,8 +310,7 @@ export async function bootstrapTenants(
|
|
|
290
310
|
tenantId: tenant.id,
|
|
291
311
|
db: deps.db,
|
|
292
312
|
dispatchSystemWrite,
|
|
293
|
-
dispatchWriteAs: ({
|
|
294
|
-
deps.dispatcher.write(handlerQn, payload, createSystemUser(tenant.id, roles)),
|
|
313
|
+
dispatchWriteAs: (args) => dispatchWriteAsRoles({ ...args, tenantId: tenant.id }),
|
|
295
314
|
});
|
|
296
315
|
}
|
|
297
316
|
const configApplied = await applyTenantConfig(deps, tenant);
|
|
@@ -1,4 +1,59 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Magic-link mail expiry and app name use the template locale",
|
|
6
|
+
"detail": "The expiry text, the app-name fallback and the notify locale follow the locale the auth mail template actually renders in, while the link keeps the request locale."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "systemInviteCreate takes an optional locale tag",
|
|
12
|
+
"detail": "The payload accepts a locale (BCP 47 tag) that wins over the request locale when rendering the invitation mail, so system callers such as the waitlist can invite in the recipient's language."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.365.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "Auth mails carry designer labels",
|
|
18
|
+
"detail": "The feature registers <feature>:feature.label and notify labels for password-reset, account-unlock, email-verification, signup-activation and invite, so the document designer lists them by name. German and Spanish ship in the locale packages."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.364.0",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "A loginUrl builder that throws no longer crashes the app tree",
|
|
24
|
+
"detail": "The auth gate resolves the login redirect once per location change and, when the builder returns an invalid URL or throws, logs the error and shows the built-in login screen instead of throwing during render."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.364.0",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "How to send an auth mail after the render*Email helpers were removed in 0.363.0",
|
|
30
|
+
"detail": "No runtime change. The 0.363.0 breaking entry lists renderActivationEmail, renderResetPasswordEmail, renderUnlockAccountEmail, renderVerifyEmail and renderInviteEmail as removed but does not say how app code that rendered one of these mails itself now sends it. This entry adds that recipe.",
|
|
31
|
+
"migration": "App code that rendered an auth mail itself and passed the result as `data`, for example `notify(\"auth-email-password:signup-activation\", { route: { email }, data: renderActivationEmail({ url, expiresAt, locale, appName }), priority: \"critical\" })`, breaks twice since 0.363.0: the helper is gone and the notification type is now `auth-email-password:notify:signup-activation`. The stored template renders subject and body; the app only passes template variables.\nFor a signup activation link, call `issueSignupActivation({ redis, notify: ctx.notify, email, appUrl, appName, locale, timeZone, tokenTtlMinutes, onTokenStored })` from `@cosmicdrift/kumiko-bundled-features/auth-email-password` (only redis, notify, email and appUrl are required). It drops any live signup token for the email, stores a new one under the keys signup-confirm reads and sends the mail. `appUrl` is the bare activation page; a function `(locale) => url` picks the page per locale.\nIf the app stores the token itself and only needs the mail, call `ctx.notify(AuthMailNotificationTypes.signupActivation, { route: { email }, data: { appName, actionUrl, validFor, validUntil }, locale, priority: \"critical\" })`. `AuthMailNotificationTypes` comes from `@cosmicdrift/kumiko-bundled-features/auth-email-password`. All four variables are required strings: ctx.notify checks them against the declared template variables and throws an InternalError on a missing or invalid one, which fails the write. `actionUrl` is the page URL with `token=<encodeURIComponent(token)>` appended as a query parameter and must be an http, https or mailto URL. `validFor` and `validUntil` come from `tokenMailExpiry({ expiresAt, issuedAt, timeZone }, locale)` in `@cosmicdrift/kumiko-bundled-features/auth-email-password/email-templates`. Only send a `locale` that has a template for the type, because the renderer has no locale fallback and a locale without a template sends nothing: use `resolveMailLocale(locale)` when `hasMailTranslationKey(resolveMailLocale(locale), \"auth.mail.activation.subject\")` is true (both from `@cosmicdrift/kumiko-framework/i18n`), otherwise `\"en\"`. The other mails use the subject keys `auth.mail.reset.subject`, `auth.mail.unlock.subject`, `auth.mail.verify.subject` and `auth.mail.invite.subject`.\nThe other auth mails work the same way with `AuthMailNotificationTypes.passwordReset`, `.accountUnlock`, `.emailVerification` and `.invite`; the invite mail also needs a `role` variable."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.364.0",
|
|
35
|
+
"type": "fix",
|
|
36
|
+
"title": "seedAdminGuarded rewrites the admin email only when its blind index is missing or stale",
|
|
37
|
+
"detail": "rewriteSeededUserEmail returns early when the stored email blind index already matches the expected value, so repeated boots no longer re-encrypt the email and grow the user event stream. The row version is now checked with a type guard instead of a cast."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"version": "0.364.0",
|
|
41
|
+
"type": "fix",
|
|
42
|
+
"title": "Bootstrap reruns no longer rewrite encrypted config or restore dropped global roles",
|
|
43
|
+
"detail": "Declarative bootstrap compares encrypted and piiEncrypted config keys by their decrypted value, so a matching rerun writes nothing, and re-issues an invitation whose stored global roles are no longer in the plan. makeDispatchWriteAs is exported from the framework api."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"version": "0.364.0",
|
|
47
|
+
"type": "fix",
|
|
48
|
+
"title": "Email payload buckets ignore plus-tags and case; auth requests are capped at 3 per 15 minutes",
|
|
49
|
+
"detail": "additionalRateLimits accepts normalize: email, which lowercases and strips the plus-tag before hashing, and bucket keys are scoped by tenant and handler. Signup and token requests (reset, unlock, verification) and the waitlist default use it with a 3-request, 900-second window."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"version": "0.364.0",
|
|
53
|
+
"type": "fix",
|
|
54
|
+
"title": "Password change and logout work for members with only an app role or TenantAdmin",
|
|
55
|
+
"detail": "Both handlers required User, Admin or SystemAdmin and answered 403 access_denied for invited members holding a single custom role. They are now open to every signed-in user, like the other self-service handlers; anonymous callers are still rejected."
|
|
56
|
+
},
|
|
2
57
|
{
|
|
3
58
|
"version": "0.363.0",
|
|
4
59
|
"type": "breaking",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PayloadRateLimitOption } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
|
|
3
|
+
// The bucket counts across all IPs, so a long window would let an attacker who
|
|
4
|
+
// knows an address lock its owner out of recovery/signup for the whole window.
|
|
5
|
+
// A short one still throttles mail bombing but clears within minutes.
|
|
6
|
+
export const AUTH_EMAIL_RATE_LIMIT_WINDOW_SECONDS = 15 * 60;
|
|
7
|
+
export const AUTH_EMAIL_RATE_LIMIT_MAX_REQUESTS = 3;
|
|
8
|
+
|
|
9
|
+
export const AUTH_EMAIL_PAYLOAD_RATE_LIMITS: readonly PayloadRateLimitOption[] = [
|
|
10
|
+
{
|
|
11
|
+
per: { payloadField: "email" },
|
|
12
|
+
limit: AUTH_EMAIL_RATE_LIMIT_MAX_REQUESTS,
|
|
13
|
+
windowSeconds: AUTH_EMAIL_RATE_LIMIT_WINDOW_SECONDS,
|
|
14
|
+
normalize: "email",
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "./handlers/signup-request.write.js";
|
|
31
31
|
import { createSwitchTenantMfaGateHandler } from "./handlers/switch-tenant-mfa-gate.write.js";
|
|
32
32
|
import { createVerifyEmailHandler } from "./handlers/verify-email.write.js";
|
|
33
|
+
import { AUTH_EMAIL_PASSWORD_MAIL_I18N } from "./mail-i18n.js";
|
|
33
34
|
import {
|
|
34
35
|
AUTH_MAIL_VARIABLES,
|
|
35
36
|
authMailLocalizedTemplate,
|
|
@@ -113,13 +114,8 @@ export type AccountLockoutOptions = {
|
|
|
113
114
|
readonly lockoutDurationMinutes?: number;
|
|
114
115
|
};
|
|
115
116
|
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// kommt aus ctx.redis — tokenTtlMinutes ist der einzige Knopf
|
|
119
|
-
// (Token-Material ist generateToken() = 256 Bit randomBytes, fest;
|
|
120
|
-
// Memory feedback_no_options_without_need: keine Knöpfe ohne Bedarf).
|
|
121
|
-
// Anders als reset/verify gibt's kein hmacSecret hier, weil der Token
|
|
122
|
-
// opaque random ist (Redis ist Source of Truth).
|
|
117
|
+
// Unlike reset/verify there is no hmacSecret: the signup token is opaque random
|
|
118
|
+
// (256 bit) and Redis is the source of truth, so tokenTtlMinutes is the only knob.
|
|
123
119
|
export type SignupOptions = SignupRequestOptions;
|
|
124
120
|
|
|
125
121
|
// Tenant-Invite Magic-Link. Wenn gesetzt, registriert das Feature die
|
|
@@ -207,6 +203,7 @@ export function createAuthEmailPasswordFeature(
|
|
|
207
203
|
// of silently dropping the mail.
|
|
208
204
|
if (sendsMagicLinkMail(opts)) r.requires("delivery");
|
|
209
205
|
r.envSchema(authEmailPasswordEnvSchema);
|
|
206
|
+
r.translations({ keys: AUTH_EMAIL_PASSWORD_MAIL_I18N });
|
|
210
207
|
|
|
211
208
|
// Declared unconditionally: apps also issue the activation link from their
|
|
212
209
|
// own flows (issueSignupActivation) without enabling the signup option.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
access,
|
|
3
2
|
createSystemUser,
|
|
4
3
|
defineWriteHandler,
|
|
5
4
|
SYSTEM_TENANT_ID,
|
|
@@ -20,7 +19,12 @@ export const changePasswordWrite = defineWriteHandler({
|
|
|
20
19
|
oldPassword: z.string().min(1),
|
|
21
20
|
newPassword: passwordSchema,
|
|
22
21
|
}),
|
|
23
|
-
access: {
|
|
22
|
+
access: {
|
|
23
|
+
openToAll: {
|
|
24
|
+
reason:
|
|
25
|
+
"Self-service: acts only on event.user.id and re-checks the caller's current password; invited members hold only an app role, so a fixed role list would lock them out.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
24
28
|
escapeHatch: {
|
|
25
29
|
grants: ["systemIdentity"],
|
|
26
30
|
reason:
|
package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts
CHANGED
|
@@ -121,9 +121,8 @@ export function createInviteAcceptWithLoginHandler(opts: InviteAcceptWithLoginOp
|
|
|
121
121
|
escapeHatch: {
|
|
122
122
|
grants: ["unsafeRaw"],
|
|
123
123
|
reason:
|
|
124
|
-
"Anonymous invite-accept has no session in the invited tenant yet —
|
|
125
|
-
"
|
|
126
|
-
"Also reads the pending invitation by id (the invitee is not yet a member of the " +
|
|
124
|
+
"Anonymous invite-accept has no session in the invited tenant yet — reads the pending " +
|
|
125
|
+
"invitation by id (the invitee is not yet a member of the " +
|
|
127
126
|
"invitation's tenant), adds the membership and accepts the invitation in the " +
|
|
128
127
|
"invitation's tenant, which differs from the caller's tenant, and reads the MFA " +
|
|
129
128
|
"enrollment of that tenant via the mfaStatusChecker callback.",
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type WriteFailure,
|
|
33
33
|
writeFailure,
|
|
34
34
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
35
|
+
import { isValidLocaleTag } from "@cosmicdrift/kumiko-framework/i18n";
|
|
35
36
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
36
37
|
import * as z from "zod";
|
|
37
38
|
// kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link-Pattern), DB-row-owner ist tenant-feature
|
|
@@ -50,7 +51,7 @@ import {
|
|
|
50
51
|
unassignableMembershipRoleError,
|
|
51
52
|
} from "../../tenant/membership-roles.js";
|
|
52
53
|
// kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
|
|
53
|
-
import { GLOBAL_ROLE_OPTIONS } from "../../user/constants.js";
|
|
54
|
+
import { GLOBAL_ROLE_OPTIONS, type GlobalRole } from "../../user/constants.js";
|
|
54
55
|
// kumiko-lint-ignore cross-feature-import invitee lookup by email reads the user row
|
|
55
56
|
import { userTable } from "../../user/schema/user.js";
|
|
56
57
|
import { AUTH_INVITE_DEFAULT_TTL_MINUTES } from "../constants.js";
|
|
@@ -60,8 +61,6 @@ import { invalidateExistingInviteToken, storeInviteToken } from "../invite-token
|
|
|
60
61
|
import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.js";
|
|
61
62
|
import { AuthMailNotificationTypes } from "../mail-notifications.js";
|
|
62
63
|
|
|
63
|
-
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
64
|
-
|
|
65
64
|
const InviteCreateSchema = z.object({
|
|
66
65
|
email: z.email(),
|
|
67
66
|
role: z.string().min(1).max(50),
|
|
@@ -116,6 +115,7 @@ type InviteIssueRequest = {
|
|
|
116
115
|
readonly email: string;
|
|
117
116
|
readonly role: string;
|
|
118
117
|
readonly globalRoles: readonly GlobalRole[];
|
|
118
|
+
readonly locale?: string | undefined;
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
function checkAssignableInviteRole(
|
|
@@ -239,7 +239,7 @@ async function issueInvitation(
|
|
|
239
239
|
const token = generateToken();
|
|
240
240
|
await storeInviteToken(ctx.redis, { invitationId, token, ttlSeconds });
|
|
241
241
|
|
|
242
|
-
const locale = resolveHandlerMailLocale(ctx, opts.locale);
|
|
242
|
+
const locale = request.locale ?? resolveHandlerMailLocale(ctx, opts.locale);
|
|
243
243
|
|
|
244
244
|
await dispatchMagicLinkMail(
|
|
245
245
|
ctx.notify,
|
|
@@ -317,6 +317,8 @@ const SystemInviteCreateSchema = z.object({
|
|
|
317
317
|
email: z.email(),
|
|
318
318
|
role: z.string().min(1).max(50),
|
|
319
319
|
globalRoles: z.array(z.enum(GLOBAL_ROLE_OPTIONS)).max(GLOBAL_ROLE_OPTIONS.length).default([]),
|
|
320
|
+
// Preferred over the request locale: provisioning callers know the invitee's language.
|
|
321
|
+
locale: z.string().max(35).refine(isValidLocaleTag, "invalid locale tag").optional(),
|
|
320
322
|
});
|
|
321
323
|
|
|
322
324
|
// System-only sibling of invite-create: the only path that can put global
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
|
|
4
4
|
// Logout — JWT is stateless, so server-side we only return OK. A future
|
|
@@ -7,7 +7,12 @@ import * as z from "zod";
|
|
|
7
7
|
export const logoutWrite = defineWriteHandler({
|
|
8
8
|
name: "logout",
|
|
9
9
|
schema: z.object({}),
|
|
10
|
-
access: {
|
|
10
|
+
access: {
|
|
11
|
+
openToAll: {
|
|
12
|
+
reason:
|
|
13
|
+
"Self-service: touches no data, any signed-in member must be able to sign out regardless of which role they hold.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
11
16
|
description:
|
|
12
17
|
"Acknowledges a sign-out for the authenticated caller so the client can discard its token; the JWT is stateless, so no server-side session state changes here.",
|
|
13
18
|
perform: stepsPipeline(({ r }) => [
|
|
@@ -25,6 +25,7 @@ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
25
25
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
26
26
|
import * as z from "zod";
|
|
27
27
|
import { findSignupHandoverProvider, type SignupHandoverBinding } from "../../shared/index.js";
|
|
28
|
+
import { AUTH_EMAIL_PAYLOAD_RATE_LIMITS } from "../email-rate-limits.js";
|
|
28
29
|
import type { AuthMailLocale } from "../email-templates.js";
|
|
29
30
|
import { resolveHandlerMailLocale } from "../magic-link-mail.js";
|
|
30
31
|
import { AUTH_SELF_REGISTRATION_FEATURE } from "../self-registration-toggle.js";
|
|
@@ -79,7 +80,7 @@ export function createSignupRequestHandler(opts: SignupRequestOptions) {
|
|
|
79
80
|
schema: SignupRequestSchema,
|
|
80
81
|
access: { roles: ["anonymous"] },
|
|
81
82
|
rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
|
|
82
|
-
additionalRateLimits:
|
|
83
|
+
additionalRateLimits: AUTH_EMAIL_PAYLOAD_RATE_LIMITS,
|
|
83
84
|
description:
|
|
84
85
|
"Starts magic-link self-registration by mailing a fresh activation link to an address and invalidating any link still outstanding for it; the answer looks the same whether or not the address is already registered.",
|
|
85
86
|
escapeHatch: { reason: HANDOVER_GRANT_VERIFY_REASON, grants: ["unsafeRaw"] },
|
package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts
CHANGED
|
@@ -21,7 +21,7 @@ const SwitchTenantMfaGateSchema = z.object({
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
export type SwitchTenantMfaGateData =
|
|
24
|
-
| { readonly kind: "mfa-gate-clear" }
|
|
24
|
+
| { readonly kind: "mfa-gate-clear"; readonly roles: readonly string[] }
|
|
25
25
|
| NonNullable<Awaited<ReturnType<typeof gateEnforceMfa>>>;
|
|
26
26
|
|
|
27
27
|
// Dispatched by POST /auth/switch-tenant with the system identity. The handler
|
|
@@ -54,7 +54,7 @@ export function createSwitchTenantMfaGateHandler(
|
|
|
54
54
|
);
|
|
55
55
|
const roles = buildSessionRoles(parseRoles(userRow?.roles ?? null), active.membership.roles);
|
|
56
56
|
const mfaGate = await gateEnforceMfa(ctx, opts, userId, tenantId, roles);
|
|
57
|
-
return { isSuccess: true, data: mfaGate ?? { kind: "mfa-gate-clear" } };
|
|
57
|
+
return { isSuccess: true, data: mfaGate ?? { kind: "mfa-gate-clear", roles } };
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
}
|
|
@@ -19,6 +19,7 @@ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
|
19
19
|
import * as z from "zod";
|
|
20
20
|
import { UserQueries } from "../../user/index.js";
|
|
21
21
|
import { type AuthUserRow, parseAuthUserRow } from "../auth-user-row.js";
|
|
22
|
+
import { AUTH_EMAIL_PAYLOAD_RATE_LIMITS } from "../email-rate-limits.js";
|
|
22
23
|
import type { AuthMailLocale } from "../email-templates.js";
|
|
23
24
|
import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.js";
|
|
24
25
|
|
|
@@ -99,7 +100,7 @@ export function createTokenRequestHandler<TName extends string, TSuccessKind ext
|
|
|
99
100
|
schema: RequestTokenSchema,
|
|
100
101
|
access: { roles: ["anonymous"] },
|
|
101
102
|
rateLimit: { per: "ip+handler", limit: 10, windowSeconds: 60 },
|
|
102
|
-
additionalRateLimits:
|
|
103
|
+
additionalRateLimits: AUTH_EMAIL_PAYLOAD_RATE_LIMITS,
|
|
103
104
|
escapeHatch: {
|
|
104
105
|
grants: ["systemIdentity"],
|
|
105
106
|
reason:
|