@cosmicdrift/kumiko-samples 0.367.0 → 0.368.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/package.json +1 -1
- package/packages/bundled-features/package.json +16 -2
- package/packages/bundled-features/src/audit/handlers/details.query.ts +1 -1
- package/packages/bundled-features/src/audit/handlers/list.query.ts +1 -1
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +5 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +10 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +10 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-info.query.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/seeding.ts +2 -2
- package/packages/bundled-features/src/auth-foundation/feature.ts +43 -34
- package/packages/bundled-features/src/auth-foundation/index.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/db/queries.ts +5 -2
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-start-preauth.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/handlers/status.query.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-code-verifier.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +1 -1
- package/packages/bundled-features/src/auth-mfa-user-data/index.ts +19 -10
- package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +138 -135
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/projection.ts +3 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +52 -1
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +203 -15
- package/packages/bundled-features/src/cap-counter/changes.json +13 -0
- package/packages/bundled-features/src/cap-counter/feature.ts +84 -73
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +3 -4
- package/packages/bundled-features/src/cap-counter/index.ts +9 -1
- package/packages/bundled-features/src/cap-counter/tables.ts +10 -3
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +1 -2
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +6 -2
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/handlers/inbox.query.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/handlers/mark-all-read.write.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/handlers/unread-count.query.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/tables.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/handlers/for-tenant.query.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/handlers/set-profile.write.ts +1 -2
- package/packages/bundled-features/src/compliance-profiles/resolve-for-tenant.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles/seeding.ts +8 -4
- package/packages/bundled-features/src/compliance-profiles-ops/index.ts +26 -17
- package/packages/bundled-features/src/config/db/queries/resolver.ts +1 -1
- package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +2 -2
- package/packages/bundled-features/src/config/resolver.ts +1 -1
- package/packages/bundled-features/src/config/write-helpers.ts +1 -1
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +9 -7
- package/packages/bundled-features/src/custom-fields/db/queries/field-access.ts +1 -1
- package/packages/bundled-features/src/custom-fields/db/queries/projection.ts +1 -1
- package/packages/bundled-features/src/custom-fields/db/queries/quota.ts +1 -1
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +6 -2
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +1 -1
- package/packages/bundled-features/src/custom-fields/feature.ts +11 -14
- package/packages/bundled-features/src/custom-fields/handlers/clear-custom-field.write.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/set-custom-field.write.ts +2 -2
- package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +4 -4
- package/packages/bundled-features/src/data-retention/resolve-for-tenant.ts +1 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +16 -10
- package/packages/bundled-features/src/delivery/address-opt-out.ts +5 -2
- package/packages/bundled-features/src/delivery/attempt-log-retention.ts +1 -1
- package/packages/bundled-features/src/delivery/channel-context.ts +1 -1
- package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +1 -1
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +1 -1
- package/packages/bundled-features/src/delivery/delivery-service.ts +16 -6
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/handlers/log.query.ts +3 -2
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +1 -1
- package/packages/bundled-features/src/delivery/upsert-preference.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/feature.ts +21 -12
- package/packages/bundled-features/src/derivatives-sharp/index.ts +1 -1
- package/packages/bundled-features/src/document-ingest-foundation/feature.ts +112 -103
- package/packages/bundled-features/src/document-ingest-foundation/forget-extract-with-file-ref.ts +3 -3
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +4 -1
- package/packages/bundled-features/src/feature-toggles/global-feature-state-table.ts +1 -1
- package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +3 -5
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +1 -1
- package/packages/bundled-features/src/file-derivatives/feature.ts +134 -129
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant-by-file-ref.query.ts +1 -1
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/feature.ts +38 -29
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +1 -1
- package/packages/bundled-features/src/file-provider-inmemory/feature.ts +32 -21
- package/packages/bundled-features/src/file-provider-inmemory/index.ts +6 -1
- package/packages/bundled-features/src/file-provider-s3/changes.json +10 -1
- package/packages/bundled-features/src/file-provider-s3/env-feature.ts +62 -0
- package/packages/bundled-features/src/file-provider-s3/feature.ts +107 -73
- package/packages/bundled-features/src/file-provider-s3/index.ts +6 -1
- package/packages/bundled-features/src/file-provider-s3/lib/index.ts +3 -0
- package/packages/bundled-features/src/{files-provider-s3 → file-provider-s3/lib}/s3-provider.ts +19 -19
- package/packages/bundled-features/src/file-provider-s3-env/index.ts +7 -2
- package/packages/bundled-features/src/files-provider-s3/index.ts +11 -3
- package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +1 -1
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +1 -2
- package/packages/bundled-features/src/folders/feature.ts +8 -3
- package/packages/bundled-features/src/folders-user-data/index.ts +27 -18
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +1 -1
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +1 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +3 -2
- package/packages/bundled-features/src/form-draft/feature.ts +6 -1
- package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +2 -2
- package/packages/bundled-features/src/form-draft/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/lookup.ts +1 -1
- package/packages/bundled-features/src/form-draft-user-data/index.ts +19 -10
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +165 -154
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -2
- package/packages/bundled-features/src/inbound-mail-foundation/index.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +5 -2
- package/packages/bundled-features/src/inbound-mail-foundation/projection.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +22 -13
- package/packages/bundled-features/src/inbound-provider-imap/index.ts +6 -1
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +20 -12
- package/packages/bundled-features/src/inbound-provider-inmemory/index.ts +1 -0
- package/packages/bundled-features/src/jobs/db/queries/retention.ts +1 -1
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/detail.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +7 -2
- package/packages/bundled-features/src/jobs/handlers/retry.write.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +1 -1
- package/packages/bundled-features/src/jobs/job-run-logger.ts +11 -10
- package/packages/bundled-features/src/jobs/tenant-job-failure-table.ts +1 -1
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +1 -1
- package/packages/bundled-features/src/ledger/feature.ts +6 -3
- package/packages/bundled-features/src/mail-foundation/feature.ts +57 -46
- package/packages/bundled-features/src/mail-foundation/index.ts +1 -0
- package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +25 -17
- package/packages/bundled-features/src/mail-transport-inmemory/index.ts +6 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +91 -79
- package/packages/bundled-features/src/mail-transport-smtp/index.ts +1 -0
- package/packages/bundled-features/src/managed-pages/handlers/by-slug.query.ts +1 -1
- package/packages/bundled-features/src/managed-pages/handlers/by-tenant-published.query.ts +1 -2
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +1 -2
- package/packages/bundled-features/src/managed-pages/seeding.ts +8 -4
- package/packages/bundled-features/src/metrics/compute.ts +1 -1
- package/packages/bundled-features/src/metrics/filters.ts +1 -1
- package/packages/bundled-features/src/metrics/validate.ts +1 -1
- package/packages/bundled-features/src/notes-history/feature.ts +6 -3
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/notes-history-user-data/hooks.ts +1 -1
- package/packages/bundled-features/src/notes-history-user-data/index.ts +27 -18
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +17 -2
- package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -2
- package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/resolver.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -2
- package/packages/bundled-features/src/personal-access-tokens/roles.ts +8 -6
- package/packages/bundled-features/src/personal-access-tokens/screens.ts +2 -0
- package/packages/bundled-features/src/readiness/feature.ts +25 -16
- package/packages/bundled-features/src/readiness/index.ts +1 -1
- package/packages/bundled-features/src/secrets/db/queries/read.ts +1 -1
- package/packages/bundled-features/src/secrets/feature.ts +8 -3
- package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +2 -2
- package/packages/bundled-features/src/secrets/secrets-context.ts +4 -3
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/detail.query.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -2
- package/packages/bundled-features/src/sessions/handlers/mine.query.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +14 -7
- package/packages/bundled-features/src/sessions/testing.ts +1 -1
- package/packages/bundled-features/src/tags/feature.ts +6 -3
- package/packages/bundled-features/src/template-resolver/api.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/by-slug.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +1 -2
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/seeding.ts +3 -3
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-preview.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-resources.ts +1 -1
- package/packages/bundled-features/src/template-resolver/testing.ts +1 -1
- package/packages/bundled-features/src/template-resolver-user-data/index.ts +23 -14
- package/packages/bundled-features/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/add-member.write.ts +1 -2
- package/packages/bundled-features/src/tenant/handlers/cancel-invitation.write.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/invitations.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/me.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/members.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +2 -1
- package/packages/bundled-features/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -2
- package/packages/bundled-features/src/tenant/invited-membership.ts +2 -2
- package/packages/bundled-features/src/tenant/last-tenant-admin.ts +1 -2
- package/packages/bundled-features/src/tenant/seeding.ts +12 -12
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +1 -2
- package/packages/bundled-features/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -2
- package/packages/bundled-features/src/tenant-lifecycle/lib/revoke-tenant-sessions.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/lifecycle-gate.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +3 -3
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +10 -5
- package/packages/bundled-features/src/tenant-lifecycle/testing.ts +3 -3
- package/packages/bundled-features/src/tenant-settings/feature.ts +34 -29
- package/packages/bundled-features/src/tier-engine/feature.ts +426 -428
- package/packages/bundled-features/src/tier-engine/handlers/get-tenant-tier.query.ts +2 -3
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +5 -2
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +1 -1
- package/packages/bundled-features/src/user/handlers/create.write.ts +1 -2
- package/packages/bundled-features/src/user/handlers/find-for-auth.query.ts +1 -1
- package/packages/bundled-features/src/user/handlers/list.query.ts +1 -1
- package/packages/bundled-features/src/user/handlers/update-roles.ts +2 -1
- package/packages/bundled-features/src/user/index.ts +5 -1
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -2
- package/packages/bundled-features/src/user/principal-status.ts +13 -3
- package/packages/bundled-features/src/user/resolve-display-names.ts +1 -1
- package/packages/bundled-features/src/user/seeding.ts +5 -5
- package/packages/bundled-features/src/user-data-rights/audit-download.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/db/queries/export-jobs.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-job.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/export-status.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +1 -2
- package/packages/bundled-features/src/user-data-rights/handlers/run-forget-cleanup.write.ts +3 -3
- package/packages/bundled-features/src/user-data-rights/lib/deny-if-target-outside-admin-tenant.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +7 -2
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +7 -3
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +11 -5
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +10 -5
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +1 -2
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +1 -1
- package/packages/bundled-features/src/waitlist/lib.ts +5 -2
- package/packages/bundled-features/src/workflow-runner/event-subscriber.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +58 -47
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/index.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/pending-projection.ts +1 -1
- package/samples/apps/admin-console/src/run-config.ts +2 -2
- package/samples/apps/cap-billing-demo/package.json +3 -0
- package/samples/apps/cap-billing-demo/src/feature.ts +2 -2
- package/samples/apps/cap-billing-demo/src/run-config.ts +8 -8
- package/samples/apps/marketing-demo/src/app/seed.ts +1 -1
- package/samples/apps/marketing-demo/src/db/queries/seed-counts.ts +1 -1
- package/samples/apps/showcase/package.json +3 -0
- package/samples/apps/showcase/src/app/seed-items.ts +1 -1
- package/samples/apps/styleguide/src/app/seed.ts +2 -2
- package/samples/apps/styleguide/src/features/ai-settings/seed.ts +2 -2
- package/samples/apps/ui-walkthrough/scripts/emit-e2e-data.ts +1 -1
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +1 -1
- package/samples/apps/ui-walkthrough/src/app/seed-rental.ts +2 -2
- package/samples/apps/ui-walkthrough/src/app/seed-vehicles.ts +1 -1
- package/samples/apps/ui-walkthrough/src/app/seed.ts +1 -1
- package/samples/apps/ui-walkthrough/src/db/queries/row-counts.ts +1 -1
- package/samples/apps/ui-walkthrough/src/db/queries/task-counts.ts +1 -1
- package/samples/apps/use-all-bundled/package.json +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +2 -2
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +1 -1
- package/samples/apps/use-all-bundled/src/app/cap-overview-caps.ts +2 -2
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +1 -1
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +1 -1
- package/samples/apps/use-all-bundled/src/app/seed.ts +11 -5
- package/samples/apps/use-all-bundled/src/run-config.ts +55 -56
- package/samples/apps/user-data-rights-demo/README.md +2 -2
- package/samples/apps/user-data-rights-demo/package.json +3 -0
- package/samples/apps/user-data-rights-demo/src/run-config.ts +7 -7
- package/samples/apps/workspaces/package.json +3 -0
- package/samples/recipes/access-control/package.json +3 -0
- package/samples/recipes/anonymous-access/package.json +3 -0
- package/samples/recipes/anonymous-access-multitenant/package.json +3 -0
- package/samples/recipes/apex-landing/package.json +1 -0
- package/samples/recipes/apex-surface-auth/package.json +3 -0
- package/samples/recipes/apex-surface-auth/src/feature.ts +2 -2
- package/samples/recipes/api-worker-split/package.json +3 -0
- package/samples/recipes/api-worker-split/src/feature.ts +1 -2
- package/samples/recipes/auth-claims/package.json +3 -0
- package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
- package/samples/recipes/auth-mfa/package.json +3 -0
- package/samples/recipes/basic-entity/package.json +3 -0
- package/samples/recipes/chat-channels/package.json +3 -0
- package/samples/recipes/compliance-profiles-demo/package.json +3 -0
- package/samples/recipes/cross-feature-events/package.json +3 -1
- package/samples/recipes/cross-feature-events/src/feature.ts +1 -1
- package/samples/recipes/crypto-shredding-hr/package.json +3 -0
- package/samples/recipes/currencies-global/package.json +3 -0
- package/samples/recipes/currencies-per-tenant/package.json +3 -0
- package/samples/recipes/custom-fields-basic/package.json +3 -0
- package/samples/recipes/custom-handlers/package.json +3 -0
- package/samples/recipes/dashboard-stats/package.json +3 -0
- package/samples/recipes/delivery-notifications/package.json +3 -0
- package/samples/recipes/e2e-generator/package.json +3 -0
- package/samples/recipes/embedded/package.json +3 -0
- package/samples/recipes/embedded-entity-form/package.json +1 -0
- package/samples/recipes/encrypted-tenant-config/README.md +1 -1
- package/samples/recipes/encrypted-tenant-config/package.json +3 -0
- package/samples/recipes/error-contract/package.json +3 -0
- package/samples/recipes/event-sourcing/package.json +3 -1
- package/samples/recipes/event-sourcing/src/feature.ts +16 -20
- package/samples/recipes/event-sourcing/tsconfig.json +2 -6
- package/samples/recipes/feature-toggles/package.json +3 -0
- package/samples/recipes/field-access/package.json +3 -0
- package/samples/recipes/files-post-processing/package.json +1 -0
- package/samples/recipes/golden-path/README.md +30 -0
- package/samples/recipes/golden-path/package.json +11 -0
- package/samples/recipes/golden-path/src/feature.ts +70 -0
- package/samples/recipes/idempotency/package.json +3 -0
- package/samples/recipes/inbound-mail-triage/package.json +3 -0
- package/samples/recipes/lane-routing/package.json +3 -0
- package/samples/recipes/ledger-banking/package.json +3 -0
- package/samples/recipes/ledger-invoicing/package.json +3 -0
- package/samples/recipes/legal-pages/package.json +3 -0
- package/samples/recipes/lifecycle-hooks/package.json +3 -0
- package/samples/recipes/managed-config/package.json +3 -0
- package/samples/recipes/managed-pages/package.json +3 -0
- package/samples/recipes/notes-history-basic/package.json +3 -0
- package/samples/recipes/ownership/package.json +3 -0
- package/samples/recipes/pdf-audit-export/package.json +1 -0
- package/samples/recipes/pipeline-basics/package.json +3 -0
- package/samples/recipes/public-share-token/package.json +3 -0
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/rate-limiting/package.json +3 -0
- package/samples/recipes/realtime-sse/package.json +3 -0
- package/samples/recipes/record-detail-layout/package.json +1 -0
- package/samples/recipes/reference-data/package.json +3 -0
- package/samples/recipes/relations/package.json +3 -0
- package/samples/recipes/screens-nav/package.json +3 -0
- package/samples/recipes/search/package.json +3 -0
- package/samples/recipes/secrets-demo/package.json +3 -0
- package/samples/recipes/security-baseline/src/feature.ts +2 -2
- package/samples/recipes/session-revocation/README.md +1 -1
- package/samples/recipes/session-revocation/package.json +3 -0
- package/samples/recipes/session-revocation/src/feature.ts +1 -1
- package/samples/recipes/state-machine/package.json +3 -1
- package/samples/recipes/state-machine/src/entities/invoice.ts +2 -2
- package/samples/recipes/state-machine/src/events.ts +19 -0
- package/samples/recipes/state-machine/src/feature.ts +25 -30
- package/samples/recipes/state-machine/src/handlers/_helpers.ts +13 -14
- package/samples/recipes/state-machine/src/handlers/create.ts +1 -1
- package/samples/recipes/state-machine/src/handlers/transitions.ts +129 -122
- package/samples/recipes/state-machine/src/reducer.ts +1 -1
- package/samples/recipes/state-machine/tsconfig.json +2 -6
- package/samples/recipes/tags-basic/package.json +3 -0
- package/samples/recipes/tenant-isolation/package.json +3 -0
- package/samples/recipes/tenant-settings/package.json +3 -0
- package/samples/recipes/tier-admin/package.json +3 -0
- package/samples/recipes/timezones/package.json +3 -0
- package/samples/recipes/tsconfig.base.json +1 -1
- package/samples/recipes/user-data-rights/package.json +3 -0
- package/samples/recipes/user-data-rights/src/feature.ts +6 -2
- package/samples/recipes/user-profile/package.json +3 -0
- package/samples/recipes/user-profile/src/feature.ts +2 -2
- package/samples/recipes/webhook-step/package.json +3 -0
- package/samples/recipes/wizard-form/package.json +1 -0
- package/samples/recipes/workflow-engine/package.json +3 -0
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +5 -2
- package/samples/recipes/writeform-section/package.json +1 -0
- package/packages/bundled-features/src/file-provider-s3-env/feature.ts +0 -58
- /package/packages/bundled-features/src/{files-provider-s3 → file-provider-s3/lib}/env-helper.ts +0 -0
|
@@ -41,11 +41,12 @@
|
|
|
41
41
|
// **Boot-Dependencies:** config + tenant.
|
|
42
42
|
|
|
43
43
|
// kumiko-lint-ignore app-feature-structure Phase-3 conversion tracked in #2312 (feature.ts > 300 lines)
|
|
44
|
-
import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
45
44
|
import {
|
|
46
45
|
buildEntityTable,
|
|
47
46
|
createEventStoreExecutor,
|
|
48
47
|
createTenantDb,
|
|
48
|
+
fetchOne,
|
|
49
|
+
selectMany,
|
|
49
50
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
50
51
|
import {
|
|
51
52
|
defineEntityCreateHandler,
|
|
@@ -197,454 +198,451 @@ function mergeAlwaysOn(
|
|
|
197
198
|
export function createTierEngineFeature<
|
|
198
199
|
TCaps extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>,
|
|
199
200
|
>(opts: CreateTierEngineOptions<TCaps> = {}): FeatureDefinition {
|
|
200
|
-
return defineFeature(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
201
|
+
return defineFeature(
|
|
202
|
+
TIER_ENGINE_FEATURE,
|
|
203
|
+
(r) => {
|
|
204
|
+
r.describe(
|
|
205
|
+
'Stores a `tier-assignment` entity per tenant (which pricing tier is active) and, when configured with a `TierMap`, registers itself as the `tenantTierResolver` extension so the dispatcher automatically gates `r.toggleable()` features per tenant based on their assigned tier. Call `createTierEngineFeature({ defaultTier, tierMap })` to get full tier composition \u2014 including an `inTransaction` entity hook that atomically writes the default tier when a new tenant is created \u2014 or use `createTierEngineFeature()` without options for storage-only mode when you manage tier assignment yourself via `composeApp`. A SystemAdmin-only `set-tenant-tier` write plus `get-tenant-tier`/`tier-options` reads let an operator assign a tier to ANY tenant manually \u2014 without a billing purchase \u2014 stamping `source: "manual"` so a future Stripe\u2192tier sync won\'t overwrite the grant. Apps surface this via the `tier-admin` screen, which is always registered — in storage-only mode (no `tierMap`) it shows an honest "no tiers configured" message instead of a tier dropdown.',
|
|
206
|
+
);
|
|
207
|
+
r.uiHints({
|
|
208
|
+
displayLabel: "Tier Engine \u00b7 Plan Composition",
|
|
209
|
+
category: "billing",
|
|
210
|
+
recommended: false,
|
|
211
|
+
});
|
|
212
|
+
r.requires({ features: ["config"] });
|
|
213
|
+
r.requires({ features: ["tenant"] });
|
|
214
|
+
|
|
215
|
+
r.entity({ name: "tier-assignment", ...tierAssignmentEntity });
|
|
216
|
+
|
|
217
|
+
// Standard-CRUD via Helper.
|
|
218
|
+
r.writeHandler(
|
|
219
|
+
defineEntityCreateHandler("tier-assignment", tierAssignmentEntity, {
|
|
220
|
+
...writeAccess,
|
|
221
|
+
// Bypasses the feature-gate refresh that set-tenant-tier performs.
|
|
222
|
+
agent: { expose: false },
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
r.writeHandler(
|
|
226
|
+
defineEntityUpdateHandler("tier-assignment", tierAssignmentEntity, {
|
|
227
|
+
...writeAccess,
|
|
228
|
+
agent: { expose: false },
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Reads.
|
|
233
|
+
r.queryHandler(
|
|
234
|
+
defineEntityListHandler("tier-assignment", tierAssignmentEntity, {
|
|
235
|
+
...adminAccess,
|
|
236
|
+
description:
|
|
237
|
+
"Lists the tier-assignment rows visible to the caller with tier name and assignment source; use it to see which plan is stored for a tenant.",
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
r.queryHandler(getActiveTierQuery);
|
|
241
|
+
|
|
242
|
+
// \u2500\u2500 Manueller Tier-Grant (SystemAdmin, ohne Billing) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
243
|
+
// Cross-tenant set + read f\u00fcr den tier-admin-Screen. tier-options liefert
|
|
244
|
+
// dem Client die App-Tier-Namen aus der tierMap-Closure (sonst hartkodiert).
|
|
245
|
+
//
|
|
246
|
+
// onAssigned runs after commit (the direct executor write fires no postSave
|
|
247
|
+
// hook). Late-bound: without tierMap it stays a no-op, the tierMap block
|
|
248
|
+
// below installs the real cache update + publish.
|
|
249
|
+
const onTierAssigned: { fn: (tenantId: TenantId, tier: string) => void } = { fn: () => {} };
|
|
250
|
+
r.writeHandler(
|
|
251
|
+
createSetTenantTierWrite({
|
|
252
|
+
onAssigned: (tenantId, tier) => onTierAssigned.fn(tenantId, tier),
|
|
253
|
+
validTiers: opts.tierMap ? new Set(Object.keys(opts.tierMap)) : undefined,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
r.queryHandler(getTenantTierQuery);
|
|
257
|
+
r.queryHandler(
|
|
258
|
+
defineQueryHandler({
|
|
259
|
+
name: "tier-options",
|
|
260
|
+
description:
|
|
261
|
+
"Returns the tier names the application defines in its tier map; use it to find out which values `set-tenant-tier` accepts.",
|
|
262
|
+
schema: z.object({}),
|
|
263
|
+
access: { roles: ["SystemAdmin"] },
|
|
264
|
+
handler: async () => ({ tiers: opts.tierMap ? Object.keys(opts.tierMap) : [] }),
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Declarative form for the manual grant. SystemAdmin-only, hard-wired
|
|
269
|
+
// (platform-admin authority, not app-configurable). Apps place it only
|
|
270
|
+
// via r.nav("tier-engine:screen:tier-admin"); tier options are built
|
|
271
|
+
// statically from the tierMap closure. Always registered — even in
|
|
272
|
+
// storage-only mode (no tierMap) — so the screen shows an honest
|
|
273
|
+
// "no tiers configured" message instead of vanishing (samples like
|
|
274
|
+
// use-all-bundled deliberately mount storage-only to screenshot that
|
|
275
|
+
// empty state).
|
|
276
|
+
r.translations({ keys: TIER_ENGINE_I18N });
|
|
277
|
+
r.screen({
|
|
278
|
+
id: TIER_ADMIN_SCREEN_ID,
|
|
279
|
+
type: "actionForm",
|
|
280
|
+
handler: TierEngineHandlers.setTenantTier,
|
|
281
|
+
fields: {
|
|
282
|
+
tenantId: {
|
|
283
|
+
type: "reference",
|
|
284
|
+
entity: "tenant:tenant",
|
|
285
|
+
labelField: "name",
|
|
286
|
+
required: true,
|
|
287
|
+
},
|
|
288
|
+
tier: {
|
|
289
|
+
type: "select",
|
|
290
|
+
options: opts.tierMap ? Object.keys(opts.tierMap) : [],
|
|
291
|
+
required: true,
|
|
292
|
+
},
|
|
284
293
|
},
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
294
|
+
layout: {
|
|
295
|
+
sections: [
|
|
296
|
+
{
|
|
297
|
+
fields: ["tenantId", "tier"],
|
|
298
|
+
description: opts.tierMap ? "tier-admin.explainer" : "tier-admin.error.noTiers",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
289
301
|
},
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const trialFeatures: ReadonlySet<string> = opts.trial
|
|
358
|
-
? featuresForTier(tierMap, opts.trial.tier)
|
|
359
|
-
: new Set();
|
|
360
|
-
const nowMs = (): number => getTemporal().Now.instant().epochMilliseconds;
|
|
361
|
-
|
|
362
|
-
// Immediate local visibility on the writing pod; other pods learn via the
|
|
363
|
-
// bus and re-read the row.
|
|
364
|
-
const applyAssignment = (tenantId: TenantId, tier: string): void => {
|
|
365
|
-
takeTicket(tenantId);
|
|
366
|
-
cache.set(tenantId, mergeAlwaysOn(alwaysOnHolder.set, featuresForTier(tierMap, tier)));
|
|
367
|
-
publishAssignmentChange(tenantId);
|
|
368
|
-
};
|
|
369
|
-
onTierAssigned.fn = applyAssignment;
|
|
370
|
-
|
|
371
|
-
// Invalidation: tier-assignment CRUD events (e.g. the billing webhook) update the cache.
|
|
372
|
-
r.hook({
|
|
373
|
-
type: "postSave",
|
|
374
|
-
target: { allOf: "tier-assignment" },
|
|
375
|
-
handler: async (result) => {
|
|
376
|
-
// result.data has tenantId + tier (after entity-update merge)
|
|
377
|
-
const data = result.data as { tenantId?: unknown; tier?: unknown }; // @cast-boundary engine-payload
|
|
378
|
-
// skip: defensive guard on the payload shape. Correct entity events
|
|
379
|
-
// always carry both fields as strings; a malformed payload (a custom
|
|
380
|
-
// handler bug) skips the cache update instead of throwing, because a
|
|
381
|
-
// hook error must not block the lifecycle pipeline (afterCommit,
|
|
382
|
-
// best-effort side effect).
|
|
383
|
-
if (typeof data.tenantId !== "string" || typeof data.tier !== "string") return;
|
|
384
|
-
applyAssignment(data.tenantId as TenantId, data.tier);
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
r.hook({
|
|
388
|
-
type: "postDelete",
|
|
389
|
-
target: { allOf: "tier-assignment" },
|
|
390
|
-
handler: async (payload) => {
|
|
391
|
-
const data = payload.data as { tenantId?: unknown }; // @cast-boundary engine-payload
|
|
392
|
-
// skip: gleiche type-guard semantik wie postSave-hook oben.
|
|
393
|
-
if (typeof data.tenantId !== "string") return;
|
|
394
|
-
const tenantId = data.tenantId as TenantId;
|
|
302
|
+
submitLabel: "tier-admin.submit",
|
|
303
|
+
successMessage: "tier-admin.success",
|
|
304
|
+
cancelTarget: false,
|
|
305
|
+
description: "tier-admin.screen.subtitle",
|
|
306
|
+
access: { roles: ["SystemAdmin"] },
|
|
307
|
+
// Apps place it via r.nav() (see above) — no nav area to resolve in
|
|
308
|
+
// isolation.
|
|
309
|
+
dormant: true,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ───────────────────────────────────────────────────────────────────
|
|
313
|
+
// Resolver-extension (only when tierMap is configured)
|
|
314
|
+
// ───────────────────────────────────────────────────────────────────
|
|
315
|
+
// skip: ohne tierMap ist die feature nur Storage (legacy back-compat
|
|
316
|
+
// zu `tierEngineFeature`). Resolver-extension + invalidation-hooks
|
|
317
|
+
// brauchen die tierMap zum Mapping tier-name → feature-set.
|
|
318
|
+
if (!opts.tierMap) return;
|
|
319
|
+
|
|
320
|
+
const tierMap = opts.tierMap;
|
|
321
|
+
|
|
322
|
+
// Closure state: per-tenant cache of effective sets. Hooks keep the
|
|
323
|
+
// cache current during the process lifetime; build() in the extension
|
|
324
|
+
// pickup pre-loads existing assignments at boot.
|
|
325
|
+
const cache = new Map<TenantId, ReadonlySet<string>>();
|
|
326
|
+
// The alwaysOn set is computed in build(deps) from registry.features (all
|
|
327
|
+
// non-toggleable features). Hooks need it for mergeAlwaysOn calls — late
|
|
328
|
+
// bound via a mutable holder, filled before any request (build runs
|
|
329
|
+
// pre-listen via the runDevApp/runProdApp pickup).
|
|
330
|
+
const alwaysOnHolder: { set: ReadonlySet<string> } = { set: new Set() };
|
|
331
|
+
// Bus handed to build(); hooks fire outside build and publish through it.
|
|
332
|
+
const busHolder: { bus: CacheSyncBus | undefined } = { bus: undefined };
|
|
333
|
+
// Out-of-order guard: every cache mutation or reload start takes a ticket; a
|
|
334
|
+
// reload result is applied only while its ticket is still the tenant's latest.
|
|
335
|
+
let ticketCounter = 0;
|
|
336
|
+
const latestTicket = new Map<TenantId, number>();
|
|
337
|
+
const takeTicket = (tenantId: TenantId): number => {
|
|
338
|
+
ticketCounter += 1;
|
|
339
|
+
latestTicket.set(tenantId, ticketCounter);
|
|
340
|
+
return ticketCounter;
|
|
341
|
+
};
|
|
342
|
+
const publishAssignmentChange = (tenantId: TenantId): void => {
|
|
343
|
+
const message: TierAssignmentSyncMessage = { tenantId };
|
|
344
|
+
// The writer already updated its own cache; only other pods need to re-read.
|
|
345
|
+
busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message, { remoteOnly: true });
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Trial: derived from time (tenant.inserted_at ≈ signup) and checked live
|
|
349
|
+
// at the feature gate — NOT in the resolver cache. The resolver is
|
|
350
|
+
// boot-cached and synchronous, so it sees neither fresh signups nor the
|
|
351
|
+
// passage of time; the trial status changes over time and applies from
|
|
352
|
+
// second 1 after signup. That is why it lives as an async trialGate
|
|
353
|
+
// (below, attached at the end of build) which the dispatcher consults
|
|
354
|
+
// only on the disabled path. trialFeatures = fixed feature set of the
|
|
355
|
+
// trial tier.
|
|
356
|
+
if (opts.trial && !tierMap[opts.trial.tier]) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`tier-engine: trial.tier "${opts.trial.tier}" is not a key in tierMap — trial would unlock no features`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const trialFeatures: ReadonlySet<string> = opts.trial
|
|
362
|
+
? featuresForTier(tierMap, opts.trial.tier)
|
|
363
|
+
: new Set();
|
|
364
|
+
const nowMs = (): number => getTemporal().Now.instant().epochMilliseconds;
|
|
365
|
+
|
|
366
|
+
// Immediate local visibility on the writing pod; other pods learn via the
|
|
367
|
+
// bus and re-read the row.
|
|
368
|
+
const applyAssignment = (tenantId: TenantId, tier: string): void => {
|
|
395
369
|
takeTicket(tenantId);
|
|
396
|
-
cache.
|
|
370
|
+
cache.set(tenantId, mergeAlwaysOn(alwaysOnHolder.set, featuresForTier(tierMap, tier)));
|
|
397
371
|
publishAssignmentChange(tenantId);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// mit tenant-create rollback), schreibt tier-assignment via Direct-
|
|
403
|
-
// Executor (nicht ctx.write — hook hat AppContext). Pattern analog
|
|
404
|
-
// tenant/seeding.ts seedTenant.
|
|
405
|
-
//
|
|
406
|
-
// **Idempotency:** deterministic aggregate-id aus tenantId. Re-replay
|
|
407
|
-
// (nach projection-rebuild oder hook-retry) findet stream-version > 0
|
|
408
|
-
// und skipt, statt version_conflict zu werfen. Caller sieht erfolgreichen
|
|
409
|
-
// tenant-create + bestehende tier-assignment.
|
|
410
|
-
//
|
|
411
|
-
// **Cross-tenant write:** executor braucht systemUser MIT tenantId =
|
|
412
|
-
// neuer Tenant (Memory `feedback_event_store_tenant_consistency`).
|
|
413
|
-
if (opts.defaultTier !== undefined) {
|
|
414
|
-
const defaultTier = opts.defaultTier;
|
|
415
|
-
const autoDefaultTierHookReason =
|
|
416
|
-
"seeds the default tier-assignment stream of the tenant just created";
|
|
372
|
+
};
|
|
373
|
+
onTierAssigned.fn = applyAssignment;
|
|
374
|
+
|
|
375
|
+
// Invalidation: tier-assignment CRUD events (e.g. the billing webhook) update the cache.
|
|
417
376
|
r.hook({
|
|
418
377
|
type: "postSave",
|
|
419
|
-
target: { allOf: "
|
|
420
|
-
handler: async (result
|
|
421
|
-
// result
|
|
422
|
-
const
|
|
423
|
-
// skip:
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (typeof data.id !== "string") return;
|
|
431
|
-
const newTenantId = data.id as TenantId; // @cast-boundary engine-payload
|
|
432
|
-
const aggregateId = tierAssignmentAggregateId(newTenantId);
|
|
433
|
-
|
|
434
|
-
// tenant is an r.systemScope() feature, so this cross-feature hook gets a
|
|
435
|
-
// fail-closed ctx.db — reach for ctx.systemDb instead (see entity-handlers.ts).
|
|
436
|
-
// Event-store paths need the raw DbConnection — TenantDb only exposes
|
|
437
|
-
// select/insert/update/delete, not execute (db.execute(sql`SELECT pg_notify(...)`) would TypeError).
|
|
438
|
-
const rawDb = ctx.systemDb
|
|
439
|
-
? ctx.systemDb.unsafeRaw(autoDefaultTierHookReason)
|
|
440
|
-
: ctx.db && "unsafeRaw" in ctx.db
|
|
441
|
-
? ctx.db.unsafeRaw()
|
|
442
|
-
: undefined;
|
|
443
|
-
// skip: defensive — inTransaction phase always sets db, but AppContext's
|
|
444
|
-
// type makes it optional. Throwing would be overreach (lifecycle
|
|
445
|
-
// blocking), silent-skip is defensive-soft.
|
|
446
|
-
if (!rawDb) return;
|
|
447
|
-
|
|
448
|
-
// Idempotency: stream-existence-check vor create. Pattern aus
|
|
449
|
-
// seedTenant.ts. Bei re-replay (rebuild) nicht versionsbumpen.
|
|
450
|
-
const streamVersion = await getUnscopedAggregateStreamMaxVersion(rawDb, aggregateId);
|
|
451
|
-
// skip: idempotent — tier-assignment stream already seeded (rebuild/replay).
|
|
452
|
-
if (streamVersion > 0) return;
|
|
453
|
-
|
|
454
|
-
// SystemUser für den NEUEN tenant — der Hook wird vom signup-
|
|
455
|
-
// user (anderer tenant, oder SystemAdmin) ausgelöst, aber das
|
|
456
|
-
// tier-assignment muss im stream des neu-erzeugten tenants
|
|
457
|
-
// landen (= aggregate-id deterministic auf newTenantId). Memory
|
|
458
|
-
// `feedback_event_store_tenant_consistency`: by.tenantId muss =
|
|
459
|
-
// ziel-tenant.
|
|
460
|
-
const systemUser: SessionUser = {
|
|
461
|
-
id: "00000000-0000-4000-8000-000000000001",
|
|
462
|
-
tenantId: newTenantId,
|
|
463
|
-
roles: ["SystemAdmin"],
|
|
464
|
-
};
|
|
465
|
-
const tdb = createTenantDb(rawDb, newTenantId, "system");
|
|
466
|
-
|
|
467
|
-
await tierAssignmentExecutor.create(
|
|
468
|
-
{ id: aggregateId, tier: defaultTier, source: TierAssignmentSources.default },
|
|
469
|
-
systemUser,
|
|
470
|
-
tdb,
|
|
471
|
-
);
|
|
378
|
+
target: { allOf: "tier-assignment" },
|
|
379
|
+
handler: async (result) => {
|
|
380
|
+
// result.data has tenantId + tier (after entity-update merge)
|
|
381
|
+
const data = result.data as { tenantId?: unknown; tier?: unknown }; // @cast-boundary engine-payload
|
|
382
|
+
// skip: defensive guard on the payload shape. Correct entity events
|
|
383
|
+
// always carry both fields as strings; a malformed payload (a custom
|
|
384
|
+
// handler bug) skips the cache update instead of throwing, because a
|
|
385
|
+
// hook error must not block the lifecycle pipeline (afterCommit,
|
|
386
|
+
// best-effort side effect).
|
|
387
|
+
if (typeof data.tenantId !== "string" || typeof data.tier !== "string") return;
|
|
388
|
+
applyAssignment(data.tenantId as TenantId, data.tier);
|
|
472
389
|
},
|
|
473
|
-
phase: HookPhases.inTransaction,
|
|
474
|
-
escapeHatch: { reason: autoDefaultTierHookReason },
|
|
475
390
|
});
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
391
|
+
r.hook({
|
|
392
|
+
type: "postDelete",
|
|
393
|
+
target: { allOf: "tier-assignment" },
|
|
394
|
+
handler: async (payload) => {
|
|
395
|
+
const data = payload.data as { tenantId?: unknown }; // @cast-boundary engine-payload
|
|
396
|
+
// skip: same type-guard semantics as the postSave hook above.
|
|
397
|
+
if (typeof data.tenantId !== "string") return;
|
|
398
|
+
const tenantId = data.tenantId as TenantId;
|
|
399
|
+
takeTicket(tenantId);
|
|
400
|
+
cache.delete(tenantId);
|
|
401
|
+
publishAssignmentChange(tenantId);
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Auto-default-tier-on-tenant-signup: the hook fires inTransaction (atomic
|
|
406
|
+
// with the tenant-create rollback) and writes the tier-assignment via the
|
|
407
|
+
// direct executor (not ctx.write — the hook has an AppContext). Same
|
|
408
|
+
// pattern as seedTenant in tenant/seeding.ts.
|
|
409
|
+
//
|
|
410
|
+
// **Idempotency:** deterministic aggregate id derived from tenantId. A
|
|
411
|
+
// re-replay (after a projection rebuild or hook retry) finds
|
|
412
|
+
// stream-version > 0 and skips instead of throwing version_conflict. The
|
|
413
|
+
// caller sees a successful tenant-create plus the existing
|
|
414
|
+
// tier-assignment.
|
|
415
|
+
//
|
|
416
|
+
// **Cross-tenant write:** the executor needs a systemUser WITH tenantId =
|
|
417
|
+
// the new tenant (memory `feedback_event_store_tenant_consistency`).
|
|
418
|
+
if (opts.defaultTier !== undefined) {
|
|
419
|
+
const defaultTier = opts.defaultTier;
|
|
420
|
+
const autoDefaultTierHookReason =
|
|
421
|
+
"seeds the default tier-assignment stream of the tenant just created";
|
|
422
|
+
r.hook({
|
|
423
|
+
type: "postSave",
|
|
424
|
+
target: { allOf: "tenant" },
|
|
425
|
+
handler: async (result, ctx) => {
|
|
426
|
+
// result shape: kumiko-framework's SaveContext with isNew + data
|
|
427
|
+
const saveResult = result as { isNew?: unknown; data?: unknown }; // @cast-boundary engine-payload
|
|
428
|
+
// skip: only on tenant-create (initial) — tenant updates fire
|
|
429
|
+
// postSave too, but a re-keying or name update must not create a
|
|
430
|
+
// new tier-assignment.
|
|
431
|
+
if (saveResult.isNew !== true) return;
|
|
432
|
+
const data = saveResult.data as { id?: unknown }; // @cast-boundary engine-payload
|
|
433
|
+
// skip: defensive type guard. The tenant entity always has an id, but
|
|
434
|
+
// CrudExecutor's payload shape is unknown at runtime.
|
|
435
|
+
if (typeof data.id !== "string") return;
|
|
436
|
+
const newTenantId = data.id as TenantId; // @cast-boundary engine-payload
|
|
437
|
+
const aggregateId = tierAssignmentAggregateId(newTenantId);
|
|
438
|
+
|
|
439
|
+
// tenant is an r.systemScope() feature, so this cross-feature hook gets a
|
|
440
|
+
// fail-closed ctx.db — reach for ctx.systemDb instead (see entity-handlers.ts).
|
|
441
|
+
// Event-store paths need the raw DbConnection — TenantDb only exposes
|
|
442
|
+
// select/insert/update/delete, not execute (db.execute(sql`SELECT pg_notify(...)`) would TypeError).
|
|
443
|
+
const rawDb = ctx.systemDb
|
|
444
|
+
? ctx.systemDb.unsafeRaw(autoDefaultTierHookReason)
|
|
445
|
+
: ctx.db && "unsafeRaw" in ctx.db
|
|
446
|
+
? ctx.db.unsafeRaw()
|
|
447
|
+
: undefined;
|
|
448
|
+
// skip: defensive — inTransaction phase always sets db, but AppContext's
|
|
449
|
+
// type makes it optional. Throwing would be overreach (lifecycle
|
|
450
|
+
// blocking), silent-skip is defensive-soft.
|
|
451
|
+
if (!rawDb) return;
|
|
452
|
+
|
|
453
|
+
// Idempotency: stream-existence check before create. Same pattern as
|
|
454
|
+
// seedTenant.ts. On a re-replay (rebuild) do not bump the version.
|
|
455
|
+
const streamVersion = await getUnscopedAggregateStreamMaxVersion(rawDb, aggregateId);
|
|
456
|
+
// skip: idempotent — tier-assignment stream already seeded (rebuild/replay).
|
|
457
|
+
if (streamVersion > 0) return;
|
|
458
|
+
|
|
459
|
+
// SystemUser for the NEW tenant — the hook is triggered by the signup
|
|
460
|
+
// user (another tenant, or SystemAdmin), but the tier-assignment has
|
|
461
|
+
// to land in the stream of the newly created tenant (= aggregate id
|
|
462
|
+
// deterministic on newTenantId). Memory
|
|
463
|
+
// `feedback_event_store_tenant_consistency`: by.tenantId must equal
|
|
464
|
+
// the target tenant.
|
|
465
|
+
const systemUser: SessionUser = {
|
|
466
|
+
id: "00000000-0000-4000-8000-000000000001",
|
|
467
|
+
tenantId: newTenantId,
|
|
468
|
+
roles: ["SystemAdmin"],
|
|
469
|
+
};
|
|
470
|
+
const tdb = createTenantDb({ db: rawDb, tenantId: newTenantId, mode: "system" });
|
|
471
|
+
|
|
472
|
+
await tierAssignmentExecutor.create(
|
|
473
|
+
{ id: aggregateId, tier: defaultTier, source: TierAssignmentSources.default },
|
|
474
|
+
systemUser,
|
|
475
|
+
tdb,
|
|
527
476
|
);
|
|
528
|
-
}
|
|
529
|
-
|
|
477
|
+
},
|
|
478
|
+
phase: HookPhases.inTransaction,
|
|
479
|
+
escapeHatch: { reason: autoDefaultTierHookReason },
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Extension-point declaration + self-registration. Same pattern as
|
|
484
|
+
// mail-foundation/file-foundation: the feature declares the extension
|
|
485
|
+
// point AND registers itself as the default plugin. Other features could
|
|
486
|
+
// register there later (e.g. a future toggle-only resolver), but only one
|
|
487
|
+
// plugin wins — see the "single-plugin" comment on findTierResolverUsage.
|
|
488
|
+
r.extendsRegistrar(TENANT_TIER_RESOLVER_EXT, { onRegister: () => {} });
|
|
489
|
+
|
|
490
|
+
// Plugin-registration: framework's runDevApp/runProdApp picks this up
|
|
491
|
+
// post-stack-setup, calls build(deps), wires effectiveFeatures.
|
|
492
|
+
const plugin: TierResolverPlugin = {
|
|
493
|
+
build: async (deps) => {
|
|
494
|
+
// Always-on set: all non-toggleable features in the registry. Canonical
|
|
495
|
+
// Kumiko pattern (matches the feature-toggles resolver): only features
|
|
496
|
+
// that explicitly register `r.toggleable()` are tier-cuttable.
|
|
497
|
+
// Framework base features (auth-email-password, config, user, tenant,
|
|
498
|
+
// sessions, etc.) are non-toggleable → ALWAYS active for every tenant
|
|
499
|
+
// regardless of tier. Otherwise the dispatcher would answer 403 on the
|
|
500
|
+
// login handler because "feature auth-email-password disabled".
|
|
501
|
+
const computedAlwaysOn = new Set<string>();
|
|
502
|
+
for (const feature of deps.registry.features.values()) {
|
|
503
|
+
if (feature.toggleableDefault === undefined) computedAlwaysOn.add(feature.name);
|
|
530
504
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
505
|
+
alwaysOnHolder.set = computedAlwaysOn;
|
|
506
|
+
|
|
507
|
+
// Pre-load all existing assignments into the cache. SaaS apps typically
|
|
508
|
+
// have <100k tenants, so a single-pass scan is acceptable. The scaling
|
|
509
|
+
// path (lazy load + LRU) is Sprint-8b, once a real need arises.
|
|
510
|
+
type AssignmentRow = { tenantId: string; tier: string };
|
|
511
|
+
const rows = await selectMany<AssignmentRow>(deps.db, tierAssignmentTable);
|
|
512
|
+
for (const row of rows) {
|
|
513
|
+
cache.set(
|
|
539
514
|
row.tenantId as TenantId,
|
|
540
515
|
mergeAlwaysOn(computedAlwaysOn, featuresForTier(tierMap, row.tier)),
|
|
541
516
|
);
|
|
542
517
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
518
|
+
|
|
519
|
+
// Read first, then set — never clear-then-refill: a cache miss falls back
|
|
520
|
+
// to the default tier, which would lock paying tenants out meanwhile.
|
|
521
|
+
const reloadTenant = async (tenantId: TenantId): Promise<void> => {
|
|
522
|
+
const ticket = takeTicket(tenantId);
|
|
523
|
+
const row = await fetchOne<AssignmentRow>(deps.db, tierAssignmentTable, { tenantId });
|
|
524
|
+
// skip: a newer write or reload for this tenant started meanwhile and owns the entry
|
|
525
|
+
if (latestTicket.get(tenantId) !== ticket) return;
|
|
526
|
+
if (row) {
|
|
527
|
+
cache.set(
|
|
528
|
+
tenantId,
|
|
529
|
+
mergeAlwaysOn(computedAlwaysOn, featuresForTier(tierMap, row.tier)),
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
547
532
|
cache.delete(tenantId);
|
|
548
|
-
latestTicket.set(tenantId, startedAtTicket);
|
|
549
533
|
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
534
|
+
};
|
|
535
|
+
const reloadAllTenants = async (): Promise<void> => {
|
|
536
|
+
ticketCounter += 1;
|
|
537
|
+
const startedAtTicket = ticketCounter;
|
|
538
|
+
const current = await selectMany<AssignmentRow>(deps.db, tierAssignmentTable);
|
|
539
|
+
const loaded = new Map<TenantId, ReadonlySet<string>>();
|
|
540
|
+
for (const row of current) {
|
|
541
|
+
loaded.set(
|
|
542
|
+
row.tenantId as TenantId,
|
|
543
|
+
mergeAlwaysOn(computedAlwaysOn, featuresForTier(tierMap, row.tier)),
|
|
544
|
+
);
|
|
556
545
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
tenantId
|
|
567
|
-
|
|
546
|
+
const isNewerThanSnapshot = (tenantId: TenantId): boolean =>
|
|
547
|
+
(latestTicket.get(tenantId) ?? 0) > startedAtTicket;
|
|
548
|
+
for (const tenantId of [...cache.keys()]) {
|
|
549
|
+
if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) {
|
|
550
|
+
cache.delete(tenantId);
|
|
551
|
+
latestTicket.set(tenantId, startedAtTicket);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
for (const [tenantId, features] of loaded) {
|
|
555
|
+
if (!isNewerThanSnapshot(tenantId)) {
|
|
556
|
+
cache.set(tenantId, features);
|
|
557
|
+
// Makes a single reload that started before this snapshot discard its older result.
|
|
558
|
+
latestTicket.set(tenantId, startedAtTicket);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
busHolder.bus = deps.cacheSync;
|
|
564
|
+
deps.cacheSync?.subscribe(CACHE_SYNC_TOPICS.tierAssignment, (message) => {
|
|
565
|
+
// skip: malformed message from another process
|
|
566
|
+
if (!isTierAssignmentSyncMessage(message)) return;
|
|
567
|
+
reloadTenant(message.tenantId).catch((err: unknown) => {
|
|
568
|
+
log.error("tier reload failed, keeping cached set", {
|
|
569
|
+
tenantId: message.tenantId,
|
|
570
|
+
err,
|
|
571
|
+
});
|
|
568
572
|
});
|
|
569
573
|
});
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
+
deps.cacheSync?.onResync(() => {
|
|
575
|
+
reloadAllTenants().catch((err: unknown) => {
|
|
576
|
+
log.error("tier resync failed, keeping cached sets", { err });
|
|
577
|
+
});
|
|
574
578
|
});
|
|
575
|
-
});
|
|
576
579
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
580
|
+
// Synchronous resolver-callback for dispatcher hot-path.
|
|
581
|
+
const resolver = (tenantId: TenantId): ReadonlySet<string> => {
|
|
582
|
+
// Operator-tooling + async-event-dispatch convention: SYSTEM_TENANT_ID
|
|
583
|
+
// gets the union of all tier features (see DispatcherOptions doc).
|
|
584
|
+
if (tenantId === SYSTEM_TENANT_ID) {
|
|
585
|
+
return mergeAlwaysOn(computedAlwaysOn, unionAllTierFeatures(tierMap));
|
|
586
|
+
}
|
|
587
|
+
// Cache miss: the tenant is not in the cache yet (e.g. brand new after
|
|
588
|
+
// boot, or the defaultTier hook has not fired yet). The default set
|
|
589
|
+
// is least-privileged — typically free-tier features. Memory
|
|
590
|
+
// `feedback_security_default_on`: secure by default. The trial is NOT
|
|
591
|
+
// added here (sync/boot-cached, cannot see the passage of time) — the
|
|
592
|
+
// trialGate below does that on the disabled-gate path.
|
|
593
|
+
const fallbackTier = opts.defaultTier;
|
|
594
|
+
return (
|
|
595
|
+
cache.get(tenantId) ??
|
|
596
|
+
(fallbackTier === undefined
|
|
597
|
+
? computedAlwaysOn
|
|
598
|
+
: mergeAlwaysOn(computedAlwaysOn, featuresForTier(tierMap, fallbackTier)))
|
|
599
|
+
);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const trial = opts.trial;
|
|
603
|
+
if (trial === undefined) return resolver;
|
|
604
|
+
|
|
605
|
+
// Live trial gate: reads tenant.inserted_at (≈ signup; exists for EVERY
|
|
606
|
+
// tenant incl. auth-signup via seedTenant — unlike the tier-assignment
|
|
607
|
+
// row, which the seed path does not create) and checks the window
|
|
608
|
+
// against the current time. Consulted only on the disabled-gate path.
|
|
609
|
+
// inserted_at is immutable → read once per tenant.
|
|
610
|
+
// Promise-memo (not a plain value cache): N concurrent requests for the
|
|
611
|
+
// same fresh tenant must await the SAME in-flight fetchOne, not each
|
|
612
|
+
// fire their own — a plain `Map<TenantId, number>` set AFTER the await
|
|
613
|
+
// lets every concurrent caller miss the cache and issue a redundant
|
|
614
|
+
// read (benign but wasteful on a signup burst).
|
|
615
|
+
const startedMemo = new Map<TenantId, Promise<number | undefined>>();
|
|
616
|
+
const trialGate: TrialGate = async (tenantId, featureName) => {
|
|
617
|
+
if (!trialFeatures.has(featureName)) return false;
|
|
618
|
+
let startedMsPromise = startedMemo.get(tenantId);
|
|
619
|
+
if (startedMsPromise === undefined) {
|
|
620
|
+
startedMsPromise = fetchOne<{ insertedAt?: Temporal.Instant }>(deps.db, tenantTable, {
|
|
621
|
+
id: tenantId,
|
|
622
|
+
}).then((row) => row?.insertedAt?.epochMilliseconds);
|
|
623
|
+
// Evict on rejection too — a transient DB error must not poison
|
|
624
|
+
// every future call for this tenant with the same cached rejection.
|
|
625
|
+
startedMsPromise.catch(() => startedMemo.delete(tenantId));
|
|
626
|
+
startedMemo.set(tenantId, startedMsPromise);
|
|
627
|
+
}
|
|
628
|
+
const startedMs = await startedMsPromise;
|
|
629
|
+
// Tenant row not projected yet (replay race) → do not memoize the
|
|
630
|
+
// miss, re-read on the next request.
|
|
631
|
+
if (startedMs === undefined) {
|
|
632
|
+
startedMemo.delete(tenantId);
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
return isTrialActive(startedMs, nowMs(), trial.durationHours);
|
|
636
|
+
};
|
|
637
|
+
return Object.assign(resolver, { trialGate });
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: r.useExtension is a framework registrar method, not a React hook.
|
|
641
|
+
r.useExtension(TENANT_TIER_RESOLVER_EXT, TIER_ENGINE_FEATURE, plugin);
|
|
642
|
+
},
|
|
643
|
+
Object.keys(opts).length === 0 ? { dedupeOptions: {} } : undefined,
|
|
644
|
+
);
|
|
640
645
|
}
|
|
641
646
|
|
|
642
|
-
/**
|
|
643
|
-
* Legacy named export — equivalent to `createTierEngineFeature()` ohne opts.
|
|
644
|
-
* Storage-only mode (no resolver, no auto-default). Existing apps die
|
|
645
|
-
* `tierEngineFeature` referenzieren bekommen identical behavior.
|
|
646
|
-
*
|
|
647
|
-
* Migration zu `createTierEngineFeature({ defaultTier, tierMap })` gibt
|
|
648
|
-
* volle Tier-Composition (Sprint-8a Pattern).
|
|
649
|
-
*/
|
|
647
|
+
/** @deprecated Use createTierEngineFeature(). Run `bun kumiko-upgrade --apply`. Removed in the first release after 2026-10-29. */
|
|
650
648
|
export const tierEngineFeature: FeatureDefinition = createTierEngineFeature();
|