@cosmicdrift/kumiko-samples 0.362.0 → 0.364.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 +23 -6
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
- package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
- package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
- package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
- package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
- package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +39 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
- package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/changes.json +6 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
- package/packages/bundled-features/src/channel-email/changes.json +19 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
- package/packages/bundled-features/src/channel-email/feature.ts +6 -3
- package/packages/bundled-features/src/channel-email/index.ts +7 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
- package/packages/bundled-features/src/channel-email/types.ts +23 -0
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
- package/packages/bundled-features/src/data-retention/changes.json +6 -0
- package/packages/bundled-features/src/data-retention/index.ts +1 -0
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
- package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
- package/packages/bundled-features/src/delivery/changes.json +66 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
- package/packages/bundled-features/src/delivery/constants.ts +2 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +19 -2
- package/packages/bundled-features/src/delivery/jobs.ts +97 -17
- package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
- package/packages/bundled-features/src/delivery/testing.ts +9 -1
- package/packages/bundled-features/src/delivery/types.ts +34 -18
- package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
- package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
- package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/changes.json +19 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/ledger/recurring.ts +1 -3
- package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
- package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
- package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
- package/packages/bundled-features/src/page-render/branding.ts +3 -0
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
- package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
- package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
- package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
- package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
- package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
- package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/sessions/feature.ts +2 -4
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +6 -0
- package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
- package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
- package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
- package/packages/bundled-features/src/template-resolver/README.md +66 -8
- package/packages/bundled-features/src/template-resolver/api.ts +85 -9
- package/packages/bundled-features/src/template-resolver/changes.json +186 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +138 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +111 -0
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
- package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
- package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
- package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
- package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +27 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
- package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
- package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
- package/packages/bundled-features/src/tenant/seeding.ts +41 -7
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
- package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
- package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
- package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +7 -0
- package/packages/bundled-features/src/waitlist/feature.ts +17 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
- package/packages/bundled-features/src/waitlist/mail.ts +69 -18
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +3 -3
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +11 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
- package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
- package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
- package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
- package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +2 -0
- package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
- package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
- package/samples/recipes/delivery-notifications/README.md +26 -25
- package/samples/recipes/delivery-notifications/package.json +2 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
- package/samples/recipes/seed-migration/README.md +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
- package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
- package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
- package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
|
@@ -16,6 +16,12 @@ import type {
|
|
|
16
16
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
17
17
|
import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
18
18
|
import * as z from "zod";
|
|
19
|
+
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
20
|
+
import {
|
|
21
|
+
emailAttachmentRefSchema,
|
|
22
|
+
MAX_EMAIL_ATTACHMENTS,
|
|
23
|
+
toAttachmentRef,
|
|
24
|
+
} from "./attachment-ref.js";
|
|
19
25
|
import { appendAttemptEvent } from "./attempt-log.js";
|
|
20
26
|
import { buildChannelContext } from "./channel-context.js";
|
|
21
27
|
import { DeliveryJobs, deliveryIdempotencyRedisKey, deliveryPriorityRank } from "./constants.js";
|
|
@@ -49,7 +55,14 @@ const renderJobPayloadSchema = z.object({
|
|
|
49
55
|
|
|
50
56
|
const sendJobPayloadSchema = renderJobPayloadSchema.extend({
|
|
51
57
|
rendered: z
|
|
52
|
-
.object({
|
|
58
|
+
.object({
|
|
59
|
+
html: z.string(),
|
|
60
|
+
subject: z.string(),
|
|
61
|
+
text: z.string().optional(),
|
|
62
|
+
fromName: z.string().optional(),
|
|
63
|
+
replyTo: z.string().optional(),
|
|
64
|
+
attachments: z.array(emailAttachmentRefSchema).max(MAX_EMAIL_ATTACHMENTS).optional(),
|
|
65
|
+
})
|
|
53
66
|
.optional(),
|
|
54
67
|
});
|
|
55
68
|
|
|
@@ -63,18 +76,36 @@ function requireTenantScopedDeps(
|
|
|
63
76
|
if (!registry) throw new Error("delivery job: missing registry in job context");
|
|
64
77
|
// delivery is r.systemScope()'d, so JobContext.systemDb is guaranteed; unsafeRaw
|
|
65
78
|
// hands the underlying DbConnection to both — safe since they filter by tenantId themselves.
|
|
79
|
+
// Attachment lookups read only this tenant's FileRefs, plus the system tenant's for inline images.
|
|
66
80
|
if (!ctx.systemDb) {
|
|
67
81
|
throw new InternalError({
|
|
68
82
|
message: "delivery job: ctx.systemDb missing on a system-scoped job",
|
|
69
83
|
});
|
|
70
84
|
}
|
|
85
|
+
// Also makes ctx.config (built for the job's tenant) the accessor of the payload tenant;
|
|
86
|
+
// job contexts carry no configFor.
|
|
71
87
|
ctx.systemDb.assertTenantMatch(tenantId);
|
|
72
88
|
const db = ctx.systemDb.unsafeRaw(
|
|
73
|
-
"
|
|
89
|
+
"attempt append and channel context filter by this job's tenantId; attachment lookup reads only the job tenant's FileRefs plus, for inline images, the system tenant's",
|
|
74
90
|
) as DbConnection; // @cast-boundary db-operator — DbRunner narrows to DbConnection, jobs never run inside a DbTx
|
|
75
91
|
return { db, registry };
|
|
76
92
|
}
|
|
77
93
|
|
|
94
|
+
function toSendRendered(
|
|
95
|
+
rendered: RenderedMessage,
|
|
96
|
+
): z.infer<typeof sendJobPayloadSchema>["rendered"] {
|
|
97
|
+
return {
|
|
98
|
+
html: rendered.html,
|
|
99
|
+
subject: rendered.subject,
|
|
100
|
+
...(rendered.text !== undefined && { text: rendered.text }),
|
|
101
|
+
...(rendered.fromName !== undefined && { fromName: rendered.fromName }),
|
|
102
|
+
...(rendered.replyTo !== undefined && { replyTo: rendered.replyTo }),
|
|
103
|
+
...(rendered.attachments !== undefined && {
|
|
104
|
+
attachments: rendered.attachments.map(toAttachmentRef),
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
function resolveChannel(registry: Registry, name: string): DeliveryChannel {
|
|
79
110
|
const channel = collectChannels(registry).find((c) => c.name === name);
|
|
80
111
|
if (!channel) throw new Error(`delivery job: unknown channel "${name}"`);
|
|
@@ -111,15 +142,11 @@ function entryFor(
|
|
|
111
142
|
};
|
|
112
143
|
}
|
|
113
144
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
p: RenderJobPayload,
|
|
120
|
-
): Promise<void> {
|
|
121
|
-
// skip: nothing to release before the last attempt or without an idempotency key
|
|
122
|
-
if (!ctx.finalAttempt || !p.idempotencyKey) return;
|
|
145
|
+
// Assumes ctx.redis is the handle the service claimed on; runProdApp wires the same
|
|
146
|
+
// instance into both.
|
|
147
|
+
async function releaseIdempotencyKey(ctx: JobContext, p: RenderJobPayload): Promise<void> {
|
|
148
|
+
// skip: no idempotency key was claimed for this notification
|
|
149
|
+
if (!p.idempotencyKey) return;
|
|
123
150
|
if (!ctx.redis) {
|
|
124
151
|
ctx.log.warn("delivery: idempotency key not released, job context has no redis", {
|
|
125
152
|
notificationType: p.notificationType,
|
|
@@ -135,6 +162,33 @@ async function releaseIdempotencyOnTerminalFailure(
|
|
|
135
162
|
}
|
|
136
163
|
}
|
|
137
164
|
|
|
165
|
+
// Only the final retry frees the key: earlier failures are retried by the runner and
|
|
166
|
+
// must still dedup.
|
|
167
|
+
async function releaseIdempotencyOnTerminalFailure(
|
|
168
|
+
ctx: JobContext,
|
|
169
|
+
p: RenderJobPayload,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
// skip: not the last attempt, the runner retries and the key must keep deduplicating
|
|
172
|
+
if (!ctx.finalAttempt) return;
|
|
173
|
+
await releaseIdempotencyKey(ctx, p);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Covers every throw after the payload parsed, not just the attempt-logged ones: a
|
|
177
|
+
// missing registry or unknown channel on the last attempt would otherwise hold the key
|
|
178
|
+
// until its TTL.
|
|
179
|
+
async function releasingKeyOnTerminalFailure<T>(
|
|
180
|
+
ctx: JobContext,
|
|
181
|
+
p: RenderJobPayload,
|
|
182
|
+
run: () => Promise<T>,
|
|
183
|
+
): Promise<T> {
|
|
184
|
+
try {
|
|
185
|
+
return await run();
|
|
186
|
+
} catch (err) {
|
|
187
|
+
await releaseIdempotencyOnTerminalFailure(ctx, p);
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
138
192
|
// The job runner stores err.message in the run row, and BullMQ keeps failedReason and
|
|
139
193
|
// the stack trace in Redis in plain text, so the rethrown error carries only the code.
|
|
140
194
|
// The redacted message goes to the log.
|
|
@@ -157,7 +211,6 @@ async function failAttempt(
|
|
|
157
211
|
p.deliveryAttemptId,
|
|
158
212
|
entryFor(p, "failed", code, p.address),
|
|
159
213
|
);
|
|
160
|
-
await releaseIdempotencyOnTerminalFailure(ctx, p);
|
|
161
214
|
throw new Error(code);
|
|
162
215
|
}
|
|
163
216
|
|
|
@@ -166,10 +219,22 @@ async function failAttempt(
|
|
|
166
219
|
// the send step is never reached, so a stuck render can't half-send.
|
|
167
220
|
export const deliveryRenderJob: JobHandlerFn = async (payload, ctx) => {
|
|
168
221
|
const p = renderJobPayloadSchema.parse(payload);
|
|
222
|
+
return releasingKeyOnTerminalFailure(ctx, p, () => renderStep(p, ctx));
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
async function renderStep(p: RenderJobPayload, ctx: JobContext): Promise<void> {
|
|
169
226
|
const tenantId = p.tenantId as TenantId; // @cast-boundary engine-payload — stream tenant
|
|
170
227
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
171
228
|
const channel = resolveChannel(registry, p.channelName);
|
|
172
|
-
const channelCtx = buildChannelContext(
|
|
229
|
+
const channelCtx = buildChannelContext(
|
|
230
|
+
db,
|
|
231
|
+
registry,
|
|
232
|
+
undefined,
|
|
233
|
+
tenantId,
|
|
234
|
+
ctx.secrets,
|
|
235
|
+
ctx.config,
|
|
236
|
+
(fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId),
|
|
237
|
+
);
|
|
173
238
|
|
|
174
239
|
let rendered: RenderedMessage;
|
|
175
240
|
try {
|
|
@@ -184,23 +249,35 @@ export const deliveryRenderJob: JobHandlerFn = async (payload, ctx) => {
|
|
|
184
249
|
const jobRunner = ctx["jobRunner"] as JobRunner; // @cast-boundary dynamic-key — dispatch lives on the concrete runner
|
|
185
250
|
await jobRunner.dispatch(
|
|
186
251
|
DeliveryJobs.send,
|
|
187
|
-
{ ...p, rendered },
|
|
252
|
+
{ ...p, rendered: toSendRendered(rendered) },
|
|
188
253
|
{ priority: deliveryPriorityRank[p.priority] },
|
|
189
254
|
);
|
|
190
255
|
} catch (err) {
|
|
191
256
|
return failAttempt(ctx, db, registry, p, "channel_error", err);
|
|
192
257
|
}
|
|
193
|
-
}
|
|
258
|
+
}
|
|
194
259
|
|
|
195
260
|
// Deliver via the channel using the rendered payload (if any). On failure:
|
|
196
261
|
// record the terminal failed event and rethrow so BullMQ retries the send with
|
|
197
262
|
// the same already-rendered HTML — no re-render needed.
|
|
198
263
|
export const deliverySendJob: JobHandlerFn = async (payload, ctx) => {
|
|
199
264
|
const p = sendJobPayloadSchema.parse(payload);
|
|
265
|
+
return releasingKeyOnTerminalFailure(ctx, p, () => sendStep(p, ctx));
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
async function sendStep(p: z.infer<typeof sendJobPayloadSchema>, ctx: JobContext): Promise<void> {
|
|
200
269
|
const tenantId = p.tenantId as TenantId; // @cast-boundary engine-payload — stream tenant
|
|
201
270
|
const { db, registry } = requireTenantScopedDeps(ctx, tenantId);
|
|
202
271
|
const channel = resolveChannel(registry, p.channelName);
|
|
203
|
-
const channelCtx = buildChannelContext(
|
|
272
|
+
const channelCtx = buildChannelContext(
|
|
273
|
+
db,
|
|
274
|
+
registry,
|
|
275
|
+
undefined,
|
|
276
|
+
tenantId,
|
|
277
|
+
ctx.secrets,
|
|
278
|
+
ctx.config,
|
|
279
|
+
(fileTenantId) => fileContextForTenant(ctx, ctx.systemUser.tenantId, fileTenantId),
|
|
280
|
+
);
|
|
204
281
|
|
|
205
282
|
try {
|
|
206
283
|
const result = await channel.send(p.address, toMessage(p), channelCtx, p.rendered);
|
|
@@ -216,7 +293,10 @@ export const deliverySendJob: JobHandlerFn = async (payload, ctx) => {
|
|
|
216
293
|
result.confirmed,
|
|
217
294
|
),
|
|
218
295
|
);
|
|
296
|
+
// A soft failure ends the job successfully, so no retry follows and the caller's
|
|
297
|
+
// retry must not be skipped as a duplicate.
|
|
298
|
+
if (result.status === "failed") await releaseIdempotencyKey(ctx, p);
|
|
219
299
|
} catch (err) {
|
|
220
300
|
return failAttempt(ctx, db, registry, p, "send_failed", err);
|
|
221
301
|
}
|
|
222
|
-
}
|
|
302
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NotifyFactory } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import type { NotifyFactory, NotifyFn, NotifyOptions } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import type { DeliveryService } from "./types.js";
|
|
3
3
|
|
|
4
4
|
// The same binding runProdApp wires into the app context: the calling context's
|
|
@@ -8,12 +8,17 @@ export function createDeliveryNotifyFactory(
|
|
|
8
8
|
deliveryService: Pick<DeliveryService, "notify">,
|
|
9
9
|
options: { readonly deliverQueuedInline?: boolean } = {},
|
|
10
10
|
): NotifyFactory {
|
|
11
|
-
return (user, tenantId, jobDispatcher
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
return (user, tenantId, jobDispatcher, deliveryDb, fileProviderResolver) => {
|
|
12
|
+
const notify: NotifyFn = (notificationType: string, notifyOptions: NotifyOptions) =>
|
|
13
|
+
deliveryService.notify(
|
|
14
|
+
notificationType,
|
|
15
|
+
notifyOptions,
|
|
16
|
+
user,
|
|
17
|
+
tenantId,
|
|
18
|
+
options.deliverQueuedInline === true ? undefined : jobDispatcher,
|
|
19
|
+
deliveryDb,
|
|
20
|
+
fileProviderResolver,
|
|
21
|
+
);
|
|
22
|
+
return notify;
|
|
23
|
+
};
|
|
19
24
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ConfigAccessor,
|
|
5
|
+
EscapeHatchAuditSink,
|
|
6
|
+
Registry,
|
|
7
|
+
TenantId,
|
|
8
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
4
10
|
import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
5
11
|
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
6
12
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
@@ -19,6 +25,8 @@ export type CreateDeliveryTestContextOptions = {
|
|
|
19
25
|
readonly jobRunner?: JobRunner;
|
|
20
26
|
readonly secrets?: SecretsContext;
|
|
21
27
|
readonly idempotencyRedis?: Redis;
|
|
28
|
+
readonly filesForTenant?: (tenantId: string) => FileContext | undefined;
|
|
29
|
+
readonly tenantConfigFor?: (tenantId: TenantId) => ConfigAccessor;
|
|
22
30
|
};
|
|
23
31
|
|
|
24
32
|
/**
|
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
-
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type {
|
|
4
4
|
DeliveryErrorCode,
|
|
5
5
|
NotifyDeliveryStatus,
|
|
6
|
+
NotifyDispatched,
|
|
6
7
|
NotifyJobDispatcher,
|
|
7
8
|
NotifyOptions,
|
|
8
9
|
NotifyPriority,
|
|
9
|
-
NotifyResult,
|
|
10
10
|
Registry,
|
|
11
11
|
SessionUser,
|
|
12
12
|
TenantId,
|
|
13
13
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
14
|
+
import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
|
|
14
15
|
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
16
|
+
import type { MailPayload, RenderedTemplateRef } from "../renderer-foundation/index.js";
|
|
17
|
+
import type { EmailAttachmentRef } from "./attachment-ref.js";
|
|
15
18
|
import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
|
|
16
19
|
|
|
17
20
|
// --- Channel Interface ---
|
|
18
21
|
|
|
22
|
+
export type RenderedMail = {
|
|
23
|
+
readonly subject: string;
|
|
24
|
+
readonly html: string;
|
|
25
|
+
readonly text: string;
|
|
26
|
+
readonly previewText?: string;
|
|
27
|
+
readonly fromName?: string;
|
|
28
|
+
readonly replyTo?: string;
|
|
29
|
+
readonly attachments?: readonly EmailAttachmentRef[];
|
|
30
|
+
/** The layout version the html was rendered through; the editor preview names it. */
|
|
31
|
+
readonly layout?: RenderedTemplateRef;
|
|
32
|
+
};
|
|
33
|
+
|
|
19
34
|
export type ChannelContext = {
|
|
20
35
|
readonly db: TenantDb;
|
|
21
36
|
readonly registry: Registry;
|
|
@@ -24,8 +39,19 @@ export type ChannelContext = {
|
|
|
24
39
|
// Chat channels read their per-tenant webhook URL / bot token from it.
|
|
25
40
|
// Absent when no secrets feature/KEK is wired.
|
|
26
41
|
readonly secrets?: SecretsContext | undefined;
|
|
42
|
+
// Renders a stored mail template, the tenant's own or the system fallback.
|
|
43
|
+
readonly renderMail?: (payload: MailPayload) => Promise<RenderedMail>;
|
|
44
|
+
// Reads the bytes behind an attachment ref at send time. Absent when no file storage is wired.
|
|
45
|
+
readonly readAttachment?: (
|
|
46
|
+
ref: EmailAttachmentRef,
|
|
47
|
+
origin?: AttachmentOrigin,
|
|
48
|
+
) => Promise<{ readonly content: Uint8Array; readonly contentType: string } | null>;
|
|
27
49
|
};
|
|
28
50
|
|
|
51
|
+
// "template": the renderer returned the ref from the stored template (own or system). "message":
|
|
52
|
+
// the ref came from notify data, so a caller chose it. Unset means "message".
|
|
53
|
+
export type AttachmentOrigin = "template" | "message";
|
|
54
|
+
|
|
29
55
|
export type ChannelMessage = {
|
|
30
56
|
readonly notificationType: string;
|
|
31
57
|
readonly title: string;
|
|
@@ -49,6 +75,9 @@ export type RenderedMessage = {
|
|
|
49
75
|
readonly html: string;
|
|
50
76
|
readonly subject: string;
|
|
51
77
|
readonly text?: string;
|
|
78
|
+
readonly fromName?: string;
|
|
79
|
+
readonly replyTo?: string;
|
|
80
|
+
readonly attachments?: readonly EmailAttachmentRef[];
|
|
52
81
|
};
|
|
53
82
|
|
|
54
83
|
// `mode` decides how the delivery-service dispatches a channel:
|
|
@@ -76,21 +105,6 @@ export type DeliveryChannel = {
|
|
|
76
105
|
): Promise<ChannelResult>;
|
|
77
106
|
};
|
|
78
107
|
|
|
79
|
-
// --- Notification Renderer ---
|
|
80
|
-
|
|
81
|
-
export type RendererInput = {
|
|
82
|
-
readonly template: string;
|
|
83
|
-
readonly variables: Readonly<Record<string, unknown>>;
|
|
84
|
-
readonly locale?: string | undefined;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export type NotificationRenderer = {
|
|
88
|
-
readonly name: string;
|
|
89
|
-
render(input: RendererInput): Promise<string>;
|
|
90
|
-
/** Plain-text version of the same mail; the email channel sends it as the text part of a multipart/alternative mail. */
|
|
91
|
-
renderText?(input: RendererInput): Promise<string>;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
108
|
// --- Delivery Log Entry ---
|
|
95
109
|
|
|
96
110
|
export type DeliveryLogEntry = {
|
|
@@ -114,7 +128,9 @@ export type DeliveryService = {
|
|
|
114
128
|
user: SessionUser,
|
|
115
129
|
tenantId: TenantId,
|
|
116
130
|
jobDispatcher?: NotifyJobDispatcher,
|
|
117
|
-
|
|
131
|
+
deliveryDb?: DbConnection,
|
|
132
|
+
fileProviderResolver?: FileProviderResolver,
|
|
133
|
+
): Promise<NotifyDispatched>;
|
|
118
134
|
};
|
|
119
135
|
|
|
120
136
|
// r.useExtension options-shape: `name` is NOT part of the registration
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/api";
|
|
8
8
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
9
|
import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
|
|
10
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
10
11
|
import * as jose from "jose";
|
|
11
12
|
import * as z from "zod";
|
|
12
13
|
import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
@@ -412,8 +413,10 @@ function tokenFromRequestBody(request: SignatureExtraRouteVerifyRequest): string
|
|
|
412
413
|
}
|
|
413
414
|
if (contentType?.includes("application/json")) {
|
|
414
415
|
try {
|
|
415
|
-
const parsed = JSON.parse(request.rawBody)
|
|
416
|
-
return typeof parsed
|
|
416
|
+
const parsed: unknown = JSON.parse(request.rawBody);
|
|
417
|
+
return isPlainObject(parsed) && typeof parsed["token"] === "string"
|
|
418
|
+
? parsed["token"]
|
|
419
|
+
: undefined;
|
|
417
420
|
} catch {
|
|
418
421
|
return undefined;
|
|
419
422
|
}
|
|
@@ -13,13 +13,10 @@ import type {
|
|
|
13
13
|
import QRCode from "qrcode";
|
|
14
14
|
import type { OverlayOptions, ResizeOptions, Sharp } from "sharp";
|
|
15
15
|
import sharp from "sharp";
|
|
16
|
+
import { assertNotSvg, SHARP_INPUT_OPTIONS } from "../shared/index.js";
|
|
16
17
|
|
|
17
18
|
const MIN_SHARP_SIGMA = 0.3;
|
|
18
19
|
const MAX_SHARP_SIGMA = 1000;
|
|
19
|
-
// Above this, a decoded pixel buffer runs into hundreds of MB before any
|
|
20
|
-
// resize even happens — sharp's own default (~268MP) is still large enough
|
|
21
|
-
// to be an OOM primitive for a single request.
|
|
22
|
-
const MAX_INPUT_PIXELS = 100_000_000;
|
|
23
20
|
// Longest output edge a caller may request via `size`/`maxEdge`. Without a
|
|
24
21
|
// cap, `{ maxEdge: 50000 }` allocates a multi-gigabyte buffer per encoder
|
|
25
22
|
// step before any client sees a byte back.
|
|
@@ -38,12 +35,6 @@ const MAX_QR_DATA_LENGTH = 1024;
|
|
|
38
35
|
// camera capture — the AC claims phone-scannable, so an under-sized layer
|
|
39
36
|
// throws instead of caching a silently unusable image.
|
|
40
37
|
const MIN_QR_PIXEL_WIDTH = 160;
|
|
41
|
-
// sequentialRead trades random pixel access for lower peak memory on
|
|
42
|
-
// streamed formats — applied at every decode entry point below since none
|
|
43
|
-
// of them need non-sequential access to the *original* bytes (extract runs
|
|
44
|
-
// against a single already-buffered region per call).
|
|
45
|
-
const SHARP_INPUT_OPTIONS = { limitInputPixels: MAX_INPUT_PIXELS, sequentialRead: true } as const;
|
|
46
|
-
|
|
47
38
|
function normalizeMimeType(mimeType: string): string {
|
|
48
39
|
return mimeType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
49
40
|
}
|
|
@@ -52,18 +43,6 @@ function clampSigma(sigma: number): number {
|
|
|
52
43
|
return Math.min(MAX_SHARP_SIGMA, Math.max(MIN_SHARP_SIGMA, sigma));
|
|
53
44
|
}
|
|
54
45
|
|
|
55
|
-
// Shared trust boundary for every image byte source this renderer decodes —
|
|
56
|
-
// the original upload (renderImage) and an overlay layer's bytes (applyOverlays)
|
|
57
|
-
// alike: a base64 string in a field declaration isn't more trustworthy than
|
|
58
|
-
// an upload just because it came from app code.
|
|
59
|
-
function assertNotSvg(format: string | undefined): void {
|
|
60
|
-
if (format === "svg") {
|
|
61
|
-
throw new Error(
|
|
62
|
-
"derivatives-sharp: SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this renderer doesn't need to cross.",
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
46
|
function assertValidRegion(region: BlurRegion): void {
|
|
68
47
|
const values = [region.x, region.y, region.width, region.height];
|
|
69
48
|
if (values.some((v) => !Number.isFinite(v)) || region.width <= 0 || region.height <= 0) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// the codec here keeps liteparse + recipe consumers lockstep with the
|
|
5
5
|
// entity shape (kumiko-framework#1549/1).
|
|
6
6
|
|
|
7
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
8
|
import type { IngestPage } from "./entity.js";
|
|
8
9
|
|
|
9
10
|
export function writeIngestPages(pages: readonly IngestPage[]): string {
|
|
@@ -28,7 +29,9 @@ function parseJson(raw: string): unknown {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function isIngestPage(value: unknown): value is IngestPage {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
return (
|
|
33
|
+
isPlainObject(value) &&
|
|
34
|
+
typeof value["pageNumber"] === "number" &&
|
|
35
|
+
typeof value["text"] === "string"
|
|
36
|
+
);
|
|
34
37
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type RedisClientOptions,
|
|
6
6
|
redisChannelPrefixFromEnv,
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/redis";
|
|
8
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
8
9
|
import type { ToggleSyncSignal } from "./toggle-runtime.js";
|
|
9
10
|
|
|
10
11
|
// Single fixed channel — unlike the SSE broker there's no per-tenant/
|
|
@@ -20,10 +21,9 @@ export type RedisToggleSyncSignal = ToggleSyncSignal & {
|
|
|
20
21
|
|
|
21
22
|
function isTogglePayload(value: unknown): value is { featureName: string; enabled: boolean } {
|
|
22
23
|
return (
|
|
23
|
-
|
|
24
|
-
value
|
|
25
|
-
typeof
|
|
26
|
-
typeof (value as { enabled?: unknown }).enabled === "boolean"
|
|
24
|
+
isPlainObject(value) &&
|
|
25
|
+
typeof value["featureName"] === "string" &&
|
|
26
|
+
typeof value["enabled"] === "boolean"
|
|
27
27
|
);
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -147,7 +147,7 @@ export const publicVariantQuery = defineQueryHandler({
|
|
|
147
147
|
const spec = resolveFieldVariant(ctx.registry, entityType, fieldName, query.payload.variant);
|
|
148
148
|
if (!spec) return null;
|
|
149
149
|
const result = await derivatives.variant(query.payload.fileRefId, spec, query.payload.variant);
|
|
150
|
-
const bytes = await files.
|
|
150
|
+
const bytes = await files.refFromFileRef(result).read();
|
|
151
151
|
|
|
152
152
|
// ponytail: fully buffered (no streaming), +33% over the wire from
|
|
153
153
|
// base64 — HttpRouteHandlerDeps only exposes {app, systemQuery} (no
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "readFileRefContent reads handed-over files; fileContextForTenant takes the context tenant",
|
|
6
|
+
"detail": "readFileRefContent, readTemplateResource and the public variant query read bytes through FileContext.refFromFileRef, so a fileRef moved by a tenant handover is readable again. fileContextForTenant gains a `contextTenantId` argument: without a provider resolver it returns undefined for any other tenant instead of the context's files, which are bound to a different key space."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.292.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { isUuid, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import {
|
|
5
|
+
createFileContext,
|
|
6
|
+
type FileContext,
|
|
7
|
+
type FileProviderResolver,
|
|
8
|
+
fileRefsTable,
|
|
9
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
10
|
+
|
|
11
|
+
export type FileRefContent = {
|
|
12
|
+
readonly content: Uint8Array;
|
|
13
|
+
readonly contentType: string;
|
|
14
|
+
readonly tenantId: string;
|
|
15
|
+
readonly size: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type FileRefRow = {
|
|
19
|
+
readonly storageKey: string;
|
|
20
|
+
readonly mimeType: string;
|
|
21
|
+
readonly tenantId: string;
|
|
22
|
+
readonly size: number | bigint | string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Tenant-bound on purpose: without a resolver `ctx.files` only reaches the storage of the
|
|
26
|
+
// context's own tenant (`contextTenantId`), so a request for another tenant's files is
|
|
27
|
+
// undefined instead of silently reading through the wrong tenant's storage.
|
|
28
|
+
export function fileContextForTenant(
|
|
29
|
+
ctx: {
|
|
30
|
+
readonly _fileProviderResolver?: FileProviderResolver | undefined;
|
|
31
|
+
readonly files?: FileContext | undefined;
|
|
32
|
+
},
|
|
33
|
+
contextTenantId: string,
|
|
34
|
+
tenantId: string,
|
|
35
|
+
): FileContext | undefined {
|
|
36
|
+
const resolver = ctx._fileProviderResolver;
|
|
37
|
+
if (resolver === undefined) return tenantId === contextTenantId ? ctx.files : undefined;
|
|
38
|
+
return fileContextFromResolver(resolver, tenantId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function fileContextFromResolver(
|
|
42
|
+
resolver: FileProviderResolver,
|
|
43
|
+
tenantId: string,
|
|
44
|
+
): FileContext {
|
|
45
|
+
// @cast-boundary engine-payload — tenantId comes from a trusted job payload or session
|
|
46
|
+
const brandedTenantId = tenantId as TenantId;
|
|
47
|
+
return createFileContext(() => resolver(brandedTenantId), tenantId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class FileRefTooLargeError extends Error {
|
|
51
|
+
constructor(readonly size: number) {
|
|
52
|
+
super(`file ref exceeds the size limit (${size} bytes)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// fileName of the row is never used: it is personal data and may be a PII ciphertext.
|
|
57
|
+
export async function readFileRefContent(args: {
|
|
58
|
+
readonly db: DbRunner | TenantDb;
|
|
59
|
+
readonly filesForTenant: (tenantId: string) => FileContext | undefined;
|
|
60
|
+
readonly fileRefId: string;
|
|
61
|
+
readonly allowedTenantIds: readonly string[];
|
|
62
|
+
/** Only accept this user's own upload that is not bound to any entity yet. */
|
|
63
|
+
readonly unclaimedUploadOf?: string;
|
|
64
|
+
/** Rejects before any bytes are read when the stored size exceeds the limit. */
|
|
65
|
+
readonly maxBytes?: number;
|
|
66
|
+
}): Promise<FileRefContent | null> {
|
|
67
|
+
if (!isUuid(args.fileRefId)) return null;
|
|
68
|
+
const [row] = await selectMany<FileRefRow>(args.db, fileRefsTable, {
|
|
69
|
+
id: args.fileRefId,
|
|
70
|
+
tenantId: { in: args.allowedTenantIds },
|
|
71
|
+
isDeleted: false,
|
|
72
|
+
...(args.unclaimedUploadOf !== undefined && {
|
|
73
|
+
insertedById: args.unclaimedUploadOf,
|
|
74
|
+
entityType: null,
|
|
75
|
+
entityId: null,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (row === undefined) return null;
|
|
79
|
+
const size = Number(row.size);
|
|
80
|
+
if (args.maxBytes !== undefined && size > args.maxBytes) {
|
|
81
|
+
throw new FileRefTooLargeError(size);
|
|
82
|
+
}
|
|
83
|
+
const files = args.filesForTenant(row.tenantId);
|
|
84
|
+
if (files === undefined) {
|
|
85
|
+
throw new Error("readFileRefContent: no file storage is available for the file's tenant");
|
|
86
|
+
}
|
|
87
|
+
const content = await files.refFromFileRef(row).read();
|
|
88
|
+
return { content, contentType: row.mimeType, tenantId: row.tenantId, size };
|
|
89
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Tenant destroy hooks make progress even when the deadline already passed",
|
|
6
|
+
"detail": "The chunked migration takes a minBatches option and the file destroy hooks process one chunk per tick, so a slow list call no longer makes every run time out empty."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.354.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -129,6 +129,7 @@ export const fileRefTenantDestroyHook: TenantDataDestroyHook = async (ctx) => {
|
|
|
129
129
|
},
|
|
130
130
|
maxFailures: 1,
|
|
131
131
|
deadlineAt: ctx.deadlineAt ?? Number.POSITIVE_INFINITY,
|
|
132
|
+
minBatches: 1,
|
|
132
133
|
onRowError: (_row, err) => {
|
|
133
134
|
firstFailure = err instanceof Error ? err.message : String(err);
|
|
134
135
|
},
|
|
@@ -222,6 +223,9 @@ export const fileRefStorageDestroyHook: StorageProviderDestroyTenantHook = async
|
|
|
222
223
|
},
|
|
223
224
|
maxFailures: 1,
|
|
224
225
|
deadlineAt: ctx.deadlineAt ?? Number.POSITIVE_INFINITY,
|
|
226
|
+
// list() and the survivor lookup above can use up the whole budget; one chunk per tick
|
|
227
|
+
// still deletes, so the stage never reports "no progress".
|
|
228
|
+
minBatches: 1,
|
|
225
229
|
onRowError: (_key, err) => {
|
|
226
230
|
firstFailure = err instanceof Error ? err.message : String(err);
|
|
227
231
|
},
|
|
@@ -74,7 +74,10 @@ const loadCollapsed = (): ReadonlySet<string> => {
|
|
|
74
74
|
if (typeof window === "undefined") return new Set();
|
|
75
75
|
try {
|
|
76
76
|
const raw = window.localStorage.getItem(COLLAPSED_KEY);
|
|
77
|
-
|
|
77
|
+
const parsed: unknown = raw ? JSON.parse(raw) : [];
|
|
78
|
+
return new Set(
|
|
79
|
+
Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string") : [],
|
|
80
|
+
);
|
|
78
81
|
} catch {
|
|
79
82
|
return new Set();
|
|
80
83
|
}
|
|
@@ -273,7 +276,7 @@ export function FolderManager({
|
|
|
273
276
|
setDragOverKey(key);
|
|
274
277
|
},
|
|
275
278
|
onDragLeave: (e: DragEvent<HTMLDivElement>) => {
|
|
276
|
-
if (!e.currentTarget.contains(e.relatedTarget
|
|
279
|
+
if (!(e.relatedTarget instanceof Node && e.currentTarget.contains(e.relatedTarget)))
|
|
277
280
|
setDragOverKey((cur) => (cur === key ? null : cur));
|
|
278
281
|
},
|
|
279
282
|
onDrop: (e: DragEvent<HTMLDivElement>) => {
|