@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
package/dist/jobs/feature.js
CHANGED
|
@@ -9,8 +9,8 @@ import { reindexEntityJob, reindexEntityPayloadSchema } from "./handlers/reindex
|
|
|
9
9
|
import { createRetentionCleanupJob, DEFAULT_JOB_RUN_RETENTION_DAYS, } from "./handlers/retention-cleanup.job.js";
|
|
10
10
|
import { retryWrite } from "./handlers/retry.write.js";
|
|
11
11
|
import { createStaleRunSweepJob, DEFAULT_JOB_RUN_STALE_TIMEOUT_HOURS, } from "./handlers/stale-run-sweep.job.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { createTenantFailuresQuery } from "./handlers/tenant-failures.query.js";
|
|
13
|
+
import { createTenantRunsQuery } from "./handlers/tenant-runs.query.js";
|
|
14
14
|
import { triggerWrite } from "./handlers/trigger.write.js";
|
|
15
15
|
import { jobRunLogsTableMeta, jobRunsTableMeta } from "./job-run-table.js";
|
|
16
16
|
import { eventConsumerDetailScreen, eventConsumersScreen, jobRunDetailScreen, jobRunsScreen, jobTriggerScreen, } from "./screens.js";
|
|
@@ -20,7 +20,12 @@ import { JOBS_I18N } from "./web/i18n.js";
|
|
|
20
20
|
export function createJobsFeature(options = {}) {
|
|
21
21
|
const retentionDays = options.retentionDays ?? DEFAULT_JOB_RUN_RETENTION_DAYS;
|
|
22
22
|
const staleRunTimeoutHours = options.staleRunTimeoutHours ?? DEFAULT_JOB_RUN_STALE_TIMEOUT_HOURS;
|
|
23
|
+
const readRoles = options.readRoles ?? [];
|
|
24
|
+
if (readRoles.includes("anonymous")) {
|
|
25
|
+
throw new Error(`createJobsFeature: readRoles must not contain "anonymous" — job status and failures are only readable by members of the tenant.`);
|
|
26
|
+
}
|
|
23
27
|
return defineFeature("jobs", (r) => {
|
|
28
|
+
r.adoptsBundledLists();
|
|
24
29
|
r.describe("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`.");
|
|
25
30
|
r.uiHints({
|
|
26
31
|
displayLabel: "Jobs · Audit & Operator UI",
|
|
@@ -79,8 +84,8 @@ export function createJobsFeature(options = {}) {
|
|
|
79
84
|
list: r.queryHandler(listQuery),
|
|
80
85
|
detail: r.queryHandler(detailQuery),
|
|
81
86
|
catalog: r.queryHandler(catalogQuery),
|
|
82
|
-
failures: r.queryHandler(
|
|
83
|
-
tenantRuns: r.queryHandler(
|
|
87
|
+
failures: r.queryHandler(createTenantFailuresQuery(readRoles)),
|
|
88
|
+
tenantRuns: r.queryHandler(createTenantRunsQuery(readRoles)),
|
|
84
89
|
consumers: r.queryHandler(consumersQuery),
|
|
85
90
|
consumerDetails: r.queryHandler(consumerDetailQuery),
|
|
86
91
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
export declare
|
|
2
|
+
export declare function createTenantFailuresQuery(readRoles: readonly string[]): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"failures", z.ZodObject<{
|
|
3
3
|
jobName: z.ZodOptional<z.ZodString>;
|
|
4
4
|
subject: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
|
|
5
5
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
@@ -20,45 +20,49 @@ function parseSubject(stored) {
|
|
|
20
20
|
return null;
|
|
21
21
|
return Object.fromEntries(parsed.filter(isSubjectEntry));
|
|
22
22
|
}
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
export function createTenantFailuresQuery(readRoles) {
|
|
24
|
+
return defineQueryHandler({
|
|
25
|
+
name: "failures",
|
|
26
|
+
description: "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.",
|
|
27
|
+
schema: z.object({
|
|
28
|
+
jobName: z.string().optional(),
|
|
29
|
+
subject: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
30
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
31
|
+
}),
|
|
32
|
+
// Every membership rank: a failure record carries a job name and a
|
|
33
|
+
// translation key, nothing a team member of the tenant may not see.
|
|
34
|
+
access: {
|
|
35
|
+
roles: [...new Set([...access.roles("User", "Editor", ...access.admin), ...readRoles])],
|
|
36
|
+
},
|
|
37
|
+
handler: async (query, ctx) => {
|
|
38
|
+
if (!ctx.systemDb) {
|
|
39
|
+
throw new InternalError({
|
|
40
|
+
message: "jobs:query:failures requires ctx.systemDb (feature must declare r.systemScope())",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// assertTenantMatch is a self-check on the caller and returns the same
|
|
44
|
+
// unfiltered system-mode db (tenant-db.ts) — the explicit tenantId below
|
|
45
|
+
// is the filter, assertRowsTenant the second net. There is no
|
|
46
|
+
// cross-tenant mode here: SystemAdmin reads runs via jobs:query:list.
|
|
47
|
+
const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
|
|
48
|
+
const where = { tenantId: [query.user.tenantId] };
|
|
49
|
+
if (query.payload.jobName)
|
|
50
|
+
where["jobName"] = query.payload.jobName;
|
|
51
|
+
if (query.payload.subject)
|
|
52
|
+
where["subject"] = serializeJobSubject(query.payload.subject);
|
|
53
|
+
const rows = await selectMany(db, tenantJobFailuresTable, where, {
|
|
54
|
+
orderBy: { col: "failedAt", direction: "desc" },
|
|
55
|
+
limit: query.payload.limit ?? DEFAULT_LIMIT,
|
|
38
56
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
orderBy: { col: "failedAt", direction: "desc" },
|
|
52
|
-
limit: query.payload.limit ?? DEFAULT_LIMIT,
|
|
53
|
-
});
|
|
54
|
-
return {
|
|
55
|
-
rows: ctx.systemDb.assertRowsTenant(rows, "tenantId").map((row) => ({
|
|
56
|
-
jobName: row.jobName,
|
|
57
|
-
subject: parseSubject(row.subject),
|
|
58
|
-
messageKey: row.messageKey,
|
|
59
|
-
failedAt: row.failedAt,
|
|
60
|
-
})),
|
|
61
|
-
nextCursor: null,
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
});
|
|
57
|
+
return {
|
|
58
|
+
rows: ctx.systemDb.assertRowsTenant(rows, "tenantId").map((row) => ({
|
|
59
|
+
jobName: row.jobName,
|
|
60
|
+
subject: parseSubject(row.subject),
|
|
61
|
+
messageKey: row.messageKey,
|
|
62
|
+
failedAt: row.failedAt,
|
|
63
|
+
})),
|
|
64
|
+
nextCursor: null,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { type TenantJobRunStatus } from "../tenant-job-run-table.ts";
|
|
3
|
-
export declare
|
|
3
|
+
export declare function createTenantRunsQuery(readRoles: readonly string[]): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"tenant-runs", z.ZodObject<{
|
|
4
4
|
jobName: z.ZodOptional<z.ZodString>;
|
|
5
5
|
subject: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
|
|
6
6
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
@@ -19,54 +19,58 @@ function parseSubject(stored) {
|
|
|
19
19
|
return null;
|
|
20
20
|
return Object.fromEntries(parsed.filter(isSubjectEntry));
|
|
21
21
|
}
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
export function createTenantRunsQuery(readRoles) {
|
|
23
|
+
return defineQueryHandler({
|
|
24
|
+
name: "tenant-runs",
|
|
25
|
+
description: "Lists the calling tenant's own job runs that opted in via tenantVisibleRun — per job and subject every queued or running run plus the latest completed or failed one, active runs first, then newest first; each row carries only status and queuedAt/startedAt/finishedAt, never payloads, error text or logs; the optional subject filter matches exactly: all declared subjectFields with the same JSON types as the job payload; use it to show whether work for a subject is waiting, running or done, and to tell a failed run from a finished one.",
|
|
26
|
+
schema: z.object({
|
|
27
|
+
jobName: z.string().optional(),
|
|
28
|
+
subject: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
29
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
30
|
+
}),
|
|
31
|
+
// Every membership rank: a run state carries a job name, a status and
|
|
32
|
+
// times, nothing a team member of the tenant may not see.
|
|
33
|
+
access: {
|
|
34
|
+
roles: [...new Set([...access.roles("User", "Editor", ...access.admin), ...readRoles])],
|
|
35
|
+
},
|
|
36
|
+
handler: async (query, ctx) => {
|
|
37
|
+
if (!ctx.systemDb) {
|
|
38
|
+
throw new InternalError({
|
|
39
|
+
message: "jobs:query:tenant-runs requires ctx.systemDb (feature must declare r.systemScope())",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// assertTenantMatch returns the same unfiltered system-mode db — the
|
|
43
|
+
// explicit tenantId below is the filter, assertRowsTenant the second net.
|
|
44
|
+
const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
|
|
45
|
+
const where = { tenantId: [query.user.tenantId] };
|
|
46
|
+
if (query.payload.jobName)
|
|
47
|
+
where["jobName"] = query.payload.jobName;
|
|
48
|
+
if (query.payload.subject)
|
|
49
|
+
where["subject"] = serializeJobSubject(query.payload.subject);
|
|
50
|
+
const limit = query.payload.limit ?? DEFAULT_LIMIT;
|
|
51
|
+
const newestFirst = { col: "updatedAt", direction: "desc" };
|
|
52
|
+
const activeRows = await selectMany(db, tenantJobRunsTable, { ...where, status: { in: ["queued", "running"] } }, { orderBy: newestFirst, limit });
|
|
53
|
+
const finishedRows = await selectMany(db, tenantJobRunsTable, { ...where, status: { in: ["completed", "failed"] } }, { orderBy: newestFirst, limit });
|
|
54
|
+
const seenFinishedKeys = new Set();
|
|
55
|
+
const finishedNewestPerKey = finishedRows.filter((row) => {
|
|
56
|
+
const key = JSON.stringify([row.jobName, row.subject]);
|
|
57
|
+
if (seenFinishedKeys.has(key))
|
|
58
|
+
return false;
|
|
59
|
+
seenFinishedKeys.add(key);
|
|
60
|
+
return true;
|
|
37
61
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const key = JSON.stringify([row.jobName, row.subject]);
|
|
54
|
-
if (seenFinishedKeys.has(key))
|
|
55
|
-
return false;
|
|
56
|
-
seenFinishedKeys.add(key);
|
|
57
|
-
return true;
|
|
58
|
-
});
|
|
59
|
-
const activeFirst = [...activeRows, ...finishedNewestPerKey];
|
|
60
|
-
return {
|
|
61
|
-
rows: ctx.systemDb.assertRowsTenant(activeFirst.slice(0, limit), "tenantId").map((row) => ({
|
|
62
|
-
jobName: row.jobName,
|
|
63
|
-
subject: parseSubject(row.subject),
|
|
64
|
-
status: row.status,
|
|
65
|
-
queuedAt: row.queuedAt,
|
|
66
|
-
startedAt: row.startedAt,
|
|
67
|
-
finishedAt: row.finishedAt,
|
|
68
|
-
})),
|
|
69
|
-
nextCursor: null,
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
});
|
|
62
|
+
const activeFirst = [...activeRows, ...finishedNewestPerKey];
|
|
63
|
+
return {
|
|
64
|
+
rows: ctx.systemDb.assertRowsTenant(activeFirst.slice(0, limit), "tenantId").map((row) => ({
|
|
65
|
+
jobName: row.jobName,
|
|
66
|
+
subject: parseSubject(row.subject),
|
|
67
|
+
status: row.status,
|
|
68
|
+
queuedAt: row.queuedAt,
|
|
69
|
+
startedAt: row.startedAt,
|
|
70
|
+
finishedAt: row.finishedAt,
|
|
71
|
+
})),
|
|
72
|
+
nextCursor: null,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -157,15 +157,16 @@ async function recordTenantRunQueued(db, jobName, bullJobId, meta) {
|
|
|
157
157
|
const now = Temporal.Now.instant();
|
|
158
158
|
await transaction(db, async (tx) => {
|
|
159
159
|
// A finished row under the same id is left over from before a Redis reset
|
|
160
|
-
// (ids restart at 1), not this job. Only rows
|
|
161
|
-
//
|
|
162
|
-
// and
|
|
160
|
+
// (ids restart at 1), not this job. Only rows queued before this BullMQ job
|
|
161
|
+
// was created qualify: a fast worker can finish before the hook runs (no
|
|
162
|
+
// queuedAt), and a deduplicated add of an already finished job re-announces
|
|
163
|
+
// it with a creation time older than its own row. Both rows must stay.
|
|
163
164
|
await deleteMany(tx, tenantJobRunsTable, {
|
|
164
165
|
tenantId: key.tenantId,
|
|
165
166
|
jobName,
|
|
166
167
|
bullJobId,
|
|
167
168
|
status: { in: ["completed", "failed"] },
|
|
168
|
-
queuedAt: {
|
|
169
|
+
queuedAt: { lt: Temporal.Instant.fromEpochMilliseconds(meta.jobCreatedAtMs) },
|
|
169
170
|
});
|
|
170
171
|
// DO NOTHING: a fast worker can report the start before this enqueue hook
|
|
171
172
|
// returns, and a repeated jobId (debounce, fan-out dedup) is the same job.
|
package/dist/ledger/web/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Client-safe ledger surface: QN constants, pure recurring helpers
|
|
2
2
|
// (projection + Soll/Ist merge), and Zod payload schemas for client-side
|
|
3
3
|
// pre-validation before dispatch. The full `../ledger` entry also re-exports
|
|
4
|
-
// the feature/handlers/executor, which pull
|
|
4
|
+
// the feature/handlers/executor, which pull the DB driver; importing it in a
|
|
5
5
|
// browser bundle fails on Node builtins. Client screens import dispatch QNs and
|
|
6
6
|
// pure forecast/merge helpers from this module, then dispatch via the renderer's
|
|
7
7
|
// useDispatcher.
|
|
@@ -9,4 +9,6 @@ export type LegalContentSeedSource = {
|
|
|
9
9
|
export type LegalContentSeedFn = (deps: {
|
|
10
10
|
readonly db: DbConnection;
|
|
11
11
|
}) => Promise<void>;
|
|
12
|
-
export declare function legalContentSeed(source: LegalContentSeedSource
|
|
12
|
+
export declare function legalContentSeed(source: LegalContentSeedSource, options?: {
|
|
13
|
+
readonly folder?: string;
|
|
14
|
+
}): LegalContentSeedFn;
|
package/dist/legal-pages/seed.js
CHANGED
|
@@ -28,11 +28,11 @@ async function loadBlocksFromJson(path) {
|
|
|
28
28
|
}
|
|
29
29
|
// Boot seed for runProdApp's `seeds: [...]`. The JSON template is authoritative
|
|
30
30
|
// (ifExists "update"), so legally required text changes reach rows seeded once.
|
|
31
|
-
export function legalContentSeed(source) {
|
|
31
|
+
export function legalContentSeed(source, options) {
|
|
32
32
|
return async ({ db }) => {
|
|
33
33
|
const blocks = "blocks" in source
|
|
34
34
|
? legalContentBlocksSchema.parse(source.blocks)
|
|
35
35
|
: await loadBlocksFromJson(source.path);
|
|
36
|
-
await seedLegalContentFromJson(db, blocks);
|
|
36
|
+
await seedLegalContentFromJson(db, blocks, options);
|
|
37
37
|
};
|
|
38
38
|
}
|
|
@@ -20,9 +20,9 @@ export declare const MAIL_TRANSPORT_EXTENSION: "mailTransport";
|
|
|
20
20
|
* config — tenant config reads (the plugin's host/port/from/...)
|
|
21
21
|
* registry — extension lookup in the factory (not plugin-internal)
|
|
22
22
|
* secrets — tenant secret reads (smtp.password)
|
|
23
|
-
* auditUserId — audit identity for secret reads. Handler path:
|
|
24
|
-
*
|
|
25
|
-
* r.job wrap sets a system identity.
|
|
23
|
+
* auditUserId — audit identity for secret reads. Handler path: unset,
|
|
24
|
+
* requireSecretsContext falls back to ctx.user.id; worker path:
|
|
25
|
+
* the r.job wrap sets a system identity.
|
|
26
26
|
*/
|
|
27
27
|
export type MailTransportContext = {
|
|
28
28
|
readonly config?: ConfigAccessor;
|
|
@@ -147,6 +147,9 @@ export async function createTransportForTenantIfConfigured(ctx, tenantId, handle
|
|
|
147
147
|
/** The `tenantTransport` resolver for `createChannelEmailFeature`: the tenant's configured
|
|
148
148
|
* provider, or null so the channel falls back to its app transport. */
|
|
149
149
|
export function tenantMailTransport(scope) {
|
|
150
|
+
// Without a tenant config the provider cannot be read: use the app transport.
|
|
151
|
+
if (scope.config === undefined)
|
|
152
|
+
return Promise.resolve(null);
|
|
150
153
|
return createTransportForTenantIfConfigured({
|
|
151
154
|
config: scope.config,
|
|
152
155
|
registry: scope.registry,
|
|
@@ -4,6 +4,7 @@ import { access, createTenantConfig } from "@cosmicdrift/kumiko-framework/engine
|
|
|
4
4
|
const tenantAdminWrite = access.withSystem(access.roles("TenantAdmin"));
|
|
5
5
|
const tenantAdminRead = access.roles("TenantAdmin", "SystemAdmin");
|
|
6
6
|
// required: true ↔ the requireNonEmpty calls in buildSmtpTransport — keep in sync.
|
|
7
|
+
// authUser stays optional: empty means an unauthenticated relay.
|
|
7
8
|
export const SMTP_CONFIG_KEYS = {
|
|
8
9
|
host: createTenantConfig("text", {
|
|
9
10
|
required: true,
|
|
@@ -39,7 +40,6 @@ export const SMTP_CONFIG_KEYS = {
|
|
|
39
40
|
},
|
|
40
41
|
}),
|
|
41
42
|
authUser: createTenantConfig("text", {
|
|
42
|
-
required: true,
|
|
43
43
|
default: "",
|
|
44
44
|
write: tenantAdminWrite,
|
|
45
45
|
read: tenantAdminRead,
|
|
@@ -76,8 +76,7 @@ export function createMailTransportSmtpFeature() {
|
|
|
76
76
|
return `${plaintext.slice(0, 3)}...${plaintext.slice(-2)}`;
|
|
77
77
|
},
|
|
78
78
|
scope: "tenant",
|
|
79
|
-
//
|
|
80
|
-
required: true,
|
|
79
|
+
// Only needed once authUser is set (readPassword throws then), so readiness must not demand it.
|
|
81
80
|
});
|
|
82
81
|
const configKeys = r.config({ keys: SMTP_CONFIG_KEYS });
|
|
83
82
|
r.translations({ keys: MAIL_TRANSPORT_SMTP_I18N });
|
|
@@ -126,8 +125,10 @@ async function buildSmtpTransport(ctx, tenantId) {
|
|
|
126
125
|
const port = requireDefined(await ctxConfig(sharedMailTransportSmtpFeature.exports.configKeys.port), FEATURE_NAME, "port"); // @cast-boundary engine-payload
|
|
127
126
|
const secure = requireDefined(await ctxConfig(sharedMailTransportSmtpFeature.exports.configKeys.secure), FEATURE_NAME, "secure"); // @cast-boundary engine-payload
|
|
128
127
|
const from = requireNonEmpty(await ctxConfig(sharedMailTransportSmtpFeature.exports.configKeys.from), FEATURE_NAME, "from", SMTP_HINT);
|
|
129
|
-
const authUser =
|
|
130
|
-
const
|
|
128
|
+
const authUser = (await ctxConfig(sharedMailTransportSmtpFeature.exports.configKeys.authUser))?.trim();
|
|
129
|
+
const auth = authUser === undefined || authUser === ""
|
|
130
|
+
? undefined
|
|
131
|
+
: { user: authUser, pass: await readPassword(ctx, tenantId) };
|
|
131
132
|
let target;
|
|
132
133
|
try {
|
|
133
134
|
target = await resolveMailConnectTarget(host, {
|
|
@@ -155,7 +156,7 @@ async function buildSmtpTransport(ctx, tenantId) {
|
|
|
155
156
|
port,
|
|
156
157
|
secure,
|
|
157
158
|
from,
|
|
158
|
-
auth
|
|
159
|
+
...(auth && { auth }),
|
|
159
160
|
...(target.servername && { servername: target.servername }),
|
|
160
161
|
});
|
|
161
162
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MANAGED_PAGES_FEATURE: "managed-pages";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MANAGED_PAGES_FEATURE = "managed-pages";
|
|
@@ -4,6 +4,7 @@ import { RateLimitError } from "@cosmicdrift/kumiko-framework/errors";
|
|
|
4
4
|
import { cachedSecurePageResponse, EMPTY_BRANDING, renderSafeMarkdown, wrapInLayout, } from "../page-render/index.js";
|
|
5
5
|
import { rateLimitedTextResponse } from "../shared/index.js";
|
|
6
6
|
import { BRANDING_KEYS, BRANDING_QUERY_QN, CUSTOM_CSS_KEY, coerceBranding } from "./branding.js";
|
|
7
|
+
import { MANAGED_PAGES_FEATURE } from "./constants.js";
|
|
7
8
|
import { createBrandingQuery } from "./handlers/branding.query.js";
|
|
8
9
|
import { bySlugQuery } from "./handlers/by-slug.query.js";
|
|
9
10
|
import { byTenantPublishedQuery } from "./handlers/by-tenant-published.query.js";
|
|
@@ -79,7 +80,8 @@ export function createManagedPagesFeature(opts) {
|
|
|
79
80
|
const basePath = opts.basePath ?? "/p";
|
|
80
81
|
const defaultLang = opts.defaultLang ?? "en";
|
|
81
82
|
const allowCustomCss = opts.allowCustomCss ?? false;
|
|
82
|
-
return defineFeature(
|
|
83
|
+
return defineFeature(MANAGED_PAGES_FEATURE, (r) => {
|
|
84
|
+
r.adoptsBundledLists();
|
|
83
85
|
r.describe("Tenant-editable, server-rendered public pages with per-tenant branding. Stores one Markdown `page` per `(tenantId, slug, lang)` in the `read_pages` entity table with a `published` gate plus `description`/`ogImage` SEO meta. Registers an anonymous `GET {basePath}/:slug` route that resolves the tenant from the request Host via the app-supplied `resolveApexTenant`, serves only published pages (drafts → 404), renders Markdown through the hardened `page-render` core, and isolates per-tenant content with `Vary: Host`. Ships TenantAdmin/SystemAdmin admin screens (`entityList` + `entityEdit`) backed by convention CRUD handlers (`managed-pages:write:page:{create,update,delete}`, `managed-pages:query:page:{list,detail}`); the app wires nav/workspace onto `managed-pages:screen:page-list`. Also exposes `managed-pages:query:by-tenant-published` (anonymous, SQL-filtered on `published`) for sitemap/discovery consumers such as the `seo` feature. Branding (via `config`, scope tenant): `branding-{title,description,site-url,accent-color,logo-url,layout-preset,custom-css}` keys with write-time validation (hex color, https URLs), a `configEdit` self-service screen (`managed-pages:screen:branding-settings`), and a `managed-pages:query:branding` read that the render path applies as scoped `:root` CSS vars + a logo/title header. Also exposes `managed-pages:write:set` (idempotent slug-keyed upsert, SystemAdmin cross-tenant via `tenantIdOverride`) as a provisioning API. Requires `config` + `anonymousAccess` wired at app bootstrap.");
|
|
84
86
|
r.uiHints({
|
|
85
87
|
displayLabel: "Managed Pages · Public CMS",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type BrandingTokens, brandingHeaderHtml, brandingStyleBlock, EMPTY_BRANDING, sanitizeTenantCss, TENANT_CONTENT_ATTR, tenantStyleBlock, } from "../page-render/index.ts";
|
|
2
2
|
export { BRANDING_QN, MANAGED_PAGES_CSS_FEATURE } from "./branding.ts";
|
|
3
|
+
export { MANAGED_PAGES_FEATURE } from "./constants.ts";
|
|
3
4
|
export { createManagedPagesCssFeature } from "./css-gate.ts";
|
|
4
5
|
export { createManagedPagesFeature, type ManagedPagesOptions, type ManagedPagesWrapLayout, } from "./feature.ts";
|
|
5
6
|
export { type PageRow, pageEntity, pagesTable } from "./table.ts";
|
|
@@ -13,6 +13,7 @@ export { brandingHeaderHtml, brandingStyleBlock, EMPTY_BRANDING, sanitizeTenantC
|
|
|
13
13
|
// (`config:write:set`) — single source for the `managed-pages:config:branding-*`
|
|
14
14
|
// strings, so apps + the per-tenant migration never hardcode them.
|
|
15
15
|
export { BRANDING_QN, MANAGED_PAGES_CSS_FEATURE } from "./branding.js";
|
|
16
|
+
export { MANAGED_PAGES_FEATURE } from "./constants.js";
|
|
16
17
|
export { createManagedPagesCssFeature } from "./css-gate.js";
|
|
17
18
|
export { createManagedPagesFeature, } from "./feature.js";
|
|
18
19
|
export { pageEntity, pagesTable } from "./table.js";
|
|
@@ -51,7 +51,7 @@ export function createAddNoteHandler(access = DEFAULT_NOTES_HISTORY_ACCESS, entr
|
|
|
51
51
|
let authorName = null;
|
|
52
52
|
try {
|
|
53
53
|
// read_users is tenant-agnostic → ctx.db.unsafeRaw, not the tenant-scoped ctx.db.
|
|
54
|
-
//
|
|
54
|
+
// Postgres aborts the whole tx after any error inside it, even one that's
|
|
55
55
|
// caught — so the lookup runs in a savepoint where available, otherwise directly
|
|
56
56
|
// (pool statements are their own units).
|
|
57
57
|
authorName = await runInSavepointIfSupported(ctx.db.unsafeRaw(), async (sp) => {
|
|
@@ -2,7 +2,7 @@ import { type FeatureDefinition, type ProjectionListScreenDefinition } from "@co
|
|
|
2
2
|
import { type CreatePatOptions } from "./handlers/create.write.ts";
|
|
3
3
|
import { type PatRateLimit } from "./web/constants.ts";
|
|
4
4
|
import type { PatScopeConfig } from "./web/scopes.ts";
|
|
5
|
-
export type BindPatAutoRevokeOnPasswordChange = (revoker: (userId: string) => Promise<number>) => void;
|
|
5
|
+
export type BindPatAutoRevokeOnPasswordChange = (revoker: (userId: string) => Promise<number>) => () => void;
|
|
6
6
|
export declare function bindPatAutoRevokeOnPasswordChangeFromFeature(feature: FeatureDefinition): BindPatAutoRevokeOnPasswordChange | undefined;
|
|
7
7
|
export type PersonalAccessTokensOptions = {
|
|
8
8
|
readonly scopes: PatScopeConfig;
|
|
@@ -41,6 +41,7 @@ export function bindPatAutoRevokeOnPasswordChangeFromFeature(feature) {
|
|
|
41
41
|
export function createPersonalAccessTokensFeature(options) {
|
|
42
42
|
const { scopes } = options;
|
|
43
43
|
return defineFeature(PAT_FEATURE, (r) => {
|
|
44
|
+
r.adoptsBundledLists();
|
|
44
45
|
r.describe("Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) — registered as an auth-foundation tokenVerifier provider, resolved generically by the middleware. Pass { toggleable: { default: false } } to tier-gate the whole feature.");
|
|
45
46
|
r.uiHints({ displayLabel: "Personal Access Tokens", category: "identity", recommended: false });
|
|
46
47
|
// Opt-in tier-gating (mirrors ledger/tags): when set, the feature declares
|
|
@@ -105,6 +106,10 @@ export function createPersonalAccessTokensFeature(options) {
|
|
|
105
106
|
});
|
|
106
107
|
const bindAutoRevokeOnPasswordChange = (revoker) => {
|
|
107
108
|
boundAutoRevoke = revoker;
|
|
109
|
+
return () => {
|
|
110
|
+
if (boundAutoRevoke === revoker)
|
|
111
|
+
boundAutoRevoke = undefined;
|
|
112
|
+
};
|
|
108
113
|
};
|
|
109
114
|
const handlers = {
|
|
110
115
|
create: r.writeHandler(createPatCreateHandler({ mfaVerifier: options.mfaVerifier })),
|
|
@@ -44,5 +44,5 @@ export type PdfTableResult = {
|
|
|
44
44
|
readonly pdfBytes: Uint8Array;
|
|
45
45
|
readonly pageCount: number;
|
|
46
46
|
};
|
|
47
|
-
export declare function renderTablePdf(
|
|
47
|
+
export declare function renderTablePdf(untrustedDoc: PdfTableDocument, options?: PdfTableOptions): Promise<PdfTableResult>;
|
|
48
48
|
export {};
|
|
@@ -5,19 +5,28 @@ import { RendererError } from "../renderer-foundation/index.js";
|
|
|
5
5
|
const PDF_TABLE_MAX_ROWS = 10_000;
|
|
6
6
|
const PDF_TABLE_MAX_COLUMNS = 30;
|
|
7
7
|
const PDF_TABLE_MAX_CELL_LENGTH = 5_000;
|
|
8
|
+
const PDF_TABLE_MAX_META_ENTRIES = 500;
|
|
9
|
+
const PDF_TABLE_MAX_TEXT_LENGTH = 20_000;
|
|
10
|
+
const PDF_TABLE_MAX_PAGE_LABEL_LENGTH = 100;
|
|
8
11
|
const pdfTableColumnSchema = z
|
|
9
12
|
.object({
|
|
10
|
-
header: z.string(),
|
|
13
|
+
header: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH),
|
|
11
14
|
weight: z.number().positive().optional(),
|
|
12
15
|
align: z.enum(["left", "right"]).optional(),
|
|
13
16
|
})
|
|
14
17
|
.readonly();
|
|
15
18
|
export const pdfTableDocumentSchema = z
|
|
16
19
|
.object({
|
|
17
|
-
title: z.string(),
|
|
18
|
-
subtitle: z.string().optional(),
|
|
20
|
+
title: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH),
|
|
21
|
+
subtitle: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH).optional(),
|
|
19
22
|
meta: z
|
|
20
|
-
.array(z
|
|
23
|
+
.array(z
|
|
24
|
+
.object({
|
|
25
|
+
label: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH),
|
|
26
|
+
value: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH),
|
|
27
|
+
})
|
|
28
|
+
.readonly())
|
|
29
|
+
.max(PDF_TABLE_MAX_META_ENTRIES)
|
|
21
30
|
.readonly()
|
|
22
31
|
.optional(),
|
|
23
32
|
columns: z.array(pdfTableColumnSchema).min(1).max(PDF_TABLE_MAX_COLUMNS).readonly(),
|
|
@@ -25,10 +34,10 @@ export const pdfTableDocumentSchema = z
|
|
|
25
34
|
.array(z.array(z.string().max(PDF_TABLE_MAX_CELL_LENGTH)).readonly())
|
|
26
35
|
.max(PDF_TABLE_MAX_ROWS)
|
|
27
36
|
.readonly(),
|
|
28
|
-
emptyText: z.string().optional(),
|
|
29
|
-
footerText: z.string().optional(),
|
|
37
|
+
emptyText: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH).optional(),
|
|
38
|
+
footerText: z.string().max(PDF_TABLE_MAX_TEXT_LENGTH).optional(),
|
|
30
39
|
// Template with `{page}` and `{pages}`; default `"{page} / {pages}"`.
|
|
31
|
-
pageLabel: z.string().optional(),
|
|
40
|
+
pageLabel: z.string().max(PDF_TABLE_MAX_PAGE_LABEL_LENGTH).optional(),
|
|
32
41
|
})
|
|
33
42
|
.readonly();
|
|
34
43
|
const marginMmSchema = z
|
|
@@ -174,10 +183,13 @@ function resolveTableLayout(doc, options, fonts, clean) {
|
|
|
174
183
|
headerHeight,
|
|
175
184
|
};
|
|
176
185
|
}
|
|
177
|
-
export async function renderTablePdf(
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
export async function renderTablePdf(untrustedDoc, options) {
|
|
187
|
+
// Direct callers bypass renderTablePdfRequest, so the size caps are enforced here too.
|
|
188
|
+
const parsed = pdfTableDocumentSchema.safeParse(untrustedDoc);
|
|
189
|
+
if (!parsed.success) {
|
|
190
|
+
throw new RendererError(`table document is invalid: ${parsed.error.message}`, "invalid_payload");
|
|
180
191
|
}
|
|
192
|
+
const doc = parsed.data;
|
|
181
193
|
assertRowShape(doc);
|
|
182
194
|
const pdf = await PDFDocument.create({ updateMetadata: false });
|
|
183
195
|
const fonts = {
|
|
@@ -6,6 +6,10 @@ function unknownKeyFailure(key) {
|
|
|
6
6
|
failure: writeFailure(new NotFoundError("secretKey", key, { i18nKey: SECRETS_ERROR_KEYS.unknownKey })),
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
|
+
function isUnderMountedFeaturePrefix(registry, key) {
|
|
10
|
+
const separator = key.indexOf(":");
|
|
11
|
+
return separator > 0 && registry.getFeature(key.slice(0, separator)) !== undefined;
|
|
12
|
+
}
|
|
9
13
|
function namespaceAcceptsKey(namespace, key) {
|
|
10
14
|
const suffix = key.slice(namespace.qualifiedPrefix.length);
|
|
11
15
|
if (suffix.length === 0)
|
|
@@ -16,13 +20,15 @@ function namespaceAcceptsKey(namespace, key) {
|
|
|
16
20
|
// are writable through the API, so a tenant admin cannot plant arbitrary rows
|
|
17
21
|
// next to the feature-owned ones. writeRoles narrows the handler access
|
|
18
22
|
// further for a key or a whole namespace. Delete passes allowUndeclared: it
|
|
19
|
-
// creates nothing, and legacy rows under since-undeclared keys must stay removable
|
|
23
|
+
// creates nothing, and legacy rows under since-undeclared keys must stay removable,
|
|
24
|
+
// unless the key sits under a mounted feature's prefix: those rows belong to the
|
|
25
|
+
// feature and must not be removable by a tenant admin outside its writeRoles.
|
|
20
26
|
export function checkSecretKeyWrite(registry, userRoles, key, options = {}) {
|
|
21
27
|
const keyDef = registry.getSecretKey(key);
|
|
22
28
|
const namespace = keyDef ? undefined : registry.findSecretNamespace(key);
|
|
23
29
|
if (!keyDef &&
|
|
24
30
|
!(namespace && namespaceAcceptsKey(namespace, key)) &&
|
|
25
|
-
options.allowUndeclared !== true) {
|
|
31
|
+
(options.allowUndeclared !== true || isUnderMountedFeaturePrefix(registry, key))) {
|
|
26
32
|
return unknownKeyFailure(key);
|
|
27
33
|
}
|
|
28
34
|
const writeRoles = keyDef?.writeRoles ?? namespace?.writeRoles;
|
|
@@ -5,7 +5,7 @@ export type SessionsFeatureOptions = {
|
|
|
5
5
|
readonly expiresInMs?: number;
|
|
6
6
|
readonly adminAccess?: "admin" | "systemAdmin";
|
|
7
7
|
};
|
|
8
|
-
export type BindAutoRevokeOnPasswordChange = (revoker: SessionMassRevoker) => void;
|
|
9
|
-
export type BindRevokeAllOtherSessions = (revoker: SessionAllOthersRevoker) => void;
|
|
8
|
+
export type BindAutoRevokeOnPasswordChange = (revoker: SessionMassRevoker) => () => void;
|
|
9
|
+
export type BindRevokeAllOtherSessions = (revoker: SessionAllOthersRevoker) => () => void;
|
|
10
10
|
export declare function bindAutoRevokeFromFeature(feature: FeatureDefinition): BindAutoRevokeOnPasswordChange | undefined;
|
|
11
11
|
export declare function createSessionsFeature(options?: SessionsFeatureOptions): FeatureDefinition;
|
package/dist/sessions/feature.js
CHANGED
|
@@ -48,6 +48,7 @@ export function bindAutoRevokeFromFeature(feature) {
|
|
|
48
48
|
export function createSessionsFeature(options) {
|
|
49
49
|
return defineFeature("sessions", (r) => {
|
|
50
50
|
const adminRoles = access[options?.adminAccess ?? "admin"];
|
|
51
|
+
r.adoptsBundledLists();
|
|
51
52
|
r.describe("Tracks signed-in clients in the `store_user_sessions` table (one row per JWT, keyed by the `sid`/`jti` claim) and exposes handlers for `mine` (list your sessions), `revoke`, and `revokeAllOthers`. Session creation and revocation on the hot auth path are handled by `createSessionCallbacks()`, wired into `buildServer({ auth: { ... } })` outside the dispatcher; the same callbacks are also registered as an auth-foundation sessionStore provider, resolvable generically via `resolveSessionStore()`. The feature also ships a manual-trigger cleanup job for pruning expired rows and an optional `autoRevokeOnPasswordChange` hook that mass-revokes all sessions for a user whenever their `passwordHash` changes.");
|
|
52
53
|
r.uiHints({
|
|
53
54
|
displayLabel: "Sessions · Server-side Logout",
|
|
@@ -170,6 +171,10 @@ export function createSessionsFeature(options) {
|
|
|
170
171
|
});
|
|
171
172
|
const bindAutoRevokeOnPasswordChange = (revoker) => {
|
|
172
173
|
boundAutoRevoke = revoker;
|
|
174
|
+
return () => {
|
|
175
|
+
if (boundAutoRevoke === revoker)
|
|
176
|
+
boundAutoRevoke = undefined;
|
|
177
|
+
};
|
|
173
178
|
};
|
|
174
179
|
r.translations({ keys: SESSIONS_I18N });
|
|
175
180
|
r.screen(createSessionListScreen(adminRoles));
|