@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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 +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +12 -0
- package/packages/bundled-features/src/agent-tools/index.ts +1 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
- package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
- package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
- package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
- package/packages/bundled-features/src/cap-counter/changes.json +30 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +18 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
- package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
- package/packages/bundled-features/src/delivery/changes.json +42 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +5 -0
- package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
- package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +12 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +15 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/ledger/web/index.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
- package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
- package/packages/bundled-features/src/managed-pages/index.ts +1 -0
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
- package/packages/bundled-features/src/sessions/changes.json +7 -0
- package/packages/bundled-features/src/sessions/feature.ts +8 -2
- package/packages/bundled-features/src/sessions/index.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
- package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/tags/feature.ts +1 -0
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +84 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
- package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/feature.ts +1 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
- package/packages/bundled-features/src/waitlist/changes.json +36 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
- package/packages/bundled-features/src/waitlist/import.ts +28 -3
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +5 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/samples/apps/styleguide/src/app/client.tsx +2 -0
- package/samples/apps/styleguide/src/app/seed.ts +6 -4
- package/samples/apps/styleguide/src/app/server.ts +4 -1
- package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
- package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
- package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
- package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
- package/samples/apps/use-all-bundled/bin/main.ts +2 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
- package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
- package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
- package/samples/apps/use-all-bundled/package.json +2 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
- package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/golden-path/src/feature.ts +2 -1
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -0
- package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
- package/samples/recipes/list-table-options/README.md +50 -0
- package/samples/recipes/list-table-options/package.json +11 -0
- package/samples/recipes/list-table-options/src/feature.ts +221 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/samples/recipes/state-machine/src/events.ts +3 -2
- package/samples/recipes/tenant-isolation/README.md +1 -0
- package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -163,6 +163,35 @@ async function runTenantDataHooks(ctx: DestructionStageCtx): Promise<StageRunOut
|
|
|
163
163
|
return total.outcome();
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// A user key outlives the tenant while the user still belongs to another live tenant:
|
|
167
|
+
// erasing it would tombstone the key and break every later PII write for that user.
|
|
168
|
+
async function userIdsWithoutOtherLiveTenant(
|
|
169
|
+
ctx: DestructionStageCtx,
|
|
170
|
+
userIds: readonly string[],
|
|
171
|
+
): Promise<readonly string[]> {
|
|
172
|
+
// skip: no members, nothing to look up.
|
|
173
|
+
if (userIds.length === 0) return [];
|
|
174
|
+
const otherMemberships = (
|
|
175
|
+
await selectMany<{ userId: string; tenantId: string }>(ctx.db, tenantMembershipsTable, {
|
|
176
|
+
userId: { in: [...userIds] },
|
|
177
|
+
})
|
|
178
|
+
).filter((membership) => membership.tenantId !== ctx.tenantId);
|
|
179
|
+
// skip: every member belongs to this tenant only.
|
|
180
|
+
if (otherMemberships.length === 0) return userIds;
|
|
181
|
+
const otherTenants = await selectMany<{ id: string; status: string }>(ctx.db, tenantTable, {
|
|
182
|
+
id: { in: [...new Set(otherMemberships.map((membership) => membership.tenantId))] },
|
|
183
|
+
});
|
|
184
|
+
const liveTenantIds = new Set(
|
|
185
|
+
otherTenants.filter((tenant) => tenant.status !== "destroyed").map((tenant) => tenant.id),
|
|
186
|
+
);
|
|
187
|
+
const retainedUserIds = new Set(
|
|
188
|
+
otherMemberships
|
|
189
|
+
.filter((membership) => liveTenantIds.has(membership.tenantId))
|
|
190
|
+
.map((membership) => membership.userId),
|
|
191
|
+
);
|
|
192
|
+
return userIds.filter((userId) => !retainedUserIds.has(userId));
|
|
193
|
+
}
|
|
194
|
+
|
|
166
195
|
async function eraseSubjectKeys(ctx: DestructionStageCtx): Promise<StageRunOutcome> {
|
|
167
196
|
const kms = configuredPiiSubjectKms();
|
|
168
197
|
if (!kms) {
|
|
@@ -177,7 +206,12 @@ async function eraseSubjectKeys(ctx: DestructionStageCtx): Promise<StageRunOutco
|
|
|
177
206
|
const subjects: SubjectId[] = [
|
|
178
207
|
{ kind: "tenant", tenantId: ctx.tenantId },
|
|
179
208
|
tenantRecordSubject,
|
|
180
|
-
...
|
|
209
|
+
...(
|
|
210
|
+
await userIdsWithoutOtherLiveTenant(
|
|
211
|
+
ctx,
|
|
212
|
+
memberships.map((m) => m.userId),
|
|
213
|
+
)
|
|
214
|
+
).map((userId) => ({ kind: "user" as const, userId })),
|
|
181
215
|
];
|
|
182
216
|
for (const subject of subjects) {
|
|
183
217
|
await kms.eraseKey(subject, {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "set-tenant-tier no longer accepts source default",
|
|
6
|
+
"detail": "The source field of set-tenant-tier is limited to manual and billing. The value default is reserved for the signup default tier and made a manual grant look like a billing row that the next sync overwrites."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.370.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -59,7 +59,10 @@ export function createSetTenantTierWrite(opts: SetTenantTierOptions = {}) {
|
|
|
59
59
|
.min(1)
|
|
60
60
|
.max(50)
|
|
61
61
|
.refine((t) => !opts.validTiers || opts.validTiers.has(t), { message: "unknown tier" }),
|
|
62
|
-
|
|
62
|
+
// `default` is reserved for the signup default tier; the billing sync treats it like `billing`.
|
|
63
|
+
source: z
|
|
64
|
+
.enum([TierAssignmentSources.manual, TierAssignmentSources.billing])
|
|
65
|
+
.default(TierAssignmentSources.manual),
|
|
63
66
|
}),
|
|
64
67
|
access: { roles: ["SystemAdmin"] },
|
|
65
68
|
agent: { risk: "high" },
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// category in the kumiko isolation model (runtime may import client, client
|
|
4
4
|
// may only import client) — otherwise the runtime isolation guard throws on
|
|
5
5
|
// the client imports. Same pattern as text-content/constants.
|
|
6
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
|
+
|
|
6
8
|
// Feature name
|
|
7
9
|
export const TIER_ENGINE_FEATURE = "tier-engine" as const;
|
|
8
10
|
|
|
@@ -24,10 +26,8 @@ export const TierAssignmentSources = {
|
|
|
24
26
|
export type TierAssignmentSource =
|
|
25
27
|
(typeof TierAssignmentSources)[keyof typeof TierAssignmentSources];
|
|
26
28
|
|
|
27
|
-
const TIER_ASSIGNMENT_SOURCE_VALUES: readonly string[] = Object.values(TierAssignmentSources);
|
|
28
|
-
|
|
29
29
|
export function isTierAssignmentSource(value: unknown): value is TierAssignmentSource {
|
|
30
|
-
return
|
|
30
|
+
return isOneOf(Object.values(TierAssignmentSources), value);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Qualified query handler names.
|
|
@@ -19,6 +19,7 @@ import { userEditScreen, userListScreen } from "./screens.ts";
|
|
|
19
19
|
// Membership + tenant-specific roles live in the tenant feature.
|
|
20
20
|
export function createUserFeature(): FeatureDefinition {
|
|
21
21
|
return defineFeature("user", (r) => {
|
|
22
|
+
r.adoptsBundledLists();
|
|
22
23
|
r.describe(
|
|
23
24
|
"Manages the cross-tenant user identity: the `read_users` table holds each user's email, `displayName`, global `roles`, `emailVerified` flag, and lifecycle `status` (active / restricted / deletionRequested / deleted). Because users exist above any individual tenant, the feature runs with `r.systemScope()` \u2014 membership and tenant-specific roles live in the `tenant` feature instead. Add this feature whenever your app needs a persistent, tenant-agnostic user record that auth and GDPR pipelines can reference.",
|
|
24
25
|
);
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "USER_DATA_RIGHTS_FEATURE is exported from the barrel",
|
|
6
|
+
"detail": "The feature name constant that lived in web/constants.ts is now part of the public barrel."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.389.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Complete, exported handler name constants",
|
|
12
|
+
"detail": "UserDataRightsHandlers now covers all eight write handlers and UserDataRightsQueries all five explicit query handlers, and both are exported from user-data-rights and user-data-rights/web. A test compares them with the feature's registered handlers."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.384.0",
|
|
4
16
|
"type": "fix",
|
|
@@ -188,6 +188,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
188
188
|
let warnedMissingMailDelivery = false;
|
|
189
189
|
const showDeletion = opts.privacyCenterShowDeletion ?? true;
|
|
190
190
|
return defineFeature("user-data-rights", (r) => {
|
|
191
|
+
r.adoptsBundledLists();
|
|
191
192
|
r.describe(
|
|
192
193
|
'Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion`, plus the anonymous email-verified `request-deletion-by-email` + `confirm-deletion-by-token` flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / `restrict-account` + `lift-restriction`), and Art. 20 (portability / async `request-export` \u2192 ZIP via `file-foundation`, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })` \u2014 the feature then orchestrates the export and forget pipelines across all registered hooks automatically. When `mail-foundation` and a `mail-transport-*` are mounted, it also sends the four GDPR notifications (export ready/failed, deletion requested/executed) itself with no app callback code, rendered in each recipient’s locale. Requires `user`, `data-retention`, `compliance-profiles`, and `sessions`.',
|
|
193
194
|
);
|
|
@@ -31,4 +31,9 @@ export {
|
|
|
31
31
|
exportJobEntity,
|
|
32
32
|
exportJobsTable,
|
|
33
33
|
} from "./schema/export-job.ts";
|
|
34
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
TENANT_MODEL_CONFIG_KEY,
|
|
36
|
+
USER_DATA_RIGHTS_FEATURE,
|
|
37
|
+
UserDataRightsHandlers,
|
|
38
|
+
UserDataRightsQueries,
|
|
39
|
+
} from "./web/constants.ts";
|
|
@@ -21,7 +21,6 @@ import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
|
21
21
|
// storage indefinitely.
|
|
22
22
|
|
|
23
23
|
import { addDurationSpec } from "@cosmicdrift/kumiko-framework/compliance";
|
|
24
|
-
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
25
24
|
import type { DbConnection, DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
26
25
|
import {
|
|
27
26
|
createEventStoreExecutor,
|
|
@@ -44,7 +43,7 @@ import {
|
|
|
44
43
|
import type { MetricsHandle } from "@cosmicdrift/kumiko-framework/observability";
|
|
45
44
|
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
46
45
|
import { resolveProfileForTenant } from "../compliance-profiles/index.ts";
|
|
47
|
-
import {
|
|
46
|
+
import { decryptStoredUserEmail } from "../shared/index.ts";
|
|
48
47
|
import { userTable } from "../user/index.ts";
|
|
49
48
|
import { selectExportJobsForStorageCleanup } from "./db/queries/export-jobs.ts";
|
|
50
49
|
import { runUserExport, type UserExportBundle } from "./run-user-export.ts";
|
|
@@ -822,8 +821,8 @@ async function lookupUserContact(
|
|
|
822
821
|
locale: string | null;
|
|
823
822
|
} | null;
|
|
824
823
|
if (!row?.email) return null;
|
|
825
|
-
const email = await
|
|
826
|
-
if (email ===
|
|
824
|
+
const email = await decryptStoredUserEmail(row.email, "user-data-rights:export-ready");
|
|
825
|
+
if (email === null) return null;
|
|
827
826
|
return { email, locale: row.locale ?? null };
|
|
828
827
|
}
|
|
829
828
|
|
|
@@ -518,7 +518,8 @@ async function processUser(args: {
|
|
|
518
518
|
// is what makes this crash-safe: eraseKey-throw → tx rollback → user
|
|
519
519
|
// stays DeletionRequested → next run retries; flip-throw after a
|
|
520
520
|
// successful erase → retry re-runs the idempotent hooks and eraseKey
|
|
521
|
-
// (
|
|
521
|
+
// (idempotent: a repeat erase keeps the original tombstone, an unknown
|
|
522
|
+
// subject gets one). Erasing after
|
|
522
523
|
// the flip instead would risk a permanent silent miss: user already
|
|
523
524
|
// Deleted, no run ever picks him up again, key stays live.
|
|
524
525
|
if (kms) {
|
|
@@ -27,6 +27,8 @@ export const UserDataRightsQueries = {
|
|
|
27
27
|
exportStatus: "user-data-rights:query:export-status",
|
|
28
28
|
myAuditLog: "user-data-rights:query:my-audit-log",
|
|
29
29
|
downloadByJob: "user-data-rights:query:download-by-job",
|
|
30
|
+
downloadByToken: "user-data-rights:query:download-by-token",
|
|
31
|
+
listDownloadAttempts: "user-data-rights:query:list-download-attempts",
|
|
30
32
|
} as const;
|
|
31
33
|
|
|
32
34
|
// App-level config: tenant-occupancy model. "single-user" tells the forget
|
|
@@ -39,8 +41,12 @@ export const TENANT_MODEL_CONFIG_KEY = "user-data-rights:config:tenant-model" as
|
|
|
39
41
|
export const UserDataRightsHandlers = {
|
|
40
42
|
requestExport: "user-data-rights:write:request-export",
|
|
41
43
|
requestDeletion: "user-data-rights:write:request-deletion",
|
|
44
|
+
requestDeletionByEmail: "user-data-rights:write:request-deletion-by-email",
|
|
45
|
+
confirmDeletionByToken: "user-data-rights:write:confirm-deletion-by-token",
|
|
42
46
|
cancelDeletion: "user-data-rights:write:cancel-deletion",
|
|
43
47
|
restrictAccount: "user-data-rights:write:restrict-account",
|
|
48
|
+
liftRestriction: "user-data-rights:write:lift-restriction",
|
|
49
|
+
runForgetCleanup: "user-data-rights:write:run-forget-cleanup",
|
|
44
50
|
} as const;
|
|
45
51
|
|
|
46
52
|
// Foreign QN: the lifecycle status (active / deletionRequested / restricted)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export { type UserDataRightsClientOptions, userDataRightsClient } from "./client-plugin.tsx";
|
|
8
8
|
export type { ConfirmAccountDeletionScreenProps } from "./confirm-deletion-screen.tsx";
|
|
9
9
|
export { ConfirmAccountDeletionScreen } from "./confirm-deletion-screen.tsx";
|
|
10
|
+
export { UserDataRightsHandlers, UserDataRightsQueries } from "./constants.ts";
|
|
10
11
|
export { defaultTranslations } from "./default-translations.ts";
|
|
11
12
|
export { formatDate } from "./privacy-center-screen.tsx";
|
|
12
13
|
export { makePublicDeletionGate, type PublicDeletionRoutes } from "./public-deletion-gate.tsx";
|
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "A failing notifyRecipient SQL statement no longer aborts the submit",
|
|
6
|
+
"detail": "The resolver receives a ctx whose systemDb runs in a savepoint, so a failed statement is rolled back and the submit answers 200 with the entry stored. Only ctx.systemDb is savepoint-scoped; other ctx handles stay in the handler transaction."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.389.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "waitlist purge retry after a failed shred is covered",
|
|
12
|
+
"detail": "An integration test fails the second key shred of a purge, checks that the entries stay and no audit event is written, and that the retried purge erases every entry and records its audit event. No runtime change."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.389.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Waitlist import enforces the live field limits",
|
|
18
|
+
"detail": "migrateLegacyWaitlist counts a source row with a value above the live field limit as unreadable instead of importing it; importWaitlistEntry throws for it."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.388.0",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "Link-on-signup failures, legacy re-run and purge audit order",
|
|
24
|
+
"detail": "link-on-signup throws when the entry update fails and the entry is still invited, so the consumer redelivers. The offlot legacy migration skips rows that are already imports. purge writes its audit event before shredding keys."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.388.0",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "Admin notice language comes from adminLocale",
|
|
30
|
+
"detail": "The admin notice mail used the request locale, which is the entrant's browser language. The new adminLocale option (en, de or es, default en) sets the operator's language."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.388.0",
|
|
34
|
+
"type": "fix",
|
|
35
|
+
"title": "Legacy migration dry run counts the streams it would archive",
|
|
36
|
+
"detail": "The dry run now runs the read-only archive checks and reports archivedStreams without writing; the CLI prints \"would archive\" for it."
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "0.379.0",
|
|
4
40
|
"type": "improvement",
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
|
|
33
33
|
export function createWaitlistFeature(opts: WaitlistOptions = {}): FeatureDefinition {
|
|
34
34
|
return defineFeature(WAITLIST_FEATURE, (r) => {
|
|
35
|
+
r.adoptsBundledLists();
|
|
35
36
|
r.describe(
|
|
36
37
|
"Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response body is identical whether or not an address was already listed), and mails the submitter a confirmation. SystemAdmins review entries in a list screen and invite or reject them; an invite creates the entrant's own tenant (or joins one shared tenant) and mails an accept link with the configured membership role, never a global role. Entries are platform-wide, their personal data is crypto-shredding-ready, and the EXT_USER_DATA hooks export and erase entries matching a user's email.",
|
|
37
38
|
);
|
|
@@ -74,8 +74,9 @@ export const purgeHandler = defineWriteHandler<"purge", typeof PurgeSchema, Wait
|
|
|
74
74
|
}),
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
// Audit first: purgeWaitlistRows shreds KMS keys, which a later failure cannot roll back.
|
|
78
78
|
await appendAudit(ctx, WAITLIST_PURGED_EVENT_QN, rows.length);
|
|
79
|
+
await purgeWaitlistRows(db, rows, event.user.id);
|
|
79
80
|
return { isSuccess: true, data: { purged: rows.length } };
|
|
80
81
|
},
|
|
81
82
|
});
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
acquireNamespacedAdvisoryLock,
|
|
3
|
+
createSystemDbView,
|
|
4
|
+
createTenantDb,
|
|
5
|
+
runInSavepointIfSupported,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
2
7
|
import {
|
|
3
8
|
access,
|
|
4
9
|
defineWriteHandler,
|
|
5
10
|
type HandlerContext,
|
|
11
|
+
type TenantId,
|
|
6
12
|
type WriteResult,
|
|
7
13
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
14
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
@@ -25,7 +31,7 @@ import { WAITLIST_NOTIFICATION_TYPES, WAITLIST_STATUS } from "../web/constants.t
|
|
|
25
31
|
// framework's other fixed advisory-lock keys.
|
|
26
32
|
const SUBMIT_LOCK_NAMESPACE = 0x776c7374;
|
|
27
33
|
const SUBMIT_LOCK_REASON =
|
|
28
|
-
"takes the per-email advisory lock so parallel submits cannot both pass the duplicate check; TenantDb has
|
|
34
|
+
"takes the per-email advisory lock so parallel submits cannot both pass the duplicate check and wraps the admin-recipient resolver in a savepoint so a failing statement cannot abort the submit transaction; TenantDb has neither API";
|
|
29
35
|
|
|
30
36
|
// Logged-in callers carry no anonymous role, so every signed-in role must be listed.
|
|
31
37
|
const SUBMIT_ROLES = access.roles(
|
|
@@ -73,15 +79,24 @@ async function hasOpenEntryForEmail(email: string, ctx: HandlerContext): Promise
|
|
|
73
79
|
return found.rows.length > 0;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
// The resolver usually reads config
|
|
77
|
-
//
|
|
78
|
-
//
|
|
82
|
+
// The resolver usually reads config. It runs before the per-email advisory lock so a slow
|
|
83
|
+
// resolver does not extend the lock. Postgres aborts the whole transaction after a failed
|
|
84
|
+
// statement, even one whose JS error is caught here, so the resolver gets a ctx whose
|
|
85
|
+
// systemDb is bound to its own savepoint: a failing statement rolls back only that.
|
|
86
|
+
// Statements through any other ctx handle (ctx.query, ctx.write) stay in the handler transaction.
|
|
79
87
|
async function resolveAdminRecipient(
|
|
80
88
|
opts: WaitlistOptions,
|
|
81
89
|
ctx: HandlerContext,
|
|
90
|
+
systemDb: NonNullable<HandlerContext["systemDb"]>,
|
|
91
|
+
tenantId: TenantId,
|
|
82
92
|
): Promise<string | null> {
|
|
83
93
|
try {
|
|
84
|
-
return
|
|
94
|
+
return await runInSavepointIfSupported(systemDb.unsafeRaw(SUBMIT_LOCK_REASON), async (sp) => {
|
|
95
|
+
const savepointSystemDb = createSystemDbView(
|
|
96
|
+
createTenantDb({ db: sp, tenantId, mode: "system" }),
|
|
97
|
+
);
|
|
98
|
+
return (await opts.notifyRecipient?.({ ...ctx, systemDb: savepointSystemDb })) || null;
|
|
99
|
+
});
|
|
85
100
|
} catch (err) {
|
|
86
101
|
ctx.log?.warn("[waitlist:submit] admin notice recipient resolution failed", {
|
|
87
102
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -129,7 +144,12 @@ export function createSubmitHandler(opts: WaitlistOptions) {
|
|
|
129
144
|
message: "waitlist:submit requires ctx.systemDb — is r.systemScope() set?",
|
|
130
145
|
});
|
|
131
146
|
}
|
|
132
|
-
const adminRecipient = await resolveAdminRecipient(
|
|
147
|
+
const adminRecipient = await resolveAdminRecipient(
|
|
148
|
+
opts,
|
|
149
|
+
ctx,
|
|
150
|
+
ctx.systemDb,
|
|
151
|
+
event.user.tenantId,
|
|
152
|
+
);
|
|
133
153
|
await acquireNamespacedAdvisoryLock(
|
|
134
154
|
ctx.systemDb.unsafeRaw(SUBMIT_LOCK_REASON),
|
|
135
155
|
SUBMIT_LOCK_NAMESPACE,
|
|
@@ -175,8 +195,7 @@ export function createSubmitHandler(opts: WaitlistOptions) {
|
|
|
175
195
|
message: payload.message,
|
|
176
196
|
}),
|
|
177
197
|
priority: "normal",
|
|
178
|
-
|
|
179
|
-
locale: resolveWaitlistMailLocale(ctx.locale),
|
|
198
|
+
locale: resolveWaitlistMailLocale(opts.adminLocale),
|
|
180
199
|
});
|
|
181
200
|
});
|
|
182
201
|
|
|
@@ -3,7 +3,7 @@ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
|
3
3
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
import { normalizeEmail, platformActor, waitlistExecutor } from "./lib.ts";
|
|
6
|
-
import type
|
|
6
|
+
import { WAITLIST_FIELD_LIMITS, type WaitlistStatus } from "./web/constants.ts";
|
|
7
7
|
|
|
8
8
|
export type WaitlistImportEntry = {
|
|
9
9
|
/** Stable identity of the source row (e.g. `offlot:<uuid>`); the new entry id derives from it. */
|
|
@@ -38,18 +38,43 @@ export function waitlistImportId(sourceKey: string): string {
|
|
|
38
38
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// The version nibble marks ids minted by waitlistImportId; legacy row ids are random UUIDs.
|
|
42
|
+
export function isWaitlistImportId(id: string): boolean {
|
|
43
|
+
return id[14] === "5";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The live submit schema caps these fields; an imported value above the cap would be
|
|
47
|
+
// stored but could never pass a later update through the same payload schema.
|
|
48
|
+
export function waitlistImportEntryWithinLimits(entry: WaitlistImportEntry): boolean {
|
|
49
|
+
const limited: ReadonlyArray<readonly [string | undefined, number]> = [
|
|
50
|
+
[entry.name, WAITLIST_FIELD_LIMITS.name],
|
|
51
|
+
[entry.email, WAITLIST_FIELD_LIMITS.email],
|
|
52
|
+
[entry.locale, WAITLIST_FIELD_LIMITS.locale],
|
|
53
|
+
[entry.company, WAITLIST_FIELD_LIMITS.company],
|
|
54
|
+
[entry.portfolio, WAITLIST_FIELD_LIMITS.portfolio],
|
|
55
|
+
[entry.message, WAITLIST_FIELD_LIMITS.message],
|
|
56
|
+
];
|
|
57
|
+
return limited.every(([value, max]) => value === undefined || value.length <= max);
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
function presentOnly(fields: Record<string, unknown>): Record<string, unknown> {
|
|
42
61
|
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
// Writes through the entity executor, so events
|
|
46
|
-
//
|
|
64
|
+
// Writes through the entity executor, so events and projection match a live submit; subject
|
|
65
|
+
// encryption applies only when the caller configured a PII KMS (the CLI wires it from env
|
|
66
|
+
// and aborts without one). No mail, no rate limit, no duplicate-by-email
|
|
47
67
|
// check: the source row is the truth and the id makes the call idempotent.
|
|
48
68
|
export async function importWaitlistEntry(
|
|
49
69
|
db: TenantDb,
|
|
50
70
|
entry: WaitlistImportEntry,
|
|
51
71
|
options: { readonly dryRun?: boolean } = {},
|
|
52
72
|
): Promise<WaitlistImportOutcome> {
|
|
73
|
+
if (!waitlistImportEntryWithinLimits(entry)) {
|
|
74
|
+
throw new InternalError({
|
|
75
|
+
message: `waitlist import entry ${entry.sourceKey} exceeds a field length limit`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
53
78
|
const id = waitlistImportId(entry.sourceKey);
|
|
54
79
|
const existing = await waitlistExecutor.detail({ id }, platformActor(), db);
|
|
55
80
|
if (existing) return "skipped";
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createTenantDb,
|
|
10
10
|
selectMany,
|
|
11
11
|
table,
|
|
12
|
+
tableExists,
|
|
12
13
|
text as textColumn,
|
|
13
14
|
timestamp,
|
|
14
15
|
transaction,
|
|
@@ -23,9 +24,17 @@ import {
|
|
|
23
24
|
} from "@cosmicdrift/kumiko-framework/event-store";
|
|
24
25
|
import type { DbConnection } from "@cosmicdrift/kumiko-types/db-connection";
|
|
25
26
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
importWaitlistEntry,
|
|
29
|
+
isWaitlistImportId,
|
|
30
|
+
type WaitlistImportEntry,
|
|
31
|
+
waitlistImportEntryWithinLimits,
|
|
32
|
+
waitlistImportId,
|
|
33
|
+
} from "./import.ts";
|
|
27
34
|
import { WAITLIST_STATUS, type WaitlistStatus } from "./web/constants.ts";
|
|
28
35
|
|
|
36
|
+
const ARCHIVED_STREAMS_TABLE = "public.kumiko_archived_streams";
|
|
37
|
+
|
|
29
38
|
export const LEGACY_WAITLIST_SOURCES = ["offlot", "publicstatus"] as const;
|
|
30
39
|
export type LegacyWaitlistSource = (typeof LEGACY_WAITLIST_SOURCES)[number];
|
|
31
40
|
|
|
@@ -210,15 +219,19 @@ async function archiveLegacyStream(
|
|
|
210
219
|
db: DbConnection,
|
|
211
220
|
legacyId: string,
|
|
212
221
|
importedId: string,
|
|
222
|
+
dryRun: boolean,
|
|
213
223
|
): Promise<number> {
|
|
214
224
|
if (legacyId === importedId) return 0;
|
|
215
|
-
|
|
225
|
+
// A dry run never creates the archive table, so a missing one means nothing is archived yet.
|
|
226
|
+
const archiveTableReady = !dryRun || (await tableExists(db, ARCHIVED_STREAMS_TABLE));
|
|
227
|
+
if (archiveTableReady && (await isStreamArchived(db, SYSTEM_TENANT_ID, legacyId))) return 0;
|
|
216
228
|
const legacyEvents = await countWhere(db, eventsTable, {
|
|
217
229
|
tenantId: SYSTEM_TENANT_ID,
|
|
218
230
|
aggregateId: legacyId,
|
|
219
231
|
aggregateType: LEGACY_AGGREGATE_TYPE,
|
|
220
232
|
});
|
|
221
233
|
if (legacyEvents === 0) return 0;
|
|
234
|
+
if (dryRun) return 1;
|
|
222
235
|
await archiveStream(db, {
|
|
223
236
|
tenantId: SYSTEM_TENANT_ID,
|
|
224
237
|
aggregateId: legacyId,
|
|
@@ -243,11 +256,17 @@ export async function migrateLegacyWaitlist(
|
|
|
243
256
|
let archivedStreams = 0;
|
|
244
257
|
if (!options.dryRun && options.from === "offlot") await createArchivedStreamsTable(db);
|
|
245
258
|
for (const row of rows) {
|
|
259
|
+
// The offlot default source is the entity table: rows a previous run imported
|
|
260
|
+
// are already migrated entries and must neither be re-imported nor archived.
|
|
261
|
+
if (options.from === "offlot" && isWaitlistImportId(String(row["id"]))) {
|
|
262
|
+
skipped += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
246
265
|
const entry =
|
|
247
266
|
options.from === "offlot"
|
|
248
267
|
? await mapOfflotRow(row)
|
|
249
268
|
: mapPublicstatusRow(row, options.defaultLocale);
|
|
250
|
-
if (!entry) {
|
|
269
|
+
if (!entry || !waitlistImportEntryWithinLimits(entry)) {
|
|
251
270
|
unreadable += 1;
|
|
252
271
|
continue;
|
|
253
272
|
}
|
|
@@ -257,11 +276,12 @@ export async function migrateLegacyWaitlist(
|
|
|
257
276
|
if (outcome === "created") created += 1;
|
|
258
277
|
else skipped += 1;
|
|
259
278
|
});
|
|
260
|
-
if (
|
|
279
|
+
if (options.from === "offlot") {
|
|
261
280
|
archivedStreams += await archiveLegacyStream(
|
|
262
281
|
db,
|
|
263
282
|
String(row["id"]),
|
|
264
283
|
waitlistImportId(entry.sourceKey),
|
|
284
|
+
options.dryRun,
|
|
265
285
|
);
|
|
266
286
|
}
|
|
267
287
|
}
|
|
@@ -75,22 +75,25 @@ export const linkWaitlistOnSignup: MultiStreamApplyFn = async (event, tx) => {
|
|
|
75
75
|
if (typeof user?.email !== "string") return;
|
|
76
76
|
const email = normalizeEmail(await decryptStoredPii(user.email, "email", PII_DECRYPT_REASON));
|
|
77
77
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
const findInvited = async () => {
|
|
79
|
+
const found = await waitlistExecutor.list(
|
|
80
|
+
{
|
|
81
|
+
filter: { field: "email", op: "eq", value: email },
|
|
82
|
+
filters: [{ field: "status", op: "in", value: [WAITLIST_STATUS.Invited] }],
|
|
83
|
+
},
|
|
84
|
+
platformActor(),
|
|
85
|
+
db,
|
|
86
|
+
);
|
|
87
|
+
return [...found.rows].sort(newestSubmittedFirst)[0];
|
|
88
|
+
};
|
|
89
|
+
const invited = await findInvited();
|
|
87
90
|
// skip: the signup email has no invited waitlist entry (open signup or stale invite).
|
|
88
91
|
if (!invited) return;
|
|
89
92
|
const { id, version } = invited;
|
|
90
93
|
// skip: defensive guard on the row shape.
|
|
91
94
|
if (typeof id !== "string" || typeof version !== "number") return;
|
|
92
95
|
|
|
93
|
-
await waitlistExecutor.update(
|
|
96
|
+
const updated = await waitlistExecutor.update(
|
|
94
97
|
{
|
|
95
98
|
id,
|
|
96
99
|
version,
|
|
@@ -103,4 +106,11 @@ export const linkWaitlistOnSignup: MultiStreamApplyFn = async (event, tx) => {
|
|
|
103
106
|
platformActor(),
|
|
104
107
|
db,
|
|
105
108
|
);
|
|
109
|
+
// skip: linked.
|
|
110
|
+
if (updated.isSuccess) return;
|
|
111
|
+
// A concurrent admin change that already moved the entry out of `invited` makes the link
|
|
112
|
+
// moot; otherwise throw so the at-least-once consumer redelivers.
|
|
113
|
+
// skip: the entry left `invited` concurrently.
|
|
114
|
+
if ((await findInvited())?.id !== id) return;
|
|
115
|
+
throw new Error(`waitlist:link-on-signup: could not link waitlist entry ${id}`);
|
|
106
116
|
};
|
|
@@ -22,8 +22,12 @@ export type WaitlistInviteOptions =
|
|
|
22
22
|
export type WaitlistOptions = {
|
|
23
23
|
/** Address for an admin notification mail on each new entry. Null, an empty
|
|
24
24
|
* string or an absent option sends none. Best effort, never fails the submit,
|
|
25
|
-
* also when the resolver throws or rejects.
|
|
25
|
+
* also when the resolver throws or rejects. `ctx.systemDb` is bound to a savepoint, so a
|
|
26
|
+
* failing statement through it is rolled back without aborting the submit transaction. */
|
|
26
27
|
readonly notifyRecipient?: (ctx: HandlerContext) => string | null | Promise<string | null>;
|
|
28
|
+
/** Language of the admin notice mail (en, de or es; anything else falls back to en).
|
|
29
|
+
* Not taken from the request: a submit comes from the entrant, not the operator. */
|
|
30
|
+
readonly adminLocale?: string;
|
|
27
31
|
/** Default `{ mode: "own-tenant", role: "TenantAdmin" }`. */
|
|
28
32
|
readonly invite?: WaitlistInviteOptions;
|
|
29
33
|
/** Default: 5 submits per IP per 10 minutes. Pass `{ disabled: true, reason }`
|
|
@@ -229,7 +229,7 @@ function readTriggerSource(startedPayload: unknown): TriggerSource | null {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
async function loadReferencedEvent(ctx: HandlerContext, ref: TriggerEventRef) {
|
|
232
|
-
const events = await ctx.loadAggregate(ref.aggregateId);
|
|
232
|
+
const events = await ctx.loadAggregate(ref.aggregateId, { atVersion: ref.version });
|
|
233
233
|
return events.find((e) => e.id === ref.eventId);
|
|
234
234
|
}
|
|
235
235
|
|
|
@@ -7,6 +7,7 @@ import { styleguideClient } from "../features/demo/web";
|
|
|
7
7
|
import { examplesClient } from "../features/examples/web";
|
|
8
8
|
import { galleryClient } from "../features/gallery/web";
|
|
9
9
|
import { widgetsClient } from "../features/widgets/web";
|
|
10
|
+
import { workboardClient } from "../features/workboard/web";
|
|
10
11
|
import { AppShell } from "./shell";
|
|
11
12
|
|
|
12
13
|
// createKumikoApp fetches the schema itself from the authenticated
|
|
@@ -24,5 +25,6 @@ createKumikoApp({
|
|
|
24
25
|
widgetsClient,
|
|
25
26
|
examplesClient,
|
|
26
27
|
contentClient(),
|
|
28
|
+
workboardClient,
|
|
27
29
|
],
|
|
28
30
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
|
|
2
|
-
import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { buildEntityTable, createTenantDb, fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
|
|
5
5
|
|
|
@@ -8,9 +8,6 @@ import { demoEntity } from "../features/demo/feature";
|
|
|
8
8
|
// Gibt der List-View echte Rows mit Status-/Flag-Vielfalt. price (money)
|
|
9
9
|
// bleibt leer — der Money-Input zeigt sich im Edit-Screen, im Seed lassen wir
|
|
10
10
|
// ihn weg um nicht von der money-Payload-Form abzuhängen.
|
|
11
|
-
// ponytail: kein Idempotenz-Check — die Screenshot-DB ist ephemer (frisch pro
|
|
12
|
-
// Boot). Beim `bun run dev` gegen die persistente Dev-DB dupliziert ein
|
|
13
|
-
// Neustart die Rows; das ist hier nur eine Demo, daher egal.
|
|
14
11
|
const STATUSES = ["draft", "review", "published", "archived"] as const;
|
|
15
12
|
|
|
16
13
|
// Fixed across boots (unlike the framework-minted ids for every other seed
|
|
@@ -19,7 +16,12 @@ const STATUSES = ["draft", "review", "published", "archived"] as const;
|
|
|
19
16
|
// otherwise change every run.
|
|
20
17
|
export const FIRST_STYLEGUIDE_ITEM_ID = "00000000-0000-4000-8000-0000000000f1";
|
|
21
18
|
|
|
19
|
+
const demoItemTable = buildEntityTable("item", demoEntity);
|
|
20
|
+
|
|
22
21
|
export const seedStyleguideItems: SeedFn = async (stack) => {
|
|
22
|
+
// The first row has a fixed id, so its presence means a persistent dev DB was seeded already.
|
|
23
|
+
if (await fetchOne(stack.db, demoItemTable, { id: FIRST_STYLEGUIDE_ITEM_ID })) return;
|
|
24
|
+
|
|
23
25
|
// Bypass the write-handler HTTP path for the first row — its schema has
|
|
24
26
|
// no `id` field (buildInsertSchema only exposes entity fields), so an
|
|
25
27
|
// explicit id would be silently stripped before reaching the executor.
|
|
@@ -20,6 +20,8 @@ import { examplesFeature } from "../features/examples/feature";
|
|
|
20
20
|
import { galleryFeature } from "../features/gallery/feature";
|
|
21
21
|
import { aiTextDemoFeature } from "../features/widgets/ai-text-demo";
|
|
22
22
|
import { widgetsFeature } from "../features/widgets/feature";
|
|
23
|
+
import { workboardFeature } from "../features/workboard/feature";
|
|
24
|
+
import { seedWorkboardTasks } from "../features/workboard/seed";
|
|
23
25
|
import { seedStyleguideItems } from "./seed";
|
|
24
26
|
|
|
25
27
|
const configResolver = createConfigResolver();
|
|
@@ -35,6 +37,7 @@ await runDevApp({
|
|
|
35
37
|
contentFeature,
|
|
36
38
|
aiTextDemoFeature,
|
|
37
39
|
aiSettingsFeature,
|
|
40
|
+
workboardFeature,
|
|
38
41
|
],
|
|
39
42
|
// Aktiviert die Upload-Routes (/api/files) + ctx.files für den Avatar-
|
|
40
43
|
// Stresstest. In-Memory reicht für die Demo (nicht-persistent).
|
|
@@ -43,7 +46,7 @@ await runDevApp({
|
|
|
43
46
|
clientEntry: "./src/app/client.tsx",
|
|
44
47
|
htmlPath: "./public/index.html",
|
|
45
48
|
watchDirs: ["./src", "../../../packages/*/src"],
|
|
46
|
-
seeds: [seedStyleguideItems, seedAiConnections],
|
|
49
|
+
seeds: [seedStyleguideItems, seedAiConnections, seedWorkboardTasks],
|
|
47
50
|
extraContext: ({ registry }) => ({
|
|
48
51
|
configResolver,
|
|
49
52
|
_configAccessorFactory: createConfigAccessorFactory(registry, configResolver),
|