@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
|
@@ -53,6 +53,12 @@ export class FileRefTooLargeError extends Error {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export class FileStorageUnavailableError extends Error {
|
|
57
|
+
constructor() {
|
|
58
|
+
super("readFileRefContent: no file storage is available for the file's tenant");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
// fileName of the row is never used: it is personal data and may be a PII ciphertext.
|
|
57
63
|
export async function readFileRefContent(args: {
|
|
58
64
|
readonly db: DbRunner | TenantDb;
|
|
@@ -82,7 +88,7 @@ export async function readFileRefContent(args: {
|
|
|
82
88
|
}
|
|
83
89
|
const files = args.filesForTenant(row.tenantId);
|
|
84
90
|
if (files === undefined) {
|
|
85
|
-
throw new
|
|
91
|
+
throw new FileStorageUnavailableError();
|
|
86
92
|
}
|
|
87
93
|
const content = await files.refFromFileRef(row).read();
|
|
88
94
|
return { content, contentType: row.mimeType, tenantId: row.tenantId, size };
|
|
@@ -13,8 +13,6 @@ export type NamedS3EnvProviderOptions = {
|
|
|
13
13
|
readonly name: string;
|
|
14
14
|
/** Env prefix of the credential set, e.g. `RELEASES_S3_`. */
|
|
15
15
|
readonly envPrefix: string;
|
|
16
|
-
/** Tolerate missing credentials at boot; the provider then fails on its first file operation. */
|
|
17
|
-
readonly lazy?: boolean;
|
|
18
16
|
};
|
|
19
17
|
|
|
20
18
|
/** An additional S3 provider next to `"s3-env"`, reading its credentials from `<envPrefix>BUCKET` etc. */
|
|
@@ -15,7 +15,7 @@ bewirtschaftet. Das öffnet die Tür für Cross-Feature-Hooks:
|
|
|
15
15
|
## Was es NICHT macht
|
|
16
16
|
|
|
17
17
|
- **Keine Upload-/Download-Routes** — die bleiben in
|
|
18
|
-
`framework/src/api/server.ts`
|
|
18
|
+
`framework/src/api/server-routes.ts` (`mountFileRoutes`).
|
|
19
19
|
Multipart-Form-Body und Binary-Streaming passen nicht ins Write/Query-
|
|
20
20
|
Handler-Pattern; ein Refactor zu `r.httpRoute` wäre orthogonal zu
|
|
21
21
|
diesem Sprint.
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Encrypted drafts without a PII key service raise an error",
|
|
6
|
+
"detail": "Reading a ciphertext draft with no PII subject key service configured throws instead of returning an empty draft, so the stale-draft cleanup defers it and no longer releases its file refs as if it were empty."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.379.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import { unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { toPostgresArrayLiteral, unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
import { readStoredDraft } from "../../decrypt-draft.ts";
|
|
@@ -48,7 +48,7 @@ export async function selectStaleDraftsBatch(
|
|
|
48
48
|
AND NOT ("id"::text = ANY($3::text[]))
|
|
49
49
|
ORDER BY COALESCE("modified_at", "inserted_at") ASC
|
|
50
50
|
LIMIT $2`,
|
|
51
|
-
[olderThanDays, batchSize, [...excludeIds]],
|
|
51
|
+
[olderThanDays, batchSize, toPostgresArrayLiteral([...excludeIds])],
|
|
52
52
|
);
|
|
53
53
|
return Promise.all(
|
|
54
54
|
rows.map(async (row) => {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
decryptPiiFieldValues,
|
|
5
5
|
isPiiCiphertext,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
7
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
7
8
|
import { type FormDraftBlob, formDraftBlobSchema } from "./schemas.ts";
|
|
8
9
|
|
|
9
10
|
// Raw reads (fetchOne/selectMany/SQL) return the stored column, which is
|
|
@@ -13,7 +14,9 @@ import { type FormDraftBlob, formDraftBlobSchema } from "./schemas.ts";
|
|
|
13
14
|
export async function readStoredDraft(stored: unknown): Promise<FormDraftBlob | null> {
|
|
14
15
|
let plain = stored;
|
|
15
16
|
const kms = configuredPiiSubjectKms();
|
|
16
|
-
if (isPiiCiphertext(stored)
|
|
17
|
+
if (isPiiCiphertext(stored)) {
|
|
18
|
+
// A silent null would make the stale-draft cleanup treat the draft as empty and leak its file refs.
|
|
19
|
+
if (!kms) throw new InternalError({ message: "form-draft: pii kms not configured" });
|
|
17
20
|
const out = await decryptPiiFieldValues({ draft: stored }, ["draft"], kms, {
|
|
18
21
|
requestId: requestContext.get()?.requestId ?? "form-draft:read",
|
|
19
22
|
});
|
|
@@ -92,5 +92,5 @@ allowlists stay in the app: wrap the dispatch function and fall through to it.
|
|
|
92
92
|
|
|
93
93
|
- `tenantBy: (host, db) => TenantId | null` resolves every host that is neither the apex nor a fixed host (profile subdomains, custom domains). The tenant must be enabled and active; null means 404 for pages and `tenant_required` for the API. `createHostTenantIdResolver(options, db)` exposes the same mapping for managed-pages' `resolveApexTenant`.
|
|
94
94
|
- `tenantPathPrefix` is optional in host and resolver mode and serves `<prefix>/<key>` on the apex. The API half still takes the apex anchor there; an app that needs a path-named tenant on API calls resolves it itself.
|
|
95
|
-
- `redirects: [{ from, to, host?, status? }]` run before host classification. `to` is configured, never derived from the request.
|
|
95
|
+
- `redirects: [{ from, to, host?, status? }]` run before host classification inside hostDispatch, i.e. only for requests that reach the HTML fallback (no static file, no API route); a `from` that matches a static file or `/api/...` never fires. `to` is configured, never derived from the request.
|
|
96
96
|
- `apexAnonymousQueryAllowlist: { "example.com": ["feature:query:x"] }` limits the anonymous apex anchor to the listed query types on that host, for `/api/query` and `/api/stream`; other paths keep the anchor. `example.com` and `www.example.com` share one entry (a host without its own uses the other's). Combined with `tenantBy: "path"` it throws at boot, because the client names its tenant there (X-Tenant wins over the resolver) and would bypass the allowlist.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Redirects from /api paths fail at boot",
|
|
6
|
+
"detail": "Redirects run inside hostDispatch after static files and API routes, so a from of /api or /api/... was silently dead config. Boot validation now throws; the options and README already describe the HTML-fallback scope."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Redirects documented as hostDispatch-only",
|
|
12
|
+
"detail": "The redirects option and README state that redirects run inside hostDispatch, so only for requests that reach the HTML fallback; a from matching a static file or an API route never fires. The shared existence lookup of the host and resolver modes is one helper."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.376.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -30,33 +30,32 @@ export type HostRoutingDispatchFn = (
|
|
|
30
30
|
|
|
31
31
|
const tenantKeyExistsResultSchema = z.object({ exists: z.boolean() });
|
|
32
32
|
|
|
33
|
-
async function
|
|
33
|
+
async function existsViaSystemQuery(
|
|
34
|
+
systemQuery: PageHeadSystemQuery,
|
|
35
|
+
queryType: string,
|
|
36
|
+
payload: Record<string, string>,
|
|
37
|
+
): Promise<boolean> {
|
|
34
38
|
let result: unknown;
|
|
35
39
|
try {
|
|
36
|
-
result = await systemQuery(
|
|
40
|
+
result = await systemQuery(queryType, payload, SYSTEM_TENANT_ID);
|
|
37
41
|
} catch (error) {
|
|
38
42
|
if (error instanceof ValidationError) return false;
|
|
39
|
-
// Over the rate limit every key looks the same: serve the
|
|
40
|
-
//
|
|
43
|
+
// Over the rate limit every key looks the same: serve the shell, which
|
|
44
|
+
// leaks nothing. The API resolver is authoritative and answers
|
|
45
|
+
// tenant_required for unknown hosts, while a 404 would hit visitors
|
|
46
|
+
// behind a shared NAT under load.
|
|
41
47
|
if (error instanceof RateLimitError) return true;
|
|
42
48
|
throw error;
|
|
43
49
|
}
|
|
44
50
|
return tenantKeyExistsResultSchema.parse(result).exists;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// The shell is harmless: the API resolver is authoritative and answers
|
|
54
|
-
// tenant_required for unknown hosts, while a 404 would hit visitors behind
|
|
55
|
-
// a shared NAT under load.
|
|
56
|
-
if (error instanceof RateLimitError) return true;
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
return tenantKeyExistsResultSchema.parse(result).exists;
|
|
53
|
+
function isTenantKeyKnown(systemQuery: PageHeadSystemQuery, key: string): Promise<boolean> {
|
|
54
|
+
return existsViaSystemQuery(systemQuery, HostRoutingQueries.tenantKeyExists, { key });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isHostKnown(systemQuery: PageHeadSystemQuery, host: string): Promise<boolean> {
|
|
58
|
+
return existsViaSystemQuery(systemQuery, HostRoutingQueries.hostTenantExists, { host });
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
function serve(
|
|
@@ -81,6 +81,7 @@ export type HostRoutingOptions = HostRoutingTenantMode & {
|
|
|
81
81
|
// Not allowed with tenantBy "path" (boot error): there the client's X-Tenant
|
|
82
82
|
// wins over the resolver, so the allowlist would be ineffective.
|
|
83
83
|
readonly apexAnonymousQueryAllowlist?: Readonly<Record<string, readonly string[]>>;
|
|
84
|
-
// Checked first,
|
|
84
|
+
// Checked first within hostDispatch, i.e. only for requests that reach the HTML
|
|
85
|
+
// fallback (no static file, no API route): a `from` matching either never fires.
|
|
85
86
|
readonly redirects?: readonly HostRoutingRedirect[];
|
|
86
87
|
};
|
|
@@ -16,6 +16,11 @@ function validateRedirects(options: HostRoutingOptions): void {
|
|
|
16
16
|
if (!REDIRECT_PATH_PATTERN.test(rule.from)) {
|
|
17
17
|
throw new Error(`host-routing: redirect from must be a path like "/old", got "${rule.from}"`);
|
|
18
18
|
}
|
|
19
|
+
if (rule.from === "/api" || rule.from.startsWith("/api/")) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`host-routing: redirect from must not be an /api path, it would never fire, got "${rule.from}"`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
19
24
|
if (!REDIRECT_TARGET_PATTERN.test(rule.to)) {
|
|
20
25
|
throw new Error(
|
|
21
26
|
`host-routing: redirect to must be a same-origin path or an https URL without query, got "${rule.to}"`,
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Undecryptable mail account no longer stops polling of other accounts",
|
|
6
|
+
"detail": "Any decryption failure of an account address (corrupt envelope, missing key, AAD mismatch) is logged by error class and the account is skipped; the other accounts keep polling."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Rename audit reason comes from the caller role",
|
|
12
|
+
"detail": "update-account records tenant_admin or system_admin from the caller's roles and honors a client-supplied reason only for the system identity, so a form call can no longer write system reasons such as watch_supervisor. The rename form no longer carries a reason field and the reason payload field is optional."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.388.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Seen-message lookup handles listings of any size",
|
|
18
|
+
"detail": "filterUnknownMessageIds queries the already seen provider message ids in chunks of 1000, so a full-mailbox listing no longer exceeds the Postgres bind parameter limit and aborts the account's poll."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.378.0",
|
|
4
22
|
"type": "fix",
|
|
@@ -103,6 +103,7 @@ export function createInboundMailFoundationFeature() {
|
|
|
103
103
|
return defineFeature(
|
|
104
104
|
INBOUND_MAIL_FOUNDATION_FEATURE,
|
|
105
105
|
(r) => {
|
|
106
|
+
r.adoptsBundledLists();
|
|
106
107
|
r.describe(
|
|
107
108
|
"Plugin host for inbound e-mail (inbox federation) — provider plugins (inbound-provider-imap, later M365 Graph / Gmail REST) register at the `inboundMailProvider` extension point with verify/fetch and optional oauth/watch (live push). The foundation owns three event-sourced streams (mail-account lifecycle, inbound-message with exactly-once ingest via deterministic aggregate ids, mail-thread rollup) and their projections `read_mail_accounts`/`read_inbound_messages`/`read_mail_threads`; PII fields are envelope-encrypted per tenant subject key before every append (crypto-shredding on tenant destroy). Wire `createInboundMailConnectRoutes` (OAuth connect + anonymous callback outside /api) and `createInboundMailSupervisor` (IDLE watch with backoff + reconciliation poll) in bin/server.ts. Consume `inbound-mail-foundation:event:inbound-message-received` from app features to attach business processes.",
|
|
108
109
|
);
|
package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
// the account after fixing its credentials. Full-snapshot event: loads the
|
|
4
4
|
// latest state, merges the delta fields, and appends the new snapshot.
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
7
|
+
import {
|
|
8
|
+
type SessionUser,
|
|
9
|
+
SYSTEM_USER_ID,
|
|
10
|
+
type WriteHandlerDef,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
12
|
import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
13
|
import * as z from "zod";
|
|
9
14
|
import { InboundMailAccountStatuses } from "../constants.ts";
|
|
@@ -27,12 +32,19 @@ export const updateAccountSchema = z.object({
|
|
|
27
32
|
/** Ops-Signal ("idle", "watching", "backoff:3", ...). */
|
|
28
33
|
watchState: z.string().max(100).optional(),
|
|
29
34
|
displayName: z.string().max(200).optional(),
|
|
30
|
-
/** Audit reason for metadata.headers
|
|
31
|
-
* "
|
|
32
|
-
reason: z.string().min(1).max(100),
|
|
35
|
+
/** Audit reason for metadata.headers, honored for the system identity only
|
|
36
|
+
* ("watch_supervisor", "oauth_refresh"); user callers get a role-derived reason. */
|
|
37
|
+
reason: z.string().min(1).max(100).optional(),
|
|
33
38
|
});
|
|
34
39
|
type UpdateAccountPayload = z.infer<typeof updateAccountSchema>;
|
|
35
40
|
|
|
41
|
+
const SYSTEM_FALLBACK_REASON = "system";
|
|
42
|
+
|
|
43
|
+
function auditReasonFor(user: SessionUser, requestedReason: string | undefined): string {
|
|
44
|
+
if (user.id === SYSTEM_USER_ID) return requestedReason ?? SYSTEM_FALLBACK_REASON;
|
|
45
|
+
return user.roles.includes(ROLES.SystemAdmin) ? "system_admin" : "tenant_admin";
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
export const updateAccountHandler: WriteHandlerDef = {
|
|
37
49
|
name: "update-account",
|
|
38
50
|
schema: updateAccountSchema,
|
|
@@ -65,7 +77,7 @@ export const updateAccountHandler: WriteHandlerDef = {
|
|
|
65
77
|
};
|
|
66
78
|
const headers: MailAccountEventHeaders = {
|
|
67
79
|
providerName: current.provider,
|
|
68
|
-
reason: payload.reason,
|
|
80
|
+
reason: auditReasonFor(event.user, payload.reason),
|
|
69
81
|
};
|
|
70
82
|
await ctx.unsafeAppendEvent({
|
|
71
83
|
aggregateId: payload.accountId,
|
|
@@ -77,8 +77,6 @@ export const mailAccountListScreen: ProjectionListScreenDefinition = {
|
|
|
77
77
|
dormant: true,
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
// The reason is the audit label update-account requires; it is not in the
|
|
81
|
-
// layout, so the default is what gets written.
|
|
82
80
|
export const mailAccountEditScreen: ActionFormScreenDefinition = {
|
|
83
81
|
id: MAIL_ACCOUNT_EDIT_SCREEN_ID,
|
|
84
82
|
type: "actionForm",
|
|
@@ -87,7 +85,6 @@ export const mailAccountEditScreen: ActionFormScreenDefinition = {
|
|
|
87
85
|
fields: {
|
|
88
86
|
accountId: { type: "text", required: true },
|
|
89
87
|
displayName: { type: "text", required: true, maxLength: 200 },
|
|
90
|
-
reason: { type: "text", default: "tenant_admin" },
|
|
91
88
|
},
|
|
92
89
|
layout: {
|
|
93
90
|
sections: [{ fields: [{ field: "accountId", readOnly: true }, "displayName"] }],
|
|
@@ -21,8 +21,9 @@ import { INBOUND_MAIL_PROVIDER_EXTENSION, type InboundMailAccountStatus } from "
|
|
|
21
21
|
* config — tenant config reads (host/port/... of the plugins)
|
|
22
22
|
* registry — extension lookup in the factory (not plugin-internal)
|
|
23
23
|
* secrets — tenant secret reads (IMAP password, OAuth refresh token)
|
|
24
|
-
* auditUserId — audit identity for secret reads. Handler path:
|
|
25
|
-
*
|
|
24
|
+
* auditUserId — audit identity for secret reads. Handler path: unset,
|
|
25
|
+
* requireSecretsContext falls back to ctx.user.id; worker/provider
|
|
26
|
+
* path: system identity set by the resolver.
|
|
26
27
|
* metricsFor — metrics handle of a feature by name, same behavior as
|
|
27
28
|
* HandlerContext.metricsFor: unregistered metrics are a silent
|
|
28
29
|
* no-op instead of a throw.
|
|
@@ -172,7 +173,7 @@ export type InboundMailProviderPlugin = {
|
|
|
172
173
|
* for password accounts and providers without an oauth flow. */
|
|
173
174
|
readonly accessToken?: string;
|
|
174
175
|
/** Batch lookup of the provider message ids not yet ingested for this
|
|
175
|
-
* account (committed seen anchors,
|
|
176
|
+
* account (committed seen anchors; lists of any size are fine, queried in chunks). Call it after
|
|
176
177
|
* the listing/delta page and download MIME only for the returned ids.
|
|
177
178
|
* Ingest dedup stays the correctness anchor; this only saves downloads. */
|
|
178
179
|
readonly filterUnknownMessageIds?: (
|
|
@@ -88,6 +88,8 @@ const DEFAULT_WATCH_LEASE_TTL_SECONDS = 90;
|
|
|
88
88
|
* can add further scopes later without a schema change. */
|
|
89
89
|
const CURSOR_SCOPE = "default";
|
|
90
90
|
|
|
91
|
+
const SEEN_LOOKUP_CHUNK_SIZE = 1000;
|
|
92
|
+
|
|
91
93
|
export type InboundMailSupervisorDeps = {
|
|
92
94
|
/** Slim context for provider calls (`registry` is required; `secrets` is
|
|
93
95
|
* used for provider credential reads). */
|
|
@@ -223,11 +225,19 @@ export function createInboundMailSupervisor(
|
|
|
223
225
|
const piiKms = configuredPiiSubjectKms();
|
|
224
226
|
const records: MailAccountRecord[] = [];
|
|
225
227
|
for (const row of rows) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
let decrypted: Record<string, unknown>;
|
|
229
|
+
try {
|
|
230
|
+
decrypted = piiKms
|
|
231
|
+
? await decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
|
|
232
|
+
requestId: "inbound-mail-foundation:supervisor:list-accounts",
|
|
233
|
+
})
|
|
234
|
+
: row;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Class name only: the message may carry ciphertext or key details.
|
|
237
|
+
const errorName = error instanceof Error ? error.name : "unknown error";
|
|
238
|
+
log(`inbound-mail: account ${row.id} skipped — address did not decrypt (${errorName})`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
231
241
|
const decryptedAddress = decrypted["address"];
|
|
232
242
|
// Falling back to row.address would hand the ciphertext to a provider.
|
|
233
243
|
// One undecryptable account must not stop polling of every other tenant's accounts.
|
|
@@ -400,17 +410,23 @@ export function createInboundMailSupervisor(
|
|
|
400
410
|
function unknownMessageIdsFilterFor(account: MailAccountRecord) {
|
|
401
411
|
return async (providerMessageIds: readonly string[]): Promise<ReadonlySet<string>> => {
|
|
402
412
|
if (providerMessageIds.length === 0) return new Set();
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
{
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
// One bind parameter per id: a full-mailbox listing would exceed Postgres' 65535 limit.
|
|
414
|
+
const seenIds = new Set<string>();
|
|
415
|
+
for (let start = 0; start < providerMessageIds.length; start += SEEN_LOOKUP_CHUNK_SIZE) {
|
|
416
|
+
const seen = await selectMany<{ providerMessageId: string }>(
|
|
417
|
+
deps.db,
|
|
418
|
+
seenMessageTable,
|
|
419
|
+
{
|
|
420
|
+
tenantId: account.tenantId,
|
|
421
|
+
accountId: account.id,
|
|
422
|
+
providerMessageId: {
|
|
423
|
+
in: providerMessageIds.slice(start, start + SEEN_LOOKUP_CHUNK_SIZE),
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
{ fields: ["providerMessageId"] },
|
|
427
|
+
);
|
|
428
|
+
for (const row of seen) seenIds.add(row.providerMessageId);
|
|
429
|
+
}
|
|
414
430
|
return new Set(providerMessageIds.filter((id) => !seenIds.has(id)));
|
|
415
431
|
};
|
|
416
432
|
}
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "readRoles for tenant job status",
|
|
6
|
+
"detail": "`createJobsFeature({ readRoles: [\"PropertyManager\"] })` lets those app roles call `jobs:query:failures` and `jobs:query:tenant-runs` for their own tenant. They are added to the default `User`, `Editor` and admin roles, which stay. Before, a member who only holds app roles got access_denied.",
|
|
7
|
+
"migration": "Optional. Apps whose members hold only their own roles pass `createJobsFeature({ readRoles: [...] })`."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.388.0",
|
|
11
|
+
"type": "fix",
|
|
12
|
+
"title": "Deduplicated job add keeps the finished tenant run row",
|
|
13
|
+
"detail": "JobQueuedMeta carries the BullMQ creation time of the job. The queued hook only removes finished rows queued before that time (leftovers of a Redis reset), so re-announcing an already finished job no longer turns its row back into queued."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.384.0",
|
|
4
17
|
"type": "improvement",
|
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
createStaleRunSweepJob,
|
|
24
24
|
DEFAULT_JOB_RUN_STALE_TIMEOUT_HOURS,
|
|
25
25
|
} from "./handlers/stale-run-sweep.job.ts";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
26
|
+
import { createTenantFailuresQuery } from "./handlers/tenant-failures.query.ts";
|
|
27
|
+
import { createTenantRunsQuery } from "./handlers/tenant-runs.query.ts";
|
|
28
28
|
import { triggerWrite } from "./handlers/trigger.write.ts";
|
|
29
29
|
import { jobRunLogsTableMeta, jobRunsTableMeta } from "./job-run-table.ts";
|
|
30
30
|
import {
|
|
@@ -49,12 +49,23 @@ export type JobsFeatureOptions = {
|
|
|
49
49
|
// it). Must clear any legitimately long-running job (reindexEntity,
|
|
50
50
|
// projectionRebuild) — set high, not tight.
|
|
51
51
|
readonly staleRunTimeoutHours?: number;
|
|
52
|
+
// Additional app roles that may read their own tenant's job status and
|
|
53
|
+
// failures (jobs:query:failures, jobs:query:tenant-runs). Added to the default
|
|
54
|
+
// User/Editor/admin roles, which stay; the scope stays the caller's own tenant.
|
|
55
|
+
readonly readRoles?: readonly string[];
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefinition {
|
|
55
59
|
const retentionDays = options.retentionDays ?? DEFAULT_JOB_RUN_RETENTION_DAYS;
|
|
56
60
|
const staleRunTimeoutHours = options.staleRunTimeoutHours ?? DEFAULT_JOB_RUN_STALE_TIMEOUT_HOURS;
|
|
61
|
+
const readRoles = options.readRoles ?? [];
|
|
62
|
+
if (readRoles.includes("anonymous")) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`createJobsFeature: readRoles must not contain "anonymous" — job status and failures are only readable by members of the tenant.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
57
67
|
return defineFeature("jobs", (r) => {
|
|
68
|
+
r.adoptsBundledLists();
|
|
58
69
|
r.describe(
|
|
59
70
|
"Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops `queued` rows older than `retentionDays`.",
|
|
60
71
|
);
|
|
@@ -137,8 +148,8 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
137
148
|
list: r.queryHandler(listQuery),
|
|
138
149
|
detail: r.queryHandler(detailQuery),
|
|
139
150
|
catalog: r.queryHandler(catalogQuery),
|
|
140
|
-
failures: r.queryHandler(
|
|
141
|
-
tenantRuns: r.queryHandler(
|
|
151
|
+
failures: r.queryHandler(createTenantFailuresQuery(readRoles)),
|
|
152
|
+
tenantRuns: r.queryHandler(createTenantRunsQuery(readRoles)),
|
|
142
153
|
consumers: r.queryHandler(consumersQuery),
|
|
143
154
|
consumerDetails: r.queryHandler(consumerDetailQuery),
|
|
144
155
|
};
|
|
@@ -30,44 +30,49 @@ function parseSubject(stored: string | null): Record<string, unknown> | null {
|
|
|
30
30
|
return Object.fromEntries(parsed.filter(isSubjectEntry));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
export function createTenantFailuresQuery(readRoles: readonly string[]) {
|
|
34
|
+
return defineQueryHandler({
|
|
35
|
+
name: "failures",
|
|
36
|
+
description:
|
|
37
|
+
"Lists the calling tenant's own failed jobs — one record per job and subject, newest first, each carrying a translation key for the reason, never the provider's own error message; the optional subject filter matches exactly: all declared subjectFields with the same JSON types as the job payload; use it to tell a tenant that their asynchronous job failed instead of leaving the screen waiting.",
|
|
38
|
+
schema: z.object({
|
|
39
|
+
jobName: z.string().optional(),
|
|
40
|
+
subject: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
41
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
42
|
+
}),
|
|
43
|
+
// Every membership rank: a failure record carries a job name and a
|
|
44
|
+
// translation key, nothing a team member of the tenant may not see.
|
|
45
|
+
access: {
|
|
46
|
+
roles: [...new Set([...access.roles("User", "Editor", ...access.admin), ...readRoles])],
|
|
47
|
+
},
|
|
48
|
+
handler: async (query, ctx) => {
|
|
49
|
+
if (!ctx.systemDb) {
|
|
50
|
+
throw new InternalError({
|
|
51
|
+
message:
|
|
52
|
+
"jobs:query:failures requires ctx.systemDb (feature must declare r.systemScope())",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// assertTenantMatch is a self-check on the caller and returns the same
|
|
56
|
+
// unfiltered system-mode db (tenant-db.ts) — the explicit tenantId below
|
|
57
|
+
// is the filter, assertRowsTenant the second net. There is no
|
|
58
|
+
// cross-tenant mode here: SystemAdmin reads runs via jobs:query:list.
|
|
59
|
+
const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
|
|
60
|
+
const where: Record<string, unknown> = { tenantId: [query.user.tenantId] };
|
|
61
|
+
if (query.payload.jobName) where["jobName"] = query.payload.jobName;
|
|
62
|
+
if (query.payload.subject) where["subject"] = serializeJobSubject(query.payload.subject);
|
|
63
|
+
const rows = await selectMany<TenantJobFailureRow>(db, tenantJobFailuresTable, where, {
|
|
64
|
+
orderBy: { col: "failedAt", direction: "desc" },
|
|
65
|
+
limit: query.payload.limit ?? DEFAULT_LIMIT,
|
|
49
66
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
return {
|
|
64
|
-
rows: ctx.systemDb.assertRowsTenant(rows, "tenantId").map((row) => ({
|
|
65
|
-
jobName: row.jobName,
|
|
66
|
-
subject: parseSubject(row.subject),
|
|
67
|
-
messageKey: row.messageKey,
|
|
68
|
-
failedAt: row.failedAt,
|
|
69
|
-
})),
|
|
70
|
-
nextCursor: null,
|
|
71
|
-
};
|
|
72
|
-
},
|
|
73
|
-
});
|
|
67
|
+
return {
|
|
68
|
+
rows: ctx.systemDb.assertRowsTenant(rows, "tenantId").map((row) => ({
|
|
69
|
+
jobName: row.jobName,
|
|
70
|
+
subject: parseSubject(row.subject),
|
|
71
|
+
messageKey: row.messageKey,
|
|
72
|
+
failedAt: row.failedAt,
|
|
73
|
+
})),
|
|
74
|
+
nextCursor: null,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|