@cosmicdrift/kumiko-bundled-features 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/dist/agent-tools/index.d.ts +1 -1
- package/dist/agent-tools/index.js +1 -1
- package/dist/agent-tools/tool-dispatch.d.ts +12 -7
- package/dist/agent-tools/tool-dispatch.js +42 -31
- package/dist/audit/feature.js +1 -0
- package/dist/audit/handlers/audit-events.js +1 -1
- package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +1 -0
- package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-email-verification.write.js +1 -0
- package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-password-reset.write.js +1 -0
- package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/signup-request.write.js +4 -0
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
- package/dist/auth-email-password/handlers/token-request-handler.js +5 -0
- package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
- package/dist/auth-email-password/magic-link-mail.js +7 -3
- package/dist/auth-email-password/signup-activation.d.ts +1 -0
- package/dist/auth-email-password/signup-activation.js +1 -0
- package/dist/auth-email-password/web/auth-client.d.ts +2 -2
- package/dist/auth-email-password/web/auth-client.js +8 -4
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -1
- package/dist/auth-email-password/web/auth-redirect.js +20 -25
- package/dist/auth-email-password/web/default-auth-pages-gate.js +6 -3
- package/dist/auth-email-password/web/forgot-password-screen.d.ts +4 -1
- package/dist/auth-email-password/web/forgot-password-screen.js +7 -2
- package/dist/auth-email-password/web/index.d.ts +1 -0
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/login-screen.js +11 -6
- package/dist/auth-email-password/web/reset-password-screen.js +7 -1
- package/dist/auth-email-password/web/return-path.d.ts +4 -0
- package/dist/auth-email-password/web/return-path.js +34 -0
- package/dist/auth-email-password/web/session.js +6 -1
- package/dist/auth-email-password/web/signup-complete-screen.js +11 -3
- package/dist/auth-email-password/web/signup-screen.d.ts +4 -1
- package/dist/auth-email-password/web/signup-screen.js +8 -3
- package/dist/auth-mfa/config.d.ts +1 -1
- package/dist/auth-mfa/config.js +3 -7
- package/dist/auth-mfa/feature-exports.d.ts +2 -2
- package/dist/auth-mfa/feature.js +8 -0
- package/dist/auth-mfa/mfa-status-checker.js +5 -3
- package/dist/auth-mfa/web/i18n.js +5 -1
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
- package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
- package/dist/billing-foundation/checkout-core.d.ts +9 -14
- package/dist/billing-foundation/checkout-core.js +24 -39
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
- package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
- package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
- package/dist/billing-foundation/events.d.ts +1 -0
- package/dist/billing-foundation/events.js +1 -0
- package/dist/billing-foundation/feature.js +3 -1
- package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
- package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
- package/dist/billing-foundation/handlers/process-event.write.js +8 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
- package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
- package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
- package/dist/billing-foundation/i18n.js +22 -0
- package/dist/billing-foundation/index.d.ts +1 -1
- package/dist/billing-foundation/plan-catalog.d.ts +21 -5
- package/dist/billing-foundation/plan-catalog.js +25 -16
- package/dist/billing-foundation/plan-switch.d.ts +12 -0
- package/dist/billing-foundation/plan-switch.js +60 -0
- package/dist/billing-foundation/types.d.ts +57 -1
- package/dist/billing-foundation/types.js +9 -0
- package/dist/billing-foundation/validate-options.js +17 -0
- package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
- package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
- package/dist/billing-foundation/web/constants.d.ts +4 -0
- package/dist/billing-foundation/web/constants.js +4 -0
- package/dist/billing-foundation/web/plan-price.d.ts +15 -0
- package/dist/billing-foundation/web/plan-price.js +32 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
- package/dist/cap-counter/cap-reservation.js +20 -7
- package/dist/cap-counter/enforce-cap.js +2 -1
- package/dist/cap-counter/feature.js +1 -0
- package/dist/cap-counter/stock-cap-guard.js +7 -1
- package/dist/cap-overview/feature.js +1 -0
- package/dist/channel-email/feature.js +1 -1
- package/dist/config/handlers/cascade.query.d.ts +5 -0
- package/dist/config/handlers/cascade.query.js +4 -3
- package/dist/config/handlers/set.write.js +12 -2
- package/dist/config/handlers/values.query.d.ts +5 -0
- package/dist/config/handlers/values.query.js +4 -3
- package/dist/config/write-helpers.d.ts +1 -0
- package/dist/config/write-helpers.js +18 -9
- package/dist/crypto-shredding/handlers/forget-subject.write.js +14 -11
- package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
- package/dist/delivery/channel-context.js +6 -3
- package/dist/delivery/db/queries/preferences.js +2 -2
- package/dist/delivery/delivery-service.js +22 -7
- package/dist/delivery/feature.js +1 -0
- package/dist/delivery/handlers/preferences.query.d.ts +1 -1
- package/dist/delivery/handlers/preferences.query.js +20 -1
- package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/system-channel-context.d.ts +15 -0
- package/dist/delivery/system-channel-context.js +5 -0
- package/dist/delivery/unsubscribe-link.d.ts +3 -1
- package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
- package/dist/delivery/unsubscribe-page-strings.js +1 -0
- package/dist/delivery/unsubscribe-page.d.ts +2 -0
- package/dist/delivery/unsubscribe-page.js +5 -0
- package/dist/delivery/unsubscribe.js +25 -7
- package/dist/document-ingest-foundation/append-missing-extract-event.d.ts +21 -0
- package/dist/document-ingest-foundation/append-missing-extract-event.js +42 -0
- package/dist/document-ingest-foundation/events.d.ts +11 -0
- package/dist/document-ingest-foundation/events.js +11 -0
- package/dist/document-ingest-foundation/index.d.ts +2 -1
- package/dist/document-ingest-foundation/index.js +2 -1
- package/dist/feature-toggles/feature.js +1 -0
- package/dist/file-foundation/index.d.ts +1 -1
- package/dist/file-foundation/index.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
- package/dist/file-foundation/read-file-ref-content.js +6 -1
- package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
- package/dist/form-draft/db/queries/cleanup.js +2 -2
- package/dist/form-draft/decrypt-draft.js +5 -1
- package/dist/host-routing/dispatch.js +11 -20
- package/dist/host-routing/validate-options.js +3 -0
- package/dist/inbound-mail-foundation/feature.js +1 -0
- package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
- package/dist/inbound-mail-foundation/i18n.js +0 -1
- package/dist/inbound-mail-foundation/screens.js +0 -3
- package/dist/inbound-mail-foundation/types.d.ts +4 -3
- package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
- package/dist/jobs/feature.d.ts +1 -0
- package/dist/jobs/feature.js +9 -4
- package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-failures.query.js +45 -41
- package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-runs.query.js +54 -50
- package/dist/jobs/job-run-logger.js +5 -4
- package/dist/ledger/web/index.js +1 -1
- package/dist/legal-pages/seed.d.ts +3 -1
- package/dist/legal-pages/seed.js +2 -2
- package/dist/mail-foundation/feature.d.ts +3 -3
- package/dist/mail-foundation/feature.js +3 -0
- package/dist/mail-transport-smtp/config-keys.js +1 -1
- package/dist/mail-transport-smtp/feature.js +6 -5
- package/dist/managed-pages/constants.d.ts +1 -0
- package/dist/managed-pages/constants.js +1 -0
- package/dist/managed-pages/feature.js +3 -1
- package/dist/managed-pages/index.d.ts +1 -0
- package/dist/managed-pages/index.js +1 -0
- package/dist/notes-history/handlers/add-note.write.js +1 -1
- package/dist/personal-access-tokens/feature.d.ts +1 -1
- package/dist/personal-access-tokens/feature.js +5 -0
- package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
- package/dist/renderer-pdf/render-table-pdf.js +22 -10
- package/dist/secrets/write-gate.js +8 -2
- package/dist/sessions/feature.d.ts +2 -2
- package/dist/sessions/feature.js +5 -0
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.js +1 -1
- package/dist/sessions/session-callbacks.d.ts +0 -2
- package/dist/sessions/session-callbacks.js +1 -3
- package/dist/shared/decrypt-stored-pii.d.ts +1 -0
- package/dist/shared/decrypt-stored-pii.js +6 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/mail-template-body.d.ts +6 -2
- package/dist/shared/mail-template-body.js +7 -4
- package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
- package/dist/shared/purge-files-reaching-subject.js +19 -2
- package/dist/shared/run-in-sub-transaction.js +5 -5
- package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
- package/dist/step-dispatcher/feature.js +1 -0
- package/dist/step-dispatcher/mail-runner.d.ts +1 -0
- package/dist/subscription-dodo/client.d.ts +17 -0
- package/dist/subscription-dodo/client.js +31 -0
- package/dist/subscription-dodo/constants.d.ts +12 -0
- package/dist/subscription-dodo/constants.js +19 -0
- package/dist/subscription-dodo/feature.d.ts +37 -0
- package/dist/subscription-dodo/feature.js +144 -0
- package/dist/subscription-dodo/index.d.ts +2 -0
- package/dist/subscription-dodo/index.js +4 -0
- package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
- package/dist/subscription-dodo/plugin-methods.js +260 -0
- package/dist/subscription-dodo/runtime.d.ts +27 -0
- package/dist/subscription-dodo/runtime.js +89 -0
- package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
- package/dist/subscription-dodo/verify-webhook.js +189 -0
- package/dist/subscription-stripe/feature.js +1 -0
- package/dist/subscription-stripe/runtime.js +7 -6
- package/dist/tags/feature.js +1 -0
- package/dist/template-resolver/api.d.ts +3 -2
- package/dist/template-resolver/api.js +24 -2
- package/dist/template-resolver/compile-document-body.js +1 -0
- package/dist/template-resolver/document-ops.d.ts +5 -0
- package/dist/template-resolver/document-ops.js +18 -2
- package/dist/template-resolver/feature.js +4 -4
- package/dist/template-resolver/handlers/bundle-import.js +3 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
- package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
- package/dist/template-resolver/handlers/document-base.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
- package/dist/template-resolver/handlers/layout-source.query.d.ts +4 -0
- package/dist/template-resolver/handlers/layout-source.query.js +26 -6
- package/dist/template-resolver/handlers/list.query.js +5 -6
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +21 -10
- package/dist/template-resolver/handlers/patch-draft.write.js +1 -1
- package/dist/template-resolver/handlers/send-test-status.query.d.ts +2 -1
- package/dist/template-resolver/handlers/send-test-status.query.js +18 -15
- package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
- package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
- package/dist/template-resolver/handlers/usage.query.js +26 -16
- package/dist/template-resolver/handlers/validate.query.js +7 -2
- package/dist/template-resolver/seeding.d.ts +1 -0
- package/dist/template-resolver/seeding.js +31 -12
- package/dist/template-resolver/store-template-resource.d.ts +1 -0
- package/dist/template-resolver/store-template-resource.js +65 -10
- package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
- package/dist/template-resolver/template-delivery-policy.js +0 -6
- package/dist/template-resolver/template-meta.js +7 -4
- package/dist/template-resolver/template-preview.js +4 -3
- package/dist/template-resolver/web/constants.d.ts +2 -2
- package/dist/template-resolver/web/constants.js +3 -3
- package/dist/template-resolver/web/i18n.js +3 -0
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
- package/dist/template-resolver/web/mail-layout-constants.js +7 -0
- package/dist/template-resolver/web/qualified-names.d.ts +1 -0
- package/dist/template-resolver/web/qualified-names.js +1 -0
- package/dist/tenant/feature.js +1 -0
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +10 -3
- package/dist/tenant/handlers/team-list.query.js +3 -3
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +8 -4
- package/dist/tenant-lifecycle/constants.d.ts +1 -0
- package/dist/tenant-lifecycle/constants.js +4 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tenant-lifecycle/stages.js +22 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
- package/dist/tier-engine/web/constants.js +2 -2
- package/dist/user/feature.js +1 -0
- package/dist/user-data-rights/feature.js +1 -0
- package/dist/user-data-rights/index.d.ts +1 -1
- package/dist/user-data-rights/index.js +1 -1
- package/dist/user-data-rights/run-export-jobs.js +3 -4
- package/dist/user-data-rights/run-forget-cleanup.js +2 -1
- package/dist/user-data-rights/web/constants.d.ts +6 -0
- package/dist/user-data-rights/web/constants.js +6 -0
- package/dist/user-data-rights/web/index.d.ts +1 -0
- package/dist/user-data-rights/web/index.js +1 -0
- package/dist/waitlist/feature.js +1 -0
- package/dist/waitlist/handlers/export-purge.write.js +2 -1
- package/dist/waitlist/handlers/submit.write.js +14 -10
- package/dist/waitlist/import.d.ts +3 -1
- package/dist/waitlist/import.js +26 -2
- package/dist/waitlist/legacy-migrate.js +18 -7
- package/dist/waitlist/link-on-signup.js +18 -6
- package/dist/waitlist/options.d.ts +5 -1
- package/dist/workflow-runner/handlers/resume-run.write.js +1 -1
- package/package.json +19 -9
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +12 -0
- package/src/auth-email-password/changes.json +12 -0
- package/src/auth-mfa/changes.json +12 -0
- package/src/billing-foundation/changes.json +55 -0
- package/src/cap-counter/changes.json +30 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +18 -0
- package/src/crypto-shredding/changes.json +18 -0
- package/src/delivery/changes.json +42 -0
- package/src/document-ingest-foundation/changes.json +6 -0
- package/src/form-draft/changes.json +6 -0
- package/src/host-routing/changes.json +12 -0
- package/src/inbound-mail-foundation/changes.json +18 -0
- package/src/jobs/changes.json +13 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/mail-transport-smtp/changes.json +6 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-pdf/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/sessions/changes.json +7 -0
- package/src/step-dispatcher/changes.json +12 -0
- package/src/subscription-dodo/changes.json +14 -0
- package/src/subscription-stripe/changes.json +12 -0
- package/src/template-resolver/changes.json +84 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +12 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/user-data-rights/changes.json +12 -0
- package/src/waitlist/changes.json +36 -0
- package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
- package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { ConfigAccessor, Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
|
|
4
|
+
import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
|
|
5
|
+
import type { ChannelContext } from "./types.ts";
|
|
6
|
+
export type SystemChannelContextDeps = {
|
|
7
|
+
readonly db: DbConnection;
|
|
8
|
+
readonly registry: Registry;
|
|
9
|
+
readonly filesForTenant: (tenantId: string) => FileContext | undefined;
|
|
10
|
+
};
|
|
11
|
+
export type SystemChannelContextOptions = {
|
|
12
|
+
readonly config?: ConfigAccessor;
|
|
13
|
+
readonly secrets?: SecretsContext;
|
|
14
|
+
};
|
|
15
|
+
export declare function buildSystemChannelContext(deps: SystemChannelContextDeps, options?: SystemChannelContextOptions): ChannelContext;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { buildChannelContext } from "./channel-context.js";
|
|
3
|
+
export function buildSystemChannelContext(deps, options = {}) {
|
|
4
|
+
return buildChannelContext(deps.db, deps.registry, undefined, SYSTEM_TENANT_ID, options.secrets, options.config, deps.filesForTenant);
|
|
5
|
+
}
|
|
@@ -5,7 +5,9 @@ export type UnsubscribeLinkConfig = {
|
|
|
5
5
|
readonly secret: string;
|
|
6
6
|
readonly baseUrl: string;
|
|
7
7
|
/** The sending tenant's own public origin (subdomain or custom domain), null when it has
|
|
8
|
-
* none. The host must serve this app's `/api` routes
|
|
8
|
+
* none. The host must serve this app's `/api` routes, and the callback must only return a
|
|
9
|
+
* verified, active domain of the tenant (the check that lets the app serve under it), else null:
|
|
10
|
+
* the link carries a bearer token that unsubscribes and yields a resubscribe token. Null, a throw or an unusable value
|
|
9
11
|
* fall back to `baseUrl`, so a missing tenant origin never fails a send. */
|
|
10
12
|
readonly tenantOrigin?: (ctx: ChannelContext) => Promise<string | null | undefined>;
|
|
11
13
|
};
|
|
@@ -10,6 +10,7 @@ export declare const UNSUBSCRIBE_PAGE_EN: {
|
|
|
10
10
|
readonly "mail.unsubscribe.resubscribedMessage": "You have been subscribed again.";
|
|
11
11
|
readonly "mail.unsubscribe.failed": "That did not work. Please try again later.";
|
|
12
12
|
readonly "mail.unsubscribe.failedTitle": "Something went wrong";
|
|
13
|
+
readonly "mail.unsubscribe.resubscribeLimitReached": "This address cannot be subscribed again here.";
|
|
13
14
|
readonly "mail.unsubscribe.invalidTitle": "Link no longer valid";
|
|
14
15
|
readonly "mail.unsubscribe.invalidMessage": "This link is invalid or has expired. Please use the unsubscribe link in a more recent email.";
|
|
15
16
|
};
|
|
@@ -13,6 +13,7 @@ export const UNSUBSCRIBE_PAGE_EN = {
|
|
|
13
13
|
"mail.unsubscribe.resubscribedMessage": "You have been subscribed again.",
|
|
14
14
|
"mail.unsubscribe.failed": "That did not work. Please try again later.",
|
|
15
15
|
"mail.unsubscribe.failedTitle": "Something went wrong",
|
|
16
|
+
"mail.unsubscribe.resubscribeLimitReached": "This address cannot be subscribed again here.",
|
|
16
17
|
"mail.unsubscribe.invalidTitle": "Link no longer valid",
|
|
17
18
|
"mail.unsubscribe.invalidMessage": "This link is invalid or has expired. Please use the unsubscribe link in a more recent email.",
|
|
18
19
|
};
|
|
@@ -135,6 +135,11 @@ ${tokenForm(DELIVERY_RESUBSCRIBE_PATH, state.resubscribeToken, mailT(lang, "mail
|
|
|
135
135
|
title: mailT(lang, "mail.unsubscribe.failedTitle"),
|
|
136
136
|
bodyHtml: paragraph("mail.unsubscribe.failed"),
|
|
137
137
|
};
|
|
138
|
+
case "resubscribeLimitReached":
|
|
139
|
+
return {
|
|
140
|
+
title: mailT(lang, "mail.unsubscribe.failedTitle"),
|
|
141
|
+
bodyHtml: paragraph("mail.unsubscribe.resubscribeLimitReached"),
|
|
142
|
+
};
|
|
138
143
|
case "invalid":
|
|
139
144
|
return {
|
|
140
145
|
title: mailT(lang, "mail.unsubscribe.invalidTitle"),
|
|
@@ -373,10 +373,16 @@ async function verifyForPage(token, encodedSecret, purpose, headers) {
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
async function verifyPost(request, token, encodedSecret, purpose) {
|
|
376
|
-
return isFormRequest(request.headers["content-type"])
|
|
376
|
+
return isFormRequest(request.headers["content-type"]) && !isOneClickPost(request)
|
|
377
377
|
? verifyForPage(token, encodedSecret, purpose, request.headers)
|
|
378
378
|
: verifyUnsubscribeToken(token, encodedSecret, purpose);
|
|
379
379
|
}
|
|
380
|
+
// RFC 8058 mail-provider POSTs are form-encoded too, but nobody looks at the answer: they keep
|
|
381
|
+
// the JSON rejection and get no branded page.
|
|
382
|
+
function isOneClickPost(request) {
|
|
383
|
+
return (isFormRequest(request.headers["content-type"]) &&
|
|
384
|
+
request.rawBody.trim() === DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE);
|
|
385
|
+
}
|
|
380
386
|
function isFormRequest(contentType) {
|
|
381
387
|
return contentType?.includes("application/x-www-form-urlencoded") === true;
|
|
382
388
|
}
|
|
@@ -435,22 +441,31 @@ export function createUnsubscribeRoutes(options) {
|
|
|
435
441
|
method: "POST",
|
|
436
442
|
path: DELIVERY_UNSUBSCRIBE_PATH,
|
|
437
443
|
entry: "signature",
|
|
438
|
-
verify: async (request) =>
|
|
444
|
+
verify: async (request) => {
|
|
445
|
+
const verified = await verifyPost(request, tokenFromPostRequest(request), encodedSecret, "unsubscribe");
|
|
446
|
+
return verified.kind === "rejected"
|
|
447
|
+
? verified
|
|
448
|
+
: { ...verified, oneClick: isOneClickPost(request) };
|
|
449
|
+
},
|
|
439
450
|
handler: async (c, verified, deps) => {
|
|
440
451
|
if (verified.kind === "rejected") {
|
|
441
452
|
return pageResponse(c, deps, { kind: "invalid" }, verified, 400);
|
|
442
453
|
}
|
|
454
|
+
// No chrome lookup for a one-click client: it never renders the page.
|
|
455
|
+
const pageScope = verified.oneClick
|
|
456
|
+
? { tenantId: undefined, locale: verified.locale }
|
|
457
|
+
: verified;
|
|
443
458
|
// Token-verify passed — everything below is a legitimate write. Don't
|
|
444
459
|
// swallow write-errors as "invalid token", that would mask real bugs
|
|
445
460
|
// (e.g. events-table missing, DB down) behind a misleading 400.
|
|
446
461
|
const succeeded = await dispatchUnsubscribeWrite(verified, deps);
|
|
447
462
|
if (!succeeded) {
|
|
448
|
-
return pageResponse(c, deps, { kind: "failed" },
|
|
463
|
+
return pageResponse(c, deps, { kind: "failed" }, pageScope, 500);
|
|
449
464
|
}
|
|
450
465
|
return pageResponse(c, deps, {
|
|
451
466
|
kind: "unsubscribed",
|
|
452
467
|
resubscribeToken: await signResubscribeToken(verified, encodedSecret),
|
|
453
|
-
},
|
|
468
|
+
}, pageScope, 200);
|
|
454
469
|
},
|
|
455
470
|
});
|
|
456
471
|
// Undo direction: token only from the JSON/form body, never the query
|
|
@@ -468,9 +483,12 @@ export function createUnsubscribeRoutes(options) {
|
|
|
468
483
|
const outcome = await dispatchResubscribeWrite(verified, deps);
|
|
469
484
|
// The undo button on the unsubscribed page posts a form; a browser should land on a page.
|
|
470
485
|
if (isFormRequest(c.req.header("content-type"))) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
486
|
+
if (outcome === "success") {
|
|
487
|
+
return pageResponse(c, deps, { kind: "resubscribed" }, verified, 200);
|
|
488
|
+
}
|
|
489
|
+
return outcome === "limit_reached"
|
|
490
|
+
? pageResponse(c, deps, { kind: "resubscribeLimitReached" }, verified, 409)
|
|
491
|
+
: pageResponse(c, deps, { kind: "failed" }, verified, 500);
|
|
474
492
|
}
|
|
475
493
|
if (outcome === "limit_reached") {
|
|
476
494
|
return c.json({ error: { code: DeliveryErrors.resubscribeLimitReached } }, 409, NO_STORE_HEADERS);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import type { Registry, SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
export type ExistingDocumentExtract = {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly meta: {
|
|
6
|
+
readonly error?: unknown;
|
|
7
|
+
} | null;
|
|
8
|
+
};
|
|
9
|
+
export type AppendMissingExtractEventDeps = {
|
|
10
|
+
readonly tenantDb: TenantDb;
|
|
11
|
+
readonly registry: Registry;
|
|
12
|
+
readonly actor: SessionUser;
|
|
13
|
+
readonly tenantId: string;
|
|
14
|
+
};
|
|
15
|
+
export type AppendMissingExtractEventOptions = {
|
|
16
|
+
readonly readyEventQn: string;
|
|
17
|
+
readonly failedEventQn: string;
|
|
18
|
+
readonly mapError?: (error: string) => string;
|
|
19
|
+
};
|
|
20
|
+
export type AppendMissingExtractEventResult = "appended-ready" | "appended-failed" | "already-terminal";
|
|
21
|
+
export declare function appendMissingDocumentExtractEvent(deps: AppendMissingExtractEventDeps, existing: ExistingDocumentExtract, fileRefId: string, options: AppendMissingExtractEventOptions): Promise<AppendMissingExtractEventResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
import { appendEventInTenantDb, getStreamVersionInTenantDb, } from "@cosmicdrift/kumiko-framework/event-store";
|
|
3
|
+
import { DOCUMENT_EXTRACT_AGGREGATE_TYPE, documentExtractFailedPayloadSchema, documentExtractReadyPayloadSchema, } from "./events.js";
|
|
4
|
+
// Version 1 of the stream is the extract's created event; anything beyond it is the terminal event.
|
|
5
|
+
const CREATED_ONLY_STREAM_VERSION = 1;
|
|
6
|
+
const UNKNOWN_EXTRACT_ERROR = "unknown extract error";
|
|
7
|
+
// A previous delivery created the extract but died before appending its ready/failed event.
|
|
8
|
+
// Without the event, consumers waiting on it hang forever.
|
|
9
|
+
export async function appendMissingDocumentExtractEvent(deps, existing, fileRefId, options) {
|
|
10
|
+
if (deps.tenantDb.tenantId !== deps.tenantId) {
|
|
11
|
+
throw new InternalError({
|
|
12
|
+
message: `document-ingest-foundation: tenantDb is bound to tenant ${deps.tenantDb.tenantId}, not ${deps.tenantId}`,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const version = await getStreamVersionInTenantDb(deps.tenantDb, existing.id);
|
|
16
|
+
// A missing stream means a wrong id; appending would make the terminal event the first event of a new stream.
|
|
17
|
+
if (version === 0) {
|
|
18
|
+
throw new InternalError({
|
|
19
|
+
message: `document-ingest-foundation: no document extract stream for id ${existing.id}`,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// skip: the terminal event was already written, nothing to catch up
|
|
23
|
+
if (version > CREATED_ONLY_STREAM_VERSION)
|
|
24
|
+
return "already-terminal";
|
|
25
|
+
const metaError = existing.meta?.error;
|
|
26
|
+
const failed = typeof metaError === "string";
|
|
27
|
+
await appendEventInTenantDb(deps.tenantDb, {
|
|
28
|
+
aggregateId: existing.id,
|
|
29
|
+
aggregateType: DOCUMENT_EXTRACT_AGGREGATE_TYPE,
|
|
30
|
+
expectedVersion: version,
|
|
31
|
+
type: failed ? options.failedEventQn : options.readyEventQn,
|
|
32
|
+
payload: failed
|
|
33
|
+
? documentExtractFailedPayloadSchema.parse({
|
|
34
|
+
documentExtractId: existing.id,
|
|
35
|
+
fileRefId,
|
|
36
|
+
error: options.mapError?.(metaError) || metaError || UNKNOWN_EXTRACT_ERROR,
|
|
37
|
+
})
|
|
38
|
+
: documentExtractReadyPayloadSchema.parse({ documentExtractId: existing.id, fileRefId }),
|
|
39
|
+
metadata: { userId: deps.actor.id },
|
|
40
|
+
}, { registry: deps.registry });
|
|
41
|
+
return failed ? "appended-failed" : "appended-ready";
|
|
42
|
+
}
|
|
@@ -26,3 +26,14 @@ export declare const documentIngestSkippedPayloadSchema: z.ZodObject<{
|
|
|
26
26
|
}>;
|
|
27
27
|
}, z.core.$strip>;
|
|
28
28
|
export type DocumentIngestSkippedPayload = z.infer<typeof documentIngestSkippedPayloadSchema>;
|
|
29
|
+
export declare const documentExtractReadyPayloadSchema: z.ZodObject<{
|
|
30
|
+
documentExtractId: z.ZodString;
|
|
31
|
+
fileRefId: z.ZodString;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type DocumentExtractReadyPayload = z.infer<typeof documentExtractReadyPayloadSchema>;
|
|
34
|
+
export declare const documentExtractFailedPayloadSchema: z.ZodObject<{
|
|
35
|
+
documentExtractId: z.ZodString;
|
|
36
|
+
fileRefId: z.ZodString;
|
|
37
|
+
error: z.ZodString;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
export type DocumentExtractFailedPayload = z.infer<typeof documentExtractFailedPayloadSchema>;
|
|
@@ -35,3 +35,14 @@ export const documentIngestSkippedPayloadSchema = z.object({
|
|
|
35
35
|
size: z.number().int().min(0),
|
|
36
36
|
reason: z.enum(["file-too-large", "unsupported-mime-type"]),
|
|
37
37
|
});
|
|
38
|
+
// Terminal events of a document-extract stream. The QNs belong to the provider that registers
|
|
39
|
+
// them; the foundation only owns the shared payload shape.
|
|
40
|
+
export const documentExtractReadyPayloadSchema = z.object({
|
|
41
|
+
documentExtractId: z.string().min(1),
|
|
42
|
+
fileRefId: z.string().min(1),
|
|
43
|
+
});
|
|
44
|
+
export const documentExtractFailedPayloadSchema = z.object({
|
|
45
|
+
documentExtractId: z.string().min(1),
|
|
46
|
+
fileRefId: z.string().min(1),
|
|
47
|
+
error: z.string().min(1),
|
|
48
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { documentIngestAggregateId } from "./aggregate-id.ts";
|
|
2
|
+
export { type AppendMissingExtractEventDeps, type AppendMissingExtractEventOptions, type AppendMissingExtractEventResult, appendMissingDocumentExtractEvent, type ExistingDocumentExtract, } from "./append-missing-extract-event.ts";
|
|
2
3
|
export { type DocumentExtractMeta, documentExtractEntity, documentExtractsTable, type IngestPage, } from "./entity.ts";
|
|
3
|
-
export { DOCUMENT_EXTRACT_AGGREGATE_TYPE, DOCUMENT_INGEST_AGGREGATE_TYPE, DOCUMENT_INGEST_REQUESTED_EVENT_QN, DOCUMENT_INGEST_REQUESTED_EVENT_SHORT, DOCUMENT_INGEST_SKIPPED_EVENT_QN, DOCUMENT_INGEST_SKIPPED_EVENT_SHORT, type DocumentIngestRequestedPayload, type DocumentIngestSkippedPayload, documentIngestRequestedPayloadSchema, documentIngestSkippedPayloadSchema, } from "./events.ts";
|
|
4
|
+
export { DOCUMENT_EXTRACT_AGGREGATE_TYPE, DOCUMENT_INGEST_AGGREGATE_TYPE, DOCUMENT_INGEST_REQUESTED_EVENT_QN, DOCUMENT_INGEST_REQUESTED_EVENT_SHORT, DOCUMENT_INGEST_SKIPPED_EVENT_QN, DOCUMENT_INGEST_SKIPPED_EVENT_SHORT, type DocumentExtractFailedPayload, type DocumentExtractReadyPayload, type DocumentIngestRequestedPayload, type DocumentIngestSkippedPayload, documentExtractFailedPayloadSchema, documentExtractReadyPayloadSchema, documentIngestRequestedPayloadSchema, documentIngestSkippedPayloadSchema, } from "./events.ts";
|
|
4
5
|
export { createDocumentIngestFoundationFeature } from "./feature.ts";
|
|
5
6
|
export { readIngestPages, writeIngestPages } from "./pages.ts";
|
|
6
7
|
export { type DocumentIngestProviderOptions, documentIngestProviderOptionsSchema, documentIngestProviderTrigger, EXT_DOCUMENT_INGEST_PROVIDER, listIngestibleMimeTypes, type ResolvedDocumentIngestProvider, resolveDocumentIngestProviders, } from "./providers.ts";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Public API of the document-ingest-foundation bundled-feature.
|
|
2
2
|
export { documentIngestAggregateId } from "./aggregate-id.js";
|
|
3
|
+
export { appendMissingDocumentExtractEvent, } from "./append-missing-extract-event.js";
|
|
3
4
|
export { documentExtractEntity, documentExtractsTable, } from "./entity.js";
|
|
4
|
-
export { DOCUMENT_EXTRACT_AGGREGATE_TYPE, DOCUMENT_INGEST_AGGREGATE_TYPE, DOCUMENT_INGEST_REQUESTED_EVENT_QN, DOCUMENT_INGEST_REQUESTED_EVENT_SHORT, DOCUMENT_INGEST_SKIPPED_EVENT_QN, DOCUMENT_INGEST_SKIPPED_EVENT_SHORT, documentIngestRequestedPayloadSchema, documentIngestSkippedPayloadSchema, } from "./events.js";
|
|
5
|
+
export { DOCUMENT_EXTRACT_AGGREGATE_TYPE, DOCUMENT_INGEST_AGGREGATE_TYPE, DOCUMENT_INGEST_REQUESTED_EVENT_QN, DOCUMENT_INGEST_REQUESTED_EVENT_SHORT, DOCUMENT_INGEST_SKIPPED_EVENT_QN, DOCUMENT_INGEST_SKIPPED_EVENT_SHORT, documentExtractFailedPayloadSchema, documentExtractReadyPayloadSchema, documentIngestRequestedPayloadSchema, documentIngestSkippedPayloadSchema, } from "./events.js";
|
|
5
6
|
export { createDocumentIngestFoundationFeature } from "./feature.js";
|
|
6
7
|
export { readIngestPages, writeIngestPages } from "./pages.js";
|
|
7
8
|
export { documentIngestProviderOptionsSchema, documentIngestProviderTrigger, EXT_DOCUMENT_INGEST_PROVIDER, listIngestibleMimeTypes, resolveDocumentIngestProviders, } from "./providers.js";
|
|
@@ -8,6 +8,7 @@ import { FEATURE_TOGGLES_I18N } from "./i18n.js";
|
|
|
8
8
|
import { FEATURE_TOGGLE_SET_EVENT_NAME, FeatureToggleHandlers, FeatureToggleQueries, TOGGLE_ADMIN_SCREEN_ID, } from "./web/constants.js";
|
|
9
9
|
export function createFeatureTogglesFeature(options = {}) {
|
|
10
10
|
return defineFeature("feature-toggles", (r) => {
|
|
11
|
+
r.adoptsBundledLists();
|
|
11
12
|
r.describe('Persists per-feature enabled/disabled state in the `store_global_feature_state` table and exposes a `set` write-handler plus `list`/`registered` query-handlers so operators can flip features at runtime without redeploying. Each API instance keeps an in-memory `GlobalFeatureToggleRuntime` snapshot (initialize it via `createFeatureToggleRuntime`, pass a `() => runtime` accessor to `createFeatureTogglesFeature`) that the dispatcher gate reads on every request; a `toggle-cache-sync` multi-stream projection with `delivery: "shared"` syncs the snapshot across instances whenever a `toggle-set` event is appended.');
|
|
12
13
|
r.uiHints({
|
|
13
14
|
displayLabel: "Feature Toggles · Operator Switches",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createFileFoundationFeature, createFileProviderForTenant, type FileProviderContext, type FileProviderPlugin, isFileProviderPlugin, } from "./feature.ts";
|
|
2
|
-
export { type FileRefContent, FileRefTooLargeError, fileContextForTenant, fileContextFromResolver, readFileRefContent, } from "./read-file-ref-content.ts";
|
|
2
|
+
export { type FileRefContent, FileRefTooLargeError, FileStorageUnavailableError, fileContextForTenant, fileContextFromResolver, readFileRefContent, } from "./read-file-ref-content.ts";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Public API of the file-foundation bundled-feature.
|
|
2
2
|
export { createFileFoundationFeature, createFileProviderForTenant, isFileProviderPlugin, } from "./feature.js";
|
|
3
|
-
export { FileRefTooLargeError, fileContextForTenant, fileContextFromResolver, readFileRefContent, } from "./read-file-ref-content.js";
|
|
3
|
+
export { FileRefTooLargeError, FileStorageUnavailableError, fileContextForTenant, fileContextFromResolver, readFileRefContent, } from "./read-file-ref-content.js";
|
|
@@ -15,6 +15,9 @@ export declare class FileRefTooLargeError extends Error {
|
|
|
15
15
|
readonly size: number;
|
|
16
16
|
constructor(size: number);
|
|
17
17
|
}
|
|
18
|
+
export declare class FileStorageUnavailableError extends Error {
|
|
19
|
+
constructor();
|
|
20
|
+
}
|
|
18
21
|
export declare function readFileRefContent(args: {
|
|
19
22
|
readonly db: DbRunner | TenantDb;
|
|
20
23
|
readonly filesForTenant: (tenantId: string) => FileContext | undefined;
|
|
@@ -22,6 +22,11 @@ export class FileRefTooLargeError extends Error {
|
|
|
22
22
|
this.size = size;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
export class FileStorageUnavailableError extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super("readFileRefContent: no file storage is available for the file's tenant");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
25
30
|
// fileName of the row is never used: it is personal data and may be a PII ciphertext.
|
|
26
31
|
export async function readFileRefContent(args) {
|
|
27
32
|
if (!isUuid(args.fileRefId))
|
|
@@ -44,7 +49,7 @@ export async function readFileRefContent(args) {
|
|
|
44
49
|
}
|
|
45
50
|
const files = args.filesForTenant(row.tenantId);
|
|
46
51
|
if (files === undefined) {
|
|
47
|
-
throw new
|
|
52
|
+
throw new FileStorageUnavailableError();
|
|
48
53
|
}
|
|
49
54
|
const content = await files.refFromFileRef(row).read();
|
|
50
55
|
return { content, contentType: row.mimeType, tenantId: row.tenantId, size };
|
|
@@ -3,8 +3,6 @@ export type NamedS3EnvProviderOptions = {
|
|
|
3
3
|
readonly name: string;
|
|
4
4
|
/** Env prefix of the credential set, e.g. `RELEASES_S3_`. */
|
|
5
5
|
readonly envPrefix: string;
|
|
6
|
-
/** Tolerate missing credentials at boot; the provider then fails on its first file operation. */
|
|
7
|
-
readonly lazy?: boolean;
|
|
8
6
|
};
|
|
9
7
|
/** An additional S3 provider next to `"s3-env"`, reading its credentials from `<envPrefix>BUCKET` etc. */
|
|
10
8
|
export declare function createFileProviderS3NamedEnvFeature(options: NamedS3EnvProviderOptions): import("@cosmicdrift/kumiko-framework/env").FeatureWithEnvSchema<import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { toPostgresArrayLiteral, unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
3
|
import { readStoredDraft } from "../../decrypt-draft.js";
|
|
4
4
|
// COALESCE(modified_at, inserted_at) — a draft's "last touched" timestamp is
|
|
@@ -17,7 +17,7 @@ export async function selectStaleDraftsBatch(db, olderThanDays, batchSize, exclu
|
|
|
17
17
|
WHERE COALESCE("modified_at", "inserted_at") < now() - ($1::int * interval '1 day')
|
|
18
18
|
AND NOT ("id"::text = ANY($3::text[]))
|
|
19
19
|
ORDER BY COALESCE("modified_at", "inserted_at") ASC
|
|
20
|
-
LIMIT $2`, [olderThanDays, batchSize, [...excludeIds]]);
|
|
20
|
+
LIMIT $2`, [olderThanDays, batchSize, toPostgresArrayLiteral([...excludeIds])]);
|
|
21
21
|
return Promise.all(rows.map(async (row) => {
|
|
22
22
|
const base = {
|
|
23
23
|
id: row.id,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
2
2
|
import { configuredPiiSubjectKms, decryptPiiFieldValues, isPiiCiphertext, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
4
|
import { formDraftBlobSchema } from "./schemas.js";
|
|
4
5
|
// Raw reads (fetchOne/selectMany/SQL) return the stored column, which is
|
|
5
6
|
// ciphertext once the owner's key exists. The field name is part of the AAD,
|
|
@@ -8,7 +9,10 @@ import { formDraftBlobSchema } from "./schemas.js";
|
|
|
8
9
|
export async function readStoredDraft(stored) {
|
|
9
10
|
let plain = stored;
|
|
10
11
|
const kms = configuredPiiSubjectKms();
|
|
11
|
-
if (isPiiCiphertext(stored)
|
|
12
|
+
if (isPiiCiphertext(stored)) {
|
|
13
|
+
// A silent null would make the stale-draft cleanup treat the draft as empty and leak its file refs.
|
|
14
|
+
if (!kms)
|
|
15
|
+
throw new InternalError({ message: "form-draft: pii kms not configured" });
|
|
12
16
|
const out = await decryptPiiFieldValues({ draft: stored }, ["draft"], kms, {
|
|
13
17
|
requestId: requestContext.get()?.requestId ?? "form-draft:read",
|
|
14
18
|
});
|
|
@@ -6,38 +6,29 @@ import { classifyHost, splitFirstSegment, tenantKeyFromPath } from "./host.js";
|
|
|
6
6
|
import { matchRedirect, sameOriginPath } from "./redirects.js";
|
|
7
7
|
import { validateHostRoutingOptions } from "./validate-options.js";
|
|
8
8
|
const tenantKeyExistsResultSchema = z.object({ exists: z.boolean() });
|
|
9
|
-
async function
|
|
9
|
+
async function existsViaSystemQuery(systemQuery, queryType, payload) {
|
|
10
10
|
let result;
|
|
11
11
|
try {
|
|
12
|
-
result = await systemQuery(
|
|
12
|
+
result = await systemQuery(queryType, payload, SYSTEM_TENANT_ID);
|
|
13
13
|
}
|
|
14
14
|
catch (error) {
|
|
15
15
|
if (error instanceof ValidationError)
|
|
16
16
|
return false;
|
|
17
|
-
// Over the rate limit every key looks the same: serve the
|
|
18
|
-
//
|
|
17
|
+
// Over the rate limit every key looks the same: serve the shell, which
|
|
18
|
+
// leaks nothing. The API resolver is authoritative and answers
|
|
19
|
+
// tenant_required for unknown hosts, while a 404 would hit visitors
|
|
20
|
+
// behind a shared NAT under load.
|
|
19
21
|
if (error instanceof RateLimitError)
|
|
20
22
|
return true;
|
|
21
23
|
throw error;
|
|
22
24
|
}
|
|
23
25
|
return tenantKeyExistsResultSchema.parse(result).exists;
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
if (error instanceof ValidationError)
|
|
32
|
-
return false;
|
|
33
|
-
// The shell is harmless: the API resolver is authoritative and answers
|
|
34
|
-
// tenant_required for unknown hosts, while a 404 would hit visitors behind
|
|
35
|
-
// a shared NAT under load.
|
|
36
|
-
if (error instanceof RateLimitError)
|
|
37
|
-
return true;
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
return tenantKeyExistsResultSchema.parse(result).exists;
|
|
27
|
+
function isTenantKeyKnown(systemQuery, key) {
|
|
28
|
+
return existsViaSystemQuery(systemQuery, HostRoutingQueries.tenantKeyExists, { key });
|
|
29
|
+
}
|
|
30
|
+
function isHostKnown(systemQuery, host) {
|
|
31
|
+
return existsViaSystemQuery(systemQuery, HostRoutingQueries.hostTenantExists, { host });
|
|
41
32
|
}
|
|
42
33
|
function serve(surface, file = surface.file) {
|
|
43
34
|
return surface.csp === undefined
|
|
@@ -11,6 +11,9 @@ function validateRedirects(options) {
|
|
|
11
11
|
if (!REDIRECT_PATH_PATTERN.test(rule.from)) {
|
|
12
12
|
throw new Error(`host-routing: redirect from must be a path like "/old", got "${rule.from}"`);
|
|
13
13
|
}
|
|
14
|
+
if (rule.from === "/api" || rule.from.startsWith("/api/")) {
|
|
15
|
+
throw new Error(`host-routing: redirect from must not be an /api path, it would never fire, got "${rule.from}"`);
|
|
16
|
+
}
|
|
14
17
|
if (!REDIRECT_TARGET_PATTERN.test(rule.to)) {
|
|
15
18
|
throw new Error(`host-routing: redirect to must be a same-origin path or an https URL without query, got "${rule.to}"`);
|
|
16
19
|
}
|
|
@@ -58,6 +58,7 @@ import { mailAccountEditScreen, mailAccountListScreen } from "./screens.js";
|
|
|
58
58
|
import { INBOUND_MAIL_TENANT_DESTROY_ARCHIVE_REASON, inboundMessageTenantDestroyHook, mailAccountTenantDestroyHook, mailThreadTenantDestroyHook, } from "./tenant-destroy-hook.js";
|
|
59
59
|
export function createInboundMailFoundationFeature() {
|
|
60
60
|
return defineFeature(INBOUND_MAIL_FOUNDATION_FEATURE, (r) => {
|
|
61
|
+
r.adoptsBundledLists();
|
|
61
62
|
r.describe("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.");
|
|
62
63
|
r.uiHints({
|
|
63
64
|
displayLabel: "Inbound Mail · Foundation",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
export declare const updateAccountSchema: z.ZodObject<{
|
|
4
4
|
accountId: z.ZodUUID;
|
|
@@ -9,6 +9,6 @@ export declare const updateAccountSchema: z.ZodObject<{
|
|
|
9
9
|
}>>;
|
|
10
10
|
watchState: z.ZodOptional<z.ZodString>;
|
|
11
11
|
displayName: z.ZodOptional<z.ZodString>;
|
|
12
|
-
reason: z.ZodString
|
|
12
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
13
13
|
}, z.core.$strip>;
|
|
14
14
|
export declare const updateAccountHandler: WriteHandlerDef;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// error/backoff, OAuth refresh fails (auth_error), or a tenant admin re-enables
|
|
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
|
+
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
6
|
+
import { SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
7
|
import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
8
|
import * as z from "zod";
|
|
7
9
|
import { InboundMailAccountStatuses } from "../constants.js";
|
|
@@ -19,10 +21,16 @@ export const updateAccountSchema = z.object({
|
|
|
19
21
|
/** Ops-Signal ("idle", "watching", "backoff:3", ...). */
|
|
20
22
|
watchState: z.string().max(100).optional(),
|
|
21
23
|
displayName: z.string().max(200).optional(),
|
|
22
|
-
/** Audit reason for metadata.headers
|
|
23
|
-
* "
|
|
24
|
-
reason: z.string().min(1).max(100),
|
|
24
|
+
/** Audit reason for metadata.headers, honored for the system identity only
|
|
25
|
+
* ("watch_supervisor", "oauth_refresh"); user callers get a role-derived reason. */
|
|
26
|
+
reason: z.string().min(1).max(100).optional(),
|
|
25
27
|
});
|
|
28
|
+
const SYSTEM_FALLBACK_REASON = "system";
|
|
29
|
+
function auditReasonFor(user, requestedReason) {
|
|
30
|
+
if (user.id === SYSTEM_USER_ID)
|
|
31
|
+
return requestedReason ?? SYSTEM_FALLBACK_REASON;
|
|
32
|
+
return user.roles.includes(ROLES.SystemAdmin) ? "system_admin" : "tenant_admin";
|
|
33
|
+
}
|
|
26
34
|
export const updateAccountHandler = {
|
|
27
35
|
name: "update-account",
|
|
28
36
|
schema: updateAccountSchema,
|
|
@@ -52,7 +60,7 @@ export const updateAccountHandler = {
|
|
|
52
60
|
};
|
|
53
61
|
const headers = {
|
|
54
62
|
providerName: current.provider,
|
|
55
|
-
reason: payload.reason,
|
|
63
|
+
reason: auditReasonFor(event.user, payload.reason),
|
|
56
64
|
};
|
|
57
65
|
await ctx.unsafeAppendEvent({
|
|
58
66
|
aggregateId: payload.accountId,
|
|
@@ -63,8 +63,6 @@ export const mailAccountListScreen = {
|
|
|
63
63
|
access: MAIL_ACCOUNT_ADMIN_ACCESS,
|
|
64
64
|
dormant: true,
|
|
65
65
|
};
|
|
66
|
-
// The reason is the audit label update-account requires; it is not in the
|
|
67
|
-
// layout, so the default is what gets written.
|
|
68
66
|
export const mailAccountEditScreen = {
|
|
69
67
|
id: MAIL_ACCOUNT_EDIT_SCREEN_ID,
|
|
70
68
|
type: "actionForm",
|
|
@@ -73,7 +71,6 @@ export const mailAccountEditScreen = {
|
|
|
73
71
|
fields: {
|
|
74
72
|
accountId: { type: "text", required: true },
|
|
75
73
|
displayName: { type: "text", required: true, maxLength: 200 },
|
|
76
|
-
reason: { type: "text", default: "tenant_admin" },
|
|
77
74
|
},
|
|
78
75
|
layout: {
|
|
79
76
|
sections: [{ fields: [{ field: "accountId", readOnly: true }, "displayName"] }],
|
|
@@ -8,8 +8,9 @@ import { INBOUND_MAIL_PROVIDER_EXTENSION, type InboundMailAccountStatus } from "
|
|
|
8
8
|
* config — tenant config reads (host/port/... of the plugins)
|
|
9
9
|
* registry — extension lookup in the factory (not plugin-internal)
|
|
10
10
|
* secrets — tenant secret reads (IMAP password, OAuth refresh token)
|
|
11
|
-
* auditUserId — audit identity for secret reads. Handler path:
|
|
12
|
-
*
|
|
11
|
+
* auditUserId — audit identity for secret reads. Handler path: unset,
|
|
12
|
+
* requireSecretsContext falls back to ctx.user.id; worker/provider
|
|
13
|
+
* path: system identity set by the resolver.
|
|
13
14
|
* metricsFor — metrics handle of a feature by name, same behavior as
|
|
14
15
|
* HandlerContext.metricsFor: unregistered metrics are a silent
|
|
15
16
|
* no-op instead of a throw.
|
|
@@ -146,7 +147,7 @@ export type InboundMailProviderPlugin = {
|
|
|
146
147
|
* for password accounts and providers without an oauth flow. */
|
|
147
148
|
readonly accessToken?: string;
|
|
148
149
|
/** Batch lookup of the provider message ids not yet ingested for this
|
|
149
|
-
* account (committed seen anchors,
|
|
150
|
+
* account (committed seen anchors; lists of any size are fine, queried in chunks). Call it after
|
|
150
151
|
* the listing/delta page and download MIME only for the returned ids.
|
|
151
152
|
* Ingest dedup stays the correctness anchor; this only saves downloads. */
|
|
152
153
|
readonly filterUnknownMessageIds?: (providerMessageIds: readonly string[]) => Promise<ReadonlySet<string>>;
|
|
@@ -66,6 +66,7 @@ const DEFAULT_WATCH_LEASE_TTL_SECONDS = 90;
|
|
|
66
66
|
/** One cursor per account (one mailbox inbox) for now; multi-folder support
|
|
67
67
|
* can add further scopes later without a schema change. */
|
|
68
68
|
const CURSOR_SCOPE = "default";
|
|
69
|
+
const SEEN_LOOKUP_CHUNK_SIZE = 1000;
|
|
69
70
|
export function createInboundMailSupervisor(deps) {
|
|
70
71
|
const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
71
72
|
const backfillWindowDays = deps.backfillWindowDays ?? DEFAULT_BACKFILL_WINDOW_DAYS;
|
|
@@ -112,11 +113,20 @@ export function createInboundMailSupervisor(deps) {
|
|
|
112
113
|
const piiKms = configuredPiiSubjectKms();
|
|
113
114
|
const records = [];
|
|
114
115
|
for (const row of rows) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
let decrypted;
|
|
117
|
+
try {
|
|
118
|
+
decrypted = piiKms
|
|
119
|
+
? await decryptPiiFieldValues(row, MAIL_ACCOUNT_PII_FIELDS, piiKms, {
|
|
120
|
+
requestId: "inbound-mail-foundation:supervisor:list-accounts",
|
|
121
|
+
})
|
|
122
|
+
: row;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Class name only: the message may carry ciphertext or key details.
|
|
126
|
+
const errorName = error instanceof Error ? error.name : "unknown error";
|
|
127
|
+
log(`inbound-mail: account ${row.id} skipped — address did not decrypt (${errorName})`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
120
130
|
const decryptedAddress = decrypted["address"];
|
|
121
131
|
// Falling back to row.address would hand the ciphertext to a provider.
|
|
122
132
|
// One undecryptable account must not stop polling of every other tenant's accounts.
|
|
@@ -258,12 +268,19 @@ export function createInboundMailSupervisor(deps) {
|
|
|
258
268
|
return async (providerMessageIds) => {
|
|
259
269
|
if (providerMessageIds.length === 0)
|
|
260
270
|
return new Set();
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
271
|
+
// One bind parameter per id: a full-mailbox listing would exceed Postgres' 65535 limit.
|
|
272
|
+
const seenIds = new Set();
|
|
273
|
+
for (let start = 0; start < providerMessageIds.length; start += SEEN_LOOKUP_CHUNK_SIZE) {
|
|
274
|
+
const seen = await selectMany(deps.db, seenMessageTable, {
|
|
275
|
+
tenantId: account.tenantId,
|
|
276
|
+
accountId: account.id,
|
|
277
|
+
providerMessageId: {
|
|
278
|
+
in: providerMessageIds.slice(start, start + SEEN_LOOKUP_CHUNK_SIZE),
|
|
279
|
+
},
|
|
280
|
+
}, { fields: ["providerMessageId"] });
|
|
281
|
+
for (const row of seen)
|
|
282
|
+
seenIds.add(row.providerMessageId);
|
|
283
|
+
}
|
|
267
284
|
return new Set(providerMessageIds.filter((id) => !seenIds.has(id)));
|
|
268
285
|
};
|
|
269
286
|
}
|
package/dist/jobs/feature.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
2
2
|
export type JobsFeatureOptions = {
|
|
3
3
|
readonly retentionDays?: number;
|
|
4
4
|
readonly staleRunTimeoutHours?: number;
|
|
5
|
+
readonly readRoles?: readonly string[];
|
|
5
6
|
};
|
|
6
7
|
export declare function createJobsFeature(options?: JobsFeatureOptions): FeatureDefinition;
|