@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
|
@@ -85,10 +85,19 @@ export type SeedSystemTemplateOptions = {
|
|
|
85
85
|
readonly ifExists?: SeedIfExists;
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
// Namespace prefix plus path, no whitespace: keeps free text from passing as a key that the UI
|
|
89
|
+
// would then show raw.
|
|
90
|
+
const I18N_KEY_FORMAT = /^[^\s:]+:\S+$/;
|
|
91
|
+
|
|
88
92
|
function seedTitleColumn(opts: SeedSystemTemplateOptions, label: string): string | null {
|
|
89
93
|
if (opts.title !== undefined && opts.titleI18nKey !== undefined) {
|
|
90
94
|
throw new Error(`${label} rejected: set either title or titleI18nKey, not both`);
|
|
91
95
|
}
|
|
96
|
+
if (opts.titleI18nKey !== undefined && !I18N_KEY_FORMAT.test(opts.titleI18nKey)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`${label} rejected: titleI18nKey "${opts.titleI18nKey}" is not a "namespace:key" i18n key`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
92
101
|
return opts.titleI18nKey ?? opts.title ?? null;
|
|
93
102
|
}
|
|
94
103
|
|
|
@@ -167,21 +176,20 @@ export async function seedSystemTemplate(
|
|
|
167
176
|
current?.title === rowFields.title &&
|
|
168
177
|
current?.variableSchema === rowFields.variableSchema &&
|
|
169
178
|
current?.meta === rowFields.meta &&
|
|
170
|
-
current?.status === rowFields.status &&
|
|
171
179
|
current?.parentTemplateId === rowFields.parentTemplateId &&
|
|
172
180
|
(linkedResources === undefined || current?.linkedResources === linkedResources)
|
|
173
181
|
) {
|
|
174
182
|
return { id: String(row.id) };
|
|
175
183
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
184
|
+
// An archived system row is a SystemAdmin decision (reversible with publish-system) that a
|
|
185
|
+
// pod-boot re-seed must not undo; content still follows the code. The seed replaces the
|
|
186
|
+
// content, so an open draft on the old content is dropped with it.
|
|
187
|
+
const { status: _status, ...contentFields } = rowFields;
|
|
188
|
+
const changes =
|
|
189
|
+
current?.status === "archived"
|
|
190
|
+
? { ...contentFields, status: "archived" as const, draft: null }
|
|
191
|
+
: { ...rowFields, ...activeStampChanges(row), draft: null };
|
|
192
|
+
const result = await executor.update({ id: row.id, version: row.version, changes }, by, tdb);
|
|
185
193
|
if (!result.isSuccess) {
|
|
186
194
|
throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
|
|
187
195
|
}
|
|
@@ -292,8 +300,10 @@ async function seedSystemTemplateBundleWithResources(
|
|
|
292
300
|
`${label}: bundles with resources need a file provider; pass opts.dispatcher (the dispatcher of the seed deps) so the files are stored through the import-bundle-system write`,
|
|
293
301
|
);
|
|
294
302
|
}
|
|
295
|
-
|
|
296
|
-
|
|
303
|
+
const skipExisting = (opts.ifExists ?? "skip") === "skip";
|
|
304
|
+
const existingIds: Record<string, string> = {};
|
|
305
|
+
let bundleToImport = bundle;
|
|
306
|
+
if (skipExisting) {
|
|
297
307
|
for (const locale of Object.keys(bundle.locales)) {
|
|
298
308
|
const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
299
309
|
tenantId: SYSTEM_TENANT_ID,
|
|
@@ -306,16 +316,24 @@ async function seedSystemTemplateBundleWithResources(
|
|
|
306
316
|
if (Object.keys(existingIds).length === Object.keys(bundle.locales).length) {
|
|
307
317
|
return { ids: existingIds };
|
|
308
318
|
}
|
|
319
|
+
// import-bundle-system always overwrites, so only the missing locales go in: a
|
|
320
|
+
// hand-edited system row of an already seeded locale must survive the new-locale boot.
|
|
321
|
+
bundleToImport = {
|
|
322
|
+
...bundle,
|
|
323
|
+
locales: Object.fromEntries(
|
|
324
|
+
Object.entries(bundle.locales).filter(([locale]) => !(locale in existingIds)),
|
|
325
|
+
),
|
|
326
|
+
};
|
|
309
327
|
}
|
|
310
328
|
const result = await dispatcher.write(
|
|
311
329
|
TemplateResolverHandlers.importBundleSystem,
|
|
312
|
-
{ bundle },
|
|
330
|
+
{ bundle: bundleToImport },
|
|
313
331
|
opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]),
|
|
314
332
|
);
|
|
315
333
|
if (!result.isSuccess) {
|
|
316
334
|
throw new Error(`${label} failed: ${result.error.code}: ${result.error.message}`);
|
|
317
335
|
}
|
|
318
|
-
return { ids: importedIds(result.data) };
|
|
336
|
+
return { ids: { ...existingIds, ...importedIds(result.data) } };
|
|
319
337
|
}
|
|
320
338
|
|
|
321
339
|
function importedIds(data: unknown): Record<string, string> {
|
|
@@ -424,12 +442,14 @@ export async function seedTextBlock(
|
|
|
424
442
|
row: { id: string; version: number },
|
|
425
443
|
current: TemplateResourceRow | null | undefined,
|
|
426
444
|
): Promise<{ id: string }> => {
|
|
445
|
+
// No folder option keeps the row's folder, so a re-seed cannot drop it from the content tree.
|
|
446
|
+
const folder = opts.folder === undefined ? (current?.folder ?? null) : fields.folder;
|
|
427
447
|
// Skip no-op updates so a legal re-seed on every pod boot does not bump
|
|
428
448
|
// version/modifiedAt (ETag, "last changed", event-store growth).
|
|
429
449
|
if (
|
|
430
450
|
current?.title === fields.title &&
|
|
431
451
|
current?.content === fields.content &&
|
|
432
|
-
current?.folder ===
|
|
452
|
+
current?.folder === folder
|
|
433
453
|
) {
|
|
434
454
|
return { id: String(row.id) };
|
|
435
455
|
}
|
|
@@ -440,7 +460,7 @@ export async function seedTextBlock(
|
|
|
440
460
|
changes: {
|
|
441
461
|
title: fields.title,
|
|
442
462
|
content: fields.content,
|
|
443
|
-
folder
|
|
463
|
+
folder,
|
|
444
464
|
// A re-seed must not revive an archived block; only a served one gets a new served version.
|
|
445
465
|
...(current?.status === "active" && servedStampChanges(row)),
|
|
446
466
|
},
|
|
@@ -515,7 +535,11 @@ export type LegalContentBlock = {
|
|
|
515
535
|
export async function seedLegalContentFromJson(
|
|
516
536
|
db: DbConnection,
|
|
517
537
|
blocks: readonly LegalContentBlock[],
|
|
518
|
-
opts: {
|
|
538
|
+
opts: {
|
|
539
|
+
readonly tenantId?: TenantId;
|
|
540
|
+
readonly by?: SessionUser;
|
|
541
|
+
readonly folder?: string;
|
|
542
|
+
} = {},
|
|
519
543
|
): Promise<void> {
|
|
520
544
|
const tenantId = opts.tenantId ?? SYSTEM_TENANT_ID;
|
|
521
545
|
const by = opts.by ?? createSystemUser(tenantId);
|
|
@@ -526,6 +550,7 @@ export async function seedLegalContentFromJson(
|
|
|
526
550
|
locale: block.locale,
|
|
527
551
|
title: block.title,
|
|
528
552
|
content: block.content,
|
|
553
|
+
...(opts.folder !== undefined && { folder: opts.folder }),
|
|
529
554
|
ifExists: "update",
|
|
530
555
|
by,
|
|
531
556
|
});
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type UploadedFileBytes,
|
|
20
20
|
uploadFileBytesBeforeTransaction,
|
|
21
21
|
} from "@cosmicdrift/kumiko-framework/files";
|
|
22
|
-
import { fileContextForTenant } from "../file-foundation/index.ts";
|
|
22
|
+
import { fileContextForTenant, readFileRefContent } from "../file-foundation/index.ts";
|
|
23
23
|
import { extensionForResourceContentType, requireForTenant } from "../shared/index.ts";
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_RESOURCE_MAX_BYTES,
|
|
@@ -29,13 +29,13 @@ import {
|
|
|
29
29
|
import { executor } from "./handlers/shared.ts";
|
|
30
30
|
import {
|
|
31
31
|
attachmentEntriesOf,
|
|
32
|
+
isFileAttachmentEntry,
|
|
32
33
|
type LinkedResourceEntry,
|
|
33
34
|
parseLinkedResourceEntries,
|
|
34
35
|
templatePdfAttachmentLinkSchema,
|
|
35
36
|
} from "./linked-resources.ts";
|
|
36
37
|
import { normalizeResourceImage } from "./resource-image.ts";
|
|
37
38
|
import { type TemplateResourceRow, templateResourcesTable } from "./table.ts";
|
|
38
|
-
import { readTemplateResource } from "./template-resources.ts";
|
|
39
39
|
import { ACTIVE_TEMPLATE_STATUS, SYSTEM_TENANT_ID } from "./web/constants.ts";
|
|
40
40
|
import { TemplateResolverErrors } from "./web/qualified-names.ts";
|
|
41
41
|
|
|
@@ -87,6 +87,10 @@ function keyInUse(): WriteResult<never> {
|
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function isImageKey(links: Readonly<Record<string, LinkedResourceEntry>>, key: string) {
|
|
91
|
+
return typeof links[key] === "string";
|
|
92
|
+
}
|
|
93
|
+
|
|
90
94
|
function isAttachmentKey(links: Readonly<Record<string, LinkedResourceEntry>>, key: string) {
|
|
91
95
|
const occupant = links[key];
|
|
92
96
|
return occupant !== undefined && typeof occupant !== "string";
|
|
@@ -265,6 +269,20 @@ export async function releaseAll(releases: readonly Release[]): Promise<void> {
|
|
|
265
269
|
if (rejected?.status === "rejected") throw rejected.reason;
|
|
266
270
|
}
|
|
267
271
|
|
|
272
|
+
// Failure paths: a failing release must not replace the upload error the caller needs to see.
|
|
273
|
+
export async function releaseAfterFailure(
|
|
274
|
+
ctx: HandlerContext,
|
|
275
|
+
releases: readonly Release[],
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
try {
|
|
278
|
+
await releaseAll(releases);
|
|
279
|
+
} catch (releaseError) {
|
|
280
|
+
ctx.log?.warn("template upload rollback: releasing stored files failed", {
|
|
281
|
+
error: releaseError instanceof Error ? releaseError.message : String(releaseError),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
268
286
|
// `upload` is absent for a rendered-PDF link, which carries no file.
|
|
269
287
|
export type PreparedLinkedResourceCopy = {
|
|
270
288
|
readonly key: string;
|
|
@@ -272,6 +290,44 @@ export type PreparedLinkedResourceCopy = {
|
|
|
272
290
|
readonly upload?: UploadedFileBytes;
|
|
273
291
|
};
|
|
274
292
|
|
|
293
|
+
// The source row's own fileName is personal data and may be ciphertext, so the copy is named from
|
|
294
|
+
// the link entry or the key.
|
|
295
|
+
function copiedFileName(key: string, entry: LinkedResourceEntry, contentType: string): string {
|
|
296
|
+
if (typeof entry !== "string" && isFileAttachmentEntry(entry)) return entry.fileName;
|
|
297
|
+
return `${key}.${extensionForResourceContentType(contentType) ?? "bin"}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function sourceUnreadable(): WriteResult<never> {
|
|
301
|
+
return writeFailure(
|
|
302
|
+
new UnprocessableError(TemplateResolverErrors.resourceSourceUnreadable, {
|
|
303
|
+
i18nKey: "templateResolver.errors.resource.sourceUnreadable",
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Undefined: the fileRef row exists but its bytes cannot be read, which must not look like a
|
|
309
|
+
// deleted file. The provider is chosen by the tenant of the fileRef, not by the caller's.
|
|
310
|
+
async function readCopySource(
|
|
311
|
+
ctx: HandlerContext,
|
|
312
|
+
fileRefId: string,
|
|
313
|
+
): Promise<{ contentType: string; bytes: Uint8Array } | null | undefined> {
|
|
314
|
+
try {
|
|
315
|
+
const found = await readFileRefContent({
|
|
316
|
+
db: ctx.db,
|
|
317
|
+
filesForTenant: (tid) => fileContextForTenant(ctx, ctx.user.tenantId, tid),
|
|
318
|
+
fileRefId,
|
|
319
|
+
allowedTenantIds: [SYSTEM_TENANT_ID, ctx.user.tenantId],
|
|
320
|
+
});
|
|
321
|
+
return found === null ? null : { contentType: found.contentType, bytes: found.content };
|
|
322
|
+
} catch (cause) {
|
|
323
|
+
ctx.log?.warn("template copy: source file could not be read", {
|
|
324
|
+
fileRefId,
|
|
325
|
+
errorClass: cause instanceof Error ? cause.name : typeof cause,
|
|
326
|
+
});
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
275
331
|
// beforeTransaction half: copies the file behind every entry into the target tenant's storage, so a
|
|
276
332
|
// template copied from the system default keeps working once the tenant exports it or measures its
|
|
277
333
|
// attachments. An entry whose source file is gone is left out: it could not be resolved at render
|
|
@@ -291,24 +347,30 @@ export async function prepareLinkedResourceCopiesBeforeTransaction(args: {
|
|
|
291
347
|
copies.push({ key, entry });
|
|
292
348
|
continue;
|
|
293
349
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
350
|
+
const source = await readCopySource(ctx, typeof entry === "string" ? entry : entry.fileRefId);
|
|
351
|
+
if (source === undefined) {
|
|
352
|
+
await releaseAfterFailure(ctx, releases);
|
|
353
|
+
return sourceUnreadable();
|
|
354
|
+
}
|
|
355
|
+
// skip: the source fileRef is deleted or unknown, so there is nothing to copy
|
|
356
|
+
if (source === null) continue;
|
|
299
357
|
const uploaded = await uploadToTenant(
|
|
300
358
|
{ ctx, user, tenantId },
|
|
301
|
-
{
|
|
359
|
+
{
|
|
360
|
+
fileName: copiedFileName(key, entry, source.contentType),
|
|
361
|
+
mimeType: source.contentType,
|
|
362
|
+
data: source.bytes,
|
|
363
|
+
},
|
|
302
364
|
);
|
|
303
365
|
if (!uploaded.isSuccess) {
|
|
304
|
-
await
|
|
366
|
+
await releaseAfterFailure(ctx, releases);
|
|
305
367
|
return uploaded;
|
|
306
368
|
}
|
|
307
369
|
if (uploaded.release) releases.push(uploaded.release);
|
|
308
370
|
copies.push({ key, entry, upload: uploaded.data });
|
|
309
371
|
}
|
|
310
372
|
} catch (cause) {
|
|
311
|
-
await
|
|
373
|
+
await releaseAfterFailure(ctx, releases);
|
|
312
374
|
throw cause;
|
|
313
375
|
}
|
|
314
376
|
return { isSuccess: true, data: copies, release: () => releaseAll(releases) };
|
|
@@ -481,8 +543,7 @@ async function checkAttachmentTarget(args: {
|
|
|
481
543
|
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
482
544
|
return attachmentLimitFailure();
|
|
483
545
|
}
|
|
484
|
-
|
|
485
|
-
if (occupant !== undefined && typeof occupant === "string") return keyInUse();
|
|
546
|
+
if (isImageKey(linked, key)) return keyInUse();
|
|
486
547
|
return { isSuccess: true, data: { row, linked } };
|
|
487
548
|
}
|
|
488
549
|
|
|
@@ -638,6 +699,7 @@ export async function linkTemplatePdfAttachment(args: {
|
|
|
638
699
|
if (row.kind !== "mail") return attachmentNeedsMailTemplate();
|
|
639
700
|
|
|
640
701
|
const linked = parseLinkedResourceEntries(row.linkedResources);
|
|
702
|
+
if (isImageKey(linked, key)) return keyInUse();
|
|
641
703
|
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
642
704
|
return attachmentLimitFailure();
|
|
643
705
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
|
-
import { isSystemAdmin } from "./template-draft.ts";
|
|
4
3
|
import { SYSTEM_TENANT_ID } from "./web/constants.ts";
|
|
5
4
|
import { TEMPLATE_RESOLVER_FEATURE } from "./web/qualified-names.ts";
|
|
6
5
|
|
|
@@ -45,16 +44,6 @@ export function isRowServed(
|
|
|
45
44
|
return row.tenantId === SYSTEM_TENANT_ID || isTenantOverrideServed(policy, row);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
// A SystemAdmin sees every stored row, so a locked override can still be reset; everyone else
|
|
49
|
-
// sees only what is delivered, which keeps a locked override out of the template-resolver queries for a TenantAdmin (the audit log keeps the history).
|
|
50
|
-
export function isRowServedTo(
|
|
51
|
-
policy: TemplateDeliveryPolicy,
|
|
52
|
-
user: { readonly roles: readonly string[] },
|
|
53
|
-
row: DeliveryTarget & { readonly tenantId: string },
|
|
54
|
-
): boolean {
|
|
55
|
-
return isSystemAdmin(user) || isRowServed(policy, row);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
47
|
function isDeliveryTarget(value: unknown): value is DeliveryTarget {
|
|
59
48
|
return (
|
|
60
49
|
typeof propertyOf(value, "kind") === "string" && typeof propertyOf(value, "slug") === "string"
|
|
@@ -6,6 +6,12 @@ import {
|
|
|
6
6
|
type TemplateKind,
|
|
7
7
|
} from "./web/constants.ts";
|
|
8
8
|
import { MAIL_TONES, type MailTone } from "./web/mail-constants.ts";
|
|
9
|
+
import {
|
|
10
|
+
MAIL_LAYOUT_ACCENT_LINES,
|
|
11
|
+
MAIL_LAYOUT_BUTTON_STYLES,
|
|
12
|
+
MAIL_LAYOUT_FONTS,
|
|
13
|
+
MAIL_LAYOUT_WIDTHS,
|
|
14
|
+
} from "./web/mail-layout-constants.ts";
|
|
9
15
|
|
|
10
16
|
// One or more kebab segments joined by ":" — notification templates are seeded
|
|
11
17
|
// under their qualified name `feature:notify:name`.
|
|
@@ -89,13 +95,15 @@ const pdfMetaSchema = z
|
|
|
89
95
|
})
|
|
90
96
|
.strict();
|
|
91
97
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
const MAIL_LAYOUT_WIDTH_LITERALS = [
|
|
99
|
+
z.literal(MAIL_LAYOUT_WIDTHS[0]),
|
|
100
|
+
z.literal(MAIL_LAYOUT_WIDTHS[1]),
|
|
101
|
+
z.literal(MAIL_LAYOUT_WIDTHS[2]),
|
|
102
|
+
] as const;
|
|
95
103
|
|
|
96
104
|
const mailLayoutMetaSchema = z
|
|
97
105
|
.object({
|
|
98
|
-
width: z.union(
|
|
106
|
+
width: z.union(MAIL_LAYOUT_WIDTH_LITERALS).optional(),
|
|
99
107
|
font: z.enum(MAIL_LAYOUT_FONTS).optional(),
|
|
100
108
|
outerBackground: z.string().regex(HEX_COLOR_PATTERN, "must be a #rrggbb color").optional(),
|
|
101
109
|
accentLine: z.enum(MAIL_LAYOUT_ACCENT_LINES).optional(),
|
|
@@ -28,7 +28,12 @@ import {
|
|
|
28
28
|
RendererError,
|
|
29
29
|
type RendererPlugin,
|
|
30
30
|
} from "../renderer-foundation/index.ts";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createTemplateResolverApi,
|
|
33
|
+
isRowDeliveredTo,
|
|
34
|
+
TemplateNotFoundError,
|
|
35
|
+
type TemplateResource,
|
|
36
|
+
} from "./api.ts";
|
|
32
37
|
import { collectDocumentResourceKeys } from "./compile-document-body.ts";
|
|
33
38
|
import { parseStoredJson } from "./handlers/template-gates.ts";
|
|
34
39
|
import { validateTemplateDraft } from "./handlers/validate.query.ts";
|
|
@@ -41,7 +46,7 @@ import {
|
|
|
41
46
|
import { type LinkedResourceEntry, parseLinkedResourceEntries } from "./linked-resources.ts";
|
|
42
47
|
import { buildDisplayHtml } from "./preview-display-html.ts";
|
|
43
48
|
import { type TemplateResourceRow, templateResourcesTable } from "./table.ts";
|
|
44
|
-
import { deliveryPolicyOf
|
|
49
|
+
import { deliveryPolicyOf } from "./template-delivery-policy.ts";
|
|
45
50
|
import {
|
|
46
51
|
isMailTemplateMeta,
|
|
47
52
|
type MailTemplateMeta,
|
|
@@ -327,7 +332,10 @@ async function loadEditedRowResources(
|
|
|
327
332
|
{ limit: 1 },
|
|
328
333
|
);
|
|
329
334
|
const row = rows[0];
|
|
330
|
-
if (
|
|
335
|
+
if (
|
|
336
|
+
row !== undefined &&
|
|
337
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, row))
|
|
338
|
+
) {
|
|
331
339
|
return undefined;
|
|
332
340
|
}
|
|
333
341
|
return row === undefined ? undefined : parseLinkedResourceEntries(row.linkedResources);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// RenderKind identifies the consumer class of a template. Plugin renderers in
|
|
5
5
|
// `renderer-foundation` match on kind; the resolver here is kind-agnostic: it
|
|
6
6
|
// only loads, and the content format (markdown/mjml/html) decides who renders it.
|
|
7
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
8
|
+
|
|
7
9
|
export const RENDER_KINDS = ["mail", "document-pdf", "image-snapshot"] as const;
|
|
8
10
|
export type RenderKind = (typeof RENDER_KINDS)[number];
|
|
9
11
|
|
|
@@ -55,8 +57,10 @@ export const COLLECTION_REFUSED_KINDS = [
|
|
|
55
57
|
"pdf-layout",
|
|
56
58
|
] as const satisfies readonly TemplateKind[];
|
|
57
59
|
|
|
58
|
-
export function isCollectionRefusedKind(
|
|
59
|
-
|
|
60
|
+
export function isCollectionRefusedKind(
|
|
61
|
+
kind: unknown,
|
|
62
|
+
): kind is (typeof COLLECTION_REFUSED_KINDS)[number] {
|
|
63
|
+
return isOneOf(COLLECTION_REFUSED_KINDS, kind);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"] as const;
|
|
@@ -69,6 +73,7 @@ export const TEMPLATE_STATUSES = ["draft", "active", "archived"] as const;
|
|
|
69
73
|
export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
|
|
70
74
|
export const ACTIVE_TEMPLATE_STATUS = "active" satisfies TemplateStatus;
|
|
71
75
|
|
|
76
|
+
export { HEX_COLOR_PATTERN } from "./mail-layout-constants.ts";
|
|
72
77
|
export {
|
|
73
78
|
PDF_DEFAULT_MARGIN_MM,
|
|
74
79
|
PDF_FOOTER_MARGIN_MM,
|
|
@@ -79,9 +84,6 @@ export {
|
|
|
79
84
|
type PdfPageFormat,
|
|
80
85
|
} from "./pdf-constants.ts";
|
|
81
86
|
|
|
82
|
-
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
83
|
-
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
84
|
-
|
|
85
87
|
export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
86
88
|
export const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
87
89
|
|
|
@@ -90,6 +90,10 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
90
90
|
"templateResolver.errors.patch.textNotFound": "The text {find} was not found in the block.",
|
|
91
91
|
"templateResolver.errors.patch.textAmbiguous":
|
|
92
92
|
"The text {find} occurs {count} times in the block; use a longer text.",
|
|
93
|
+
"templateResolver.errors.patch.textSpansFormatting":
|
|
94
|
+
"The text {find} crosses a formatting change in the block; replace each formatted part on its own.",
|
|
95
|
+
"templateResolver.errors.patch.placeholderNotReplaceable":
|
|
96
|
+
"The placeholder {find} cannot be replaced as text; set the text around it with another operation or remove the block.",
|
|
93
97
|
"templateResolver.errors.patch.blockNotAllowed":
|
|
94
98
|
"The block type {type} is not available in {kind} templates.",
|
|
95
99
|
"templateResolver.errors.patch.blockTypeMismatch":
|
|
@@ -113,6 +117,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
113
117
|
"Your account has no email address to send the test mail to.",
|
|
114
118
|
"templateResolver.errors.testMail.failed": "The test mail could not be sent.",
|
|
115
119
|
"templateResolver.errors.resource.tooLarge": "The image is too large.",
|
|
120
|
+
"templateResolver.errors.resource.sourceUnreadable":
|
|
121
|
+
"A file of the source template could not be read, so nothing was copied.",
|
|
116
122
|
"templateResolver.errors.resource.unsupportedImage":
|
|
117
123
|
"This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
|
|
118
124
|
"templateResolver.errors.template.systemResourcesNeedSystemTenant":
|
|
@@ -16,6 +16,13 @@ export {
|
|
|
16
16
|
PDF_DOCUMENT_BLOCKS,
|
|
17
17
|
} from "./document-toolbar-blocks.ts";
|
|
18
18
|
export { MAIL_TONES, type MailTone } from "./mail-constants.ts";
|
|
19
|
+
export {
|
|
20
|
+
HEX_COLOR_PATTERN,
|
|
21
|
+
MAIL_LAYOUT_ACCENT_LINES,
|
|
22
|
+
MAIL_LAYOUT_BUTTON_STYLES,
|
|
23
|
+
MAIL_LAYOUT_FONTS,
|
|
24
|
+
MAIL_LAYOUT_WIDTHS,
|
|
25
|
+
} from "./mail-layout-constants.ts";
|
|
19
26
|
export {
|
|
20
27
|
PDF_DEFAULT_MARGIN_MM,
|
|
21
28
|
PDF_FOOTER_MARGIN_MM,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// No imports: web/ re-exports these, and template-meta.ts pulls in zod and the framework engine.
|
|
2
|
+
export const MAIL_LAYOUT_WIDTHS = [560, 600, 640] as const;
|
|
3
|
+
export const MAIL_LAYOUT_FONTS = ["sans", "serif"] as const;
|
|
4
|
+
export const MAIL_LAYOUT_ACCENT_LINES = ["branding", "none"] as const;
|
|
5
|
+
export const MAIL_LAYOUT_BUTTON_STYLES = ["filled-rounded", "filled-square", "outline"] as const;
|
|
6
|
+
|
|
7
|
+
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
8
|
+
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
@@ -68,6 +68,7 @@ export const TemplateResolverErrors = {
|
|
|
68
68
|
bundleLocalesDiffer: "bundle_locales_differ",
|
|
69
69
|
resourceTooLarge: "template_resource_too_large",
|
|
70
70
|
resourceUnsupportedImage: "template_resource_unsupported_image",
|
|
71
|
+
resourceSourceUnreadable: "template_resource_source_unreadable",
|
|
71
72
|
attachmentUnsupportedType: "template_attachment_unsupported_type",
|
|
72
73
|
attachmentLimit: "template_attachment_limit",
|
|
73
74
|
attachmentInvalid: "template_attachment_invalid",
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Tenant directory search contract documented",
|
|
6
|
+
"detail": "A search returns the first limit matches in tenant-key order, sorted by label afterwards; the handler description now says so."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.383.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -71,6 +71,7 @@ export function collectAssignableAppRoles(
|
|
|
71
71
|
|
|
72
72
|
export function createTenantFeature(options?: TenantFeatureOptions): FeatureDefinition {
|
|
73
73
|
return defineFeature("tenant", (r) => {
|
|
74
|
+
r.adoptsBundledLists();
|
|
74
75
|
r.describe(
|
|
75
76
|
"Registers the three core multi-tenancy entities \u2014 `tenant`, `tenant-membership`, and `tenant-invitation` (DB tables `read_tenants`, `read_tenant_memberships`, and `read_tenant_invitations`) \u2014 along with write handlers for create/update/disable/enable/addMember/removeMember/updateMemberRoles and the matching queries. It also declares a set of per-tenant config keys (companyName, timezone, locale, SMTP credentials) and system-only keys (priceModel, maxUsers) via `r.config({ keys: { ... } })`. Use this feature in every multi-tenant app; membership resolution and invitation flows depend on it, and `auth-email-password` requires it.",
|
|
76
77
|
);
|
|
@@ -50,6 +50,8 @@ export const memberDirectoryQuery = definePagedQueryHandler({
|
|
|
50
50
|
limit: z.number().int().min(1).max(MAX_LIST_LIMIT).default(MAX_LIST_LIMIT),
|
|
51
51
|
// Sent by the reference combobox while the user types.
|
|
52
52
|
search: z.string().trim().min(1).optional(),
|
|
53
|
+
// Resolves exactly these users; callers that know their ids must not depend on the cap.
|
|
54
|
+
ids: z.array(z.string().min(1)).max(MAX_LIST_LIMIT).optional(),
|
|
53
55
|
}),
|
|
54
56
|
access: { roles: access.admin },
|
|
55
57
|
description:
|
|
@@ -62,7 +64,8 @@ export const memberDirectoryQuery = definePagedQueryHandler({
|
|
|
62
64
|
"tenant:query:member-directory requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
|
-
const { limit, search } = query.payload;
|
|
67
|
+
const { limit, search, ids } = query.payload;
|
|
68
|
+
if (ids?.length === 0) return { rows: [], nextCursor: null };
|
|
66
69
|
// Without a search the first `limit` rows are enough; with one, every
|
|
67
70
|
// candidate must be decrypted before the limit can apply.
|
|
68
71
|
const scanLimit = search === undefined ? limit : SEARCH_SCAN_CAP;
|
|
@@ -71,13 +74,16 @@ export const memberDirectoryQuery = definePagedQueryHandler({
|
|
|
71
74
|
const db = ctx.systemDb.acknowledgeCrossTenant(
|
|
72
75
|
"SystemAdmin reference labels span every tenant, as user:query:user:list did",
|
|
73
76
|
);
|
|
74
|
-
users = await selectMany(db, userTable, undefined, {
|
|
77
|
+
users = await selectMany(db, userTable, ids === undefined ? undefined : { id: ids }, {
|
|
75
78
|
limit: scanLimit,
|
|
76
79
|
orderBy: { col: "id", direction: "asc" },
|
|
77
80
|
});
|
|
78
81
|
} else {
|
|
79
82
|
const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
|
|
80
|
-
const
|
|
83
|
+
const requestedIds = ids === undefined ? undefined : new Set(ids);
|
|
84
|
+
const userIds = (await loadMemberUserIds(db, query.user.tenantId))
|
|
85
|
+
.filter((id) => requestedIds === undefined || requestedIds.has(id))
|
|
86
|
+
.slice(0, scanLimit);
|
|
81
87
|
users =
|
|
82
88
|
userIds.length > 0
|
|
83
89
|
? await selectMany(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { isUndefinedTable, selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import {
|
|
3
3
|
access,
|
|
4
4
|
definePagedQueryHandler,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
9
9
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
10
|
-
import {
|
|
10
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
11
11
|
import * as z from "zod";
|
|
12
12
|
import { userSessionTable } from "../../sessions/index.ts";
|
|
13
13
|
import { decryptStoredPii, mapWithConcurrency } from "../../shared/index.ts";
|
|
@@ -142,7 +142,7 @@ export const teamListQuery = definePagedQueryHandler({
|
|
|
142
142
|
// ponytail: lean sample apps (admin-console) mount auth without
|
|
143
143
|
// sessions — store_user_sessions never gets pushed. Integration tests
|
|
144
144
|
// may seed the table without mounting the feature; prod mounts both.
|
|
145
|
-
if (
|
|
145
|
+
if (!isUndefinedTable(err)) throw err;
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -27,10 +27,12 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
|
|
|
27
27
|
limit: z.number().int().min(1).max(MAX_LIST_LIMIT).default(MAX_LIST_LIMIT),
|
|
28
28
|
// Sent by the reference combobox while the user types.
|
|
29
29
|
search: z.string().trim().min(1).optional(),
|
|
30
|
+
// Resolves exactly these tenants; callers that know their ids must not depend on the cap.
|
|
31
|
+
ids: z.array(z.string().min(1)).max(MAX_LIST_LIMIT).optional(),
|
|
30
32
|
}),
|
|
31
33
|
access: { roles: access.admin },
|
|
32
34
|
description:
|
|
33
|
-
"Lists tenants as id/label pairs for resolving tenant references to display names: the caller's own tenant for an admin, every tenant for a SystemAdmin.",
|
|
35
|
+
"Lists tenants as id/label pairs for resolving tenant references to display names: the caller's own tenant for an admin, every tenant for a SystemAdmin. A search returns the first `limit` matches in tenant-key order (sorted by label afterwards), not the alphabetically best matches.",
|
|
34
36
|
agent: { expose: false },
|
|
35
37
|
handler: async (query, ctx) => {
|
|
36
38
|
if (!ctx.systemDb) {
|
|
@@ -39,14 +41,15 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
|
|
|
39
41
|
"tenant:query:tenant-directory requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
|
-
const { limit, search } = query.payload;
|
|
44
|
+
const { limit, search, ids } = query.payload;
|
|
45
|
+
if (ids?.length === 0) return { rows: [], nextCursor: null };
|
|
43
46
|
const scanLimit = search === undefined ? limit : SEARCH_SCAN_CAP;
|
|
44
47
|
let tenants: readonly { id: unknown; name?: unknown }[];
|
|
45
48
|
if (isSystemAdmin(query.user)) {
|
|
46
49
|
const db = ctx.systemDb.acknowledgeCrossTenant(
|
|
47
50
|
"SystemAdmin reference labels span every tenant, as tenant:query:tenant:list did",
|
|
48
51
|
);
|
|
49
|
-
tenants = await selectMany(db, tenantTable, undefined, {
|
|
52
|
+
tenants = await selectMany(db, tenantTable, ids === undefined ? undefined : { id: ids }, {
|
|
50
53
|
limit: scanLimit,
|
|
51
54
|
orderBy: [
|
|
52
55
|
{ col: "key", direction: "asc" },
|
|
@@ -56,7 +59,7 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
|
|
|
56
59
|
} else {
|
|
57
60
|
const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
|
|
58
61
|
const row = await fetchOne(db, tenantTable, { id: query.user.tenantId });
|
|
59
|
-
tenants = row ? [row] : [];
|
|
62
|
+
tenants = row && (ids === undefined || ids.includes(query.user.tenantId)) ? [row] : [];
|
|
60
63
|
}
|
|
61
64
|
// name is ciphertext at rest, so the SQL page is keyed by the plaintext
|
|
62
65
|
// `key`; label order and search happen in memory after decrypting.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Tenant destroy keeps user keys of members in other live tenants",
|
|
6
|
+
"detail": "The subject-keys stage erases user keys only for members without a membership in another tenant whose status is not destroyed. The tenant and tenant-record keys are erased as before."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Destroy sweep keeps a minimum time slice per tenant",
|
|
12
|
+
"detail": "With many tenants in destroying state the shared tick budget no longer shrinks a tenant's slice below two seconds, so chunking hooks make progress instead of failing the stage as no-progress attempts."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.365.0",
|
|
4
16
|
"type": "fix",
|
|
@@ -62,3 +62,8 @@ export type TenantDestructionStageName = (typeof TENANT_DESTRUCTION_STAGES)[numb
|
|
|
62
62
|
// The sweep cron fires every minute with concurrency "skip", so a tick must
|
|
63
63
|
// finish well inside that window or the next one is skipped.
|
|
64
64
|
export const DESTRUCTION_STAGE_TICK_BUDGET_MS = 30_000;
|
|
65
|
+
|
|
66
|
+
// Lower bound of a tenant's share of the tick budget. Below it a chunking hook
|
|
67
|
+
// without a guaranteed first batch processes nothing, and a no-progress stage
|
|
68
|
+
// counts as a failed attempt, which would abandon the pipeline under load.
|
|
69
|
+
export const DESTRUCTION_STAGE_MIN_SLICE_MS = 2_000;
|
|
@@ -23,6 +23,7 @@ import type { SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
|
|
|
23
23
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
24
24
|
import { tenantEntity, tenantTable } from "../tenant/index.ts";
|
|
25
25
|
import {
|
|
26
|
+
DESTRUCTION_STAGE_MIN_SLICE_MS,
|
|
26
27
|
DESTRUCTION_STAGE_TICK_BUDGET_MS,
|
|
27
28
|
TENANT_AGGREGATE_TYPE,
|
|
28
29
|
TENANT_DESTRUCTION_COMPLETED_EVENT_QN,
|
|
@@ -461,7 +462,10 @@ export async function runTenantDestructionSweep(args: {
|
|
|
461
462
|
// window and starve the ones behind it. Each slice starts when its tenant
|
|
462
463
|
// starts: anchoring to a shared tick deadline would hand tenants after an
|
|
463
464
|
// overrun a zero slice, and a no-progress stage counts as a failed attempt.
|
|
464
|
-
const sliceMs =
|
|
465
|
+
const sliceMs = Math.max(
|
|
466
|
+
DESTRUCTION_STAGE_MIN_SLICE_MS,
|
|
467
|
+
DESTRUCTION_STAGE_TICK_BUDGET_MS / Math.max(1, destroying.length),
|
|
468
|
+
);
|
|
465
469
|
for (const row of destroying) {
|
|
466
470
|
const tenantDeadlineAt = Date.now() + sliceMs;
|
|
467
471
|
try {
|