@cosmicdrift/kumiko-samples 0.363.0 → 0.365.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/LICENSE +1 -1
- package/package.json +1 -1
- package/packages/bundled-features/package.json +7 -1
- package/packages/bundled-features/src/audit/changes.json +18 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
- package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +18 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
- package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +19 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
- package/packages/bundled-features/src/channel-email/changes.json +18 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +47 -24
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +54 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
- package/packages/bundled-features/src/delivery/changes.json +30 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/delivery/index.ts +2 -0
- package/packages/bundled-features/src/delivery/jobs.ts +51 -14
- package/packages/bundled-features/src/delivery/types.ts +7 -1
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/packages/bundled-features/src/jobs/changes.json +31 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +56 -18
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/metrics/changes.json +6 -0
- package/packages/bundled-features/src/metrics/filters.ts +28 -12
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
- package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +12 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +8 -0
- package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
- package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/template-resolver/README.md +4 -2
- package/packages/bundled-features/src/template-resolver/api.ts +3 -2
- package/packages/bundled-features/src/template-resolver/changes.json +149 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +121 -53
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +18 -2
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/table.ts +13 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
- package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
- package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
- package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
- package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
- package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
- package/packages/bundled-features/src/tenant/seeding.ts +33 -27
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
- package/packages/bundled-features/src/tier-engine/changes.json +18 -0
- package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
- package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
- package/packages/bundled-features/src/tier-engine/index.ts +2 -0
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
- package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
- package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
- package/packages/bundled-features/src/waitlist/lib.ts +20 -0
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +9 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
- package/samples/recipes/auth-mfa/README.md +2 -1
- package/samples/recipes/auth-mfa/src/feature.ts +1 -0
- package/samples/recipes/custom-handlers/README.md +4 -3
- package/samples/recipes/custom-handlers/src/feature.ts +21 -12
- package/samples/recipes/dashboard-stats/README.md +38 -0
- package/samples/recipes/dashboard-stats/package.json +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/pdf-audit-export/README.md +2 -1
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
- package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
- package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
- package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
- package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
runnerCanBeginTransaction,
|
|
3
|
+
transaction,
|
|
4
|
+
updateMany,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
6
|
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
7
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
8
|
import { append } from "@cosmicdrift/kumiko-framework/event-store";
|
|
5
9
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
6
10
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
7
|
-
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
8
11
|
import {
|
|
9
12
|
PAT_REVOKED_AGGREGATE_TYPE,
|
|
10
13
|
PAT_REVOKED_EVENT_QN,
|
|
@@ -12,11 +15,6 @@ import {
|
|
|
12
15
|
} from "./pat-revoked-event.js";
|
|
13
16
|
import { apiTokenTable } from "./schema/api-token.js";
|
|
14
17
|
|
|
15
|
-
// A pool has begin(); a tx/savepoint handle does not and is already atomic.
|
|
16
|
-
function canBeginTransaction(db: DbRunner): boolean {
|
|
17
|
-
return typeof propertyOf(db, "begin") === "function";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
18
|
// Cross-tenant revoke: password-change and MFA-enable/disable are account-
|
|
21
19
|
// level security events, not scoped to one tenant. Mirrors sessions'
|
|
22
20
|
// sessionMassRevoker (session-callbacks.ts) which passes the boot-time
|
|
@@ -24,7 +22,8 @@ function canBeginTransaction(db: DbRunner): boolean {
|
|
|
24
22
|
export async function revokeAllPatTokensForUser(db: DbRunner, userId: string): Promise<number> {
|
|
25
23
|
// Update and event commit together: otherwise a failed append leaves the
|
|
26
24
|
// tokens revoked with no event, and a retry matches no live row.
|
|
27
|
-
|
|
25
|
+
// A pool has begin(); a tx/savepoint handle does not and is already atomic.
|
|
26
|
+
return runnerCanBeginTransaction(db)
|
|
28
27
|
? transaction(db, (tx) => revokeAndAppend(tx, userId))
|
|
29
28
|
: revokeAndAppend(db, userId);
|
|
30
29
|
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "renderTablePdf rejects tables above 10000 rows, 30 columns or 5000 characters per cell",
|
|
6
|
+
"detail": "The table document schema now caps rows at 10000, columns at 30 and each cell string at 5000 characters. Larger input fails with invalid_payload instead of exhausting memory during layout."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.364.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "render-table-pdf validates input and paginates its intro",
|
|
12
|
+
"detail": "Table documents reject negative margins and unusable column widths, normalize CRLF and paginate the intro text and the first header row."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.360.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -1,52 +1,56 @@
|
|
|
1
1
|
import { PDFDocument, type PDFFont, type PDFPage, rgb, StandardFonts } from "pdf-lib";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { RendererError } from "../renderer-foundation/index.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
readonly
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
4
|
+
import type { PdfPageFormat } from "../template-resolver/index.js";
|
|
5
|
+
|
|
6
|
+
// Upper bounds keep a hostile or runaway payload from exhausting memory and CPU in layout.
|
|
7
|
+
const PDF_TABLE_MAX_ROWS = 10_000;
|
|
8
|
+
const PDF_TABLE_MAX_COLUMNS = 30;
|
|
9
|
+
const PDF_TABLE_MAX_CELL_LENGTH = 5_000;
|
|
10
|
+
|
|
11
|
+
const pdfTableColumnSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
header: z.string(),
|
|
14
|
+
weight: z.number().positive().optional(),
|
|
15
|
+
align: z.enum(["left", "right"]).optional(),
|
|
16
|
+
})
|
|
17
|
+
.readonly();
|
|
18
|
+
|
|
19
|
+
export const pdfTableDocumentSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
title: z.string(),
|
|
22
|
+
subtitle: z.string().optional(),
|
|
23
|
+
meta: z
|
|
24
|
+
.array(z.object({ label: z.string(), value: z.string() }).readonly())
|
|
25
|
+
.readonly()
|
|
26
|
+
.optional(),
|
|
27
|
+
columns: z.array(pdfTableColumnSchema).min(1).max(PDF_TABLE_MAX_COLUMNS).readonly(),
|
|
28
|
+
rows: z
|
|
29
|
+
.array(z.array(z.string().max(PDF_TABLE_MAX_CELL_LENGTH)).readonly())
|
|
30
|
+
.max(PDF_TABLE_MAX_ROWS)
|
|
31
|
+
.readonly(),
|
|
32
|
+
emptyText: z.string().optional(),
|
|
33
|
+
footerText: z.string().optional(),
|
|
34
|
+
// Template with `{page}` and `{pages}`; default `"{page} / {pages}"`.
|
|
35
|
+
pageLabel: z.string().optional(),
|
|
36
|
+
})
|
|
37
|
+
.readonly();
|
|
38
|
+
|
|
39
|
+
export type PdfTableColumn = z.infer<typeof pdfTableColumnSchema>;
|
|
40
|
+
export type PdfTableDocument = z.infer<typeof pdfTableDocumentSchema>;
|
|
41
|
+
|
|
42
|
+
const marginMmSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
top: z.number().nonnegative().optional(),
|
|
45
|
+
right: z.number().nonnegative().optional(),
|
|
46
|
+
bottom: z.number().nonnegative().optional(),
|
|
47
|
+
left: z.number().nonnegative().optional(),
|
|
48
|
+
})
|
|
49
|
+
.readonly();
|
|
41
50
|
|
|
42
51
|
export type PdfTableOptions = {
|
|
43
|
-
readonly format?:
|
|
44
|
-
readonly marginMm?:
|
|
45
|
-
readonly top?: number;
|
|
46
|
-
readonly right?: number;
|
|
47
|
-
readonly bottom?: number;
|
|
48
|
-
readonly left?: number;
|
|
49
|
-
};
|
|
52
|
+
readonly format?: PdfPageFormat;
|
|
53
|
+
readonly marginMm?: z.input<typeof marginMmSchema>;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
export type PdfTableResult = { readonly pdfBytes: Uint8Array; readonly pageCount: number };
|
|
@@ -55,7 +59,7 @@ const POINTS_PER_MM = 72 / 25.4;
|
|
|
55
59
|
const PAGE_SIZE_POINTS = {
|
|
56
60
|
A4: { width: 595.28, height: 841.89 },
|
|
57
61
|
Letter: { width: 612, height: 792 },
|
|
58
|
-
} as const;
|
|
62
|
+
} as const satisfies Record<PdfPageFormat, { readonly width: number; readonly height: number }>;
|
|
59
63
|
const DEFAULT_MARGIN_MM = { top: 18, right: 15, bottom: 18, left: 15 } as const;
|
|
60
64
|
|
|
61
65
|
const TITLE_SIZE = 16;
|
|
@@ -66,6 +70,7 @@ const LINE_HEIGHT_FACTOR = 1.3;
|
|
|
66
70
|
const CELL_PADDING_X = 4;
|
|
67
71
|
const CELL_PADDING_Y = 3;
|
|
68
72
|
const FOOTER_RESERVE = 16;
|
|
73
|
+
const MIN_COLUMN_WIDTH = 20;
|
|
69
74
|
const META_LABEL_GAP = 10;
|
|
70
75
|
const BLOCK_GAP = 10;
|
|
71
76
|
const REPLACEMENT_CHAR = "?";
|
|
@@ -86,7 +91,7 @@ function createEncodableText(font: PDFFont): (text: string) => string {
|
|
|
86
91
|
const supported = new Set(font.getCharacterSet());
|
|
87
92
|
return (text) => {
|
|
88
93
|
let result = "";
|
|
89
|
-
for (const char of text.replace(/\t/g, " ")) {
|
|
94
|
+
for (const char of text.replace(/\r\n?/g, "\n").replace(/\t/g, " ")) {
|
|
90
95
|
const codePoint = char.codePointAt(0) ?? 0;
|
|
91
96
|
result += codePoint === 0x0a || supported.has(codePoint) ? char : REPLACEMENT_CHAR;
|
|
92
97
|
}
|
|
@@ -157,30 +162,43 @@ function assertRowShape(doc: PdfTableDocument): void {
|
|
|
157
162
|
});
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
assertRowShape(doc);
|
|
168
|
-
|
|
169
|
-
const pdf = await PDFDocument.create({ updateMetadata: false });
|
|
170
|
-
const fonts: Fonts = {
|
|
171
|
-
regular: await pdf.embedFont(StandardFonts.Helvetica),
|
|
172
|
-
bold: await pdf.embedFont(StandardFonts.HelveticaBold),
|
|
165
|
+
type TableLayout = {
|
|
166
|
+
readonly pageSize: { readonly width: number; readonly height: number };
|
|
167
|
+
readonly margin: {
|
|
168
|
+
readonly top: number;
|
|
169
|
+
readonly right: number;
|
|
170
|
+
readonly bottom: number;
|
|
171
|
+
readonly left: number;
|
|
173
172
|
};
|
|
174
|
-
|
|
173
|
+
readonly contentWidth: number;
|
|
174
|
+
readonly contentBottom: number;
|
|
175
|
+
readonly columnWidths: readonly number[];
|
|
176
|
+
readonly bodyLineHeight: number;
|
|
177
|
+
readonly headerLines: readonly (readonly string[])[];
|
|
178
|
+
readonly headerHeight: number;
|
|
179
|
+
};
|
|
175
180
|
|
|
181
|
+
function resolveTableLayout(
|
|
182
|
+
doc: PdfTableDocument,
|
|
183
|
+
options: PdfTableOptions | undefined,
|
|
184
|
+
fonts: Fonts,
|
|
185
|
+
clean: (text: string) => string,
|
|
186
|
+
): TableLayout {
|
|
187
|
+
const marginMm = marginMmSchema.safeParse(options?.marginMm ?? {});
|
|
188
|
+
if (!marginMm.success) {
|
|
189
|
+
throw new RendererError("margins must be non-negative numbers", "invalid_payload");
|
|
190
|
+
}
|
|
176
191
|
const pageSize = PAGE_SIZE_POINTS[options?.format ?? "A4"];
|
|
177
192
|
const margin = {
|
|
178
|
-
top: (
|
|
179
|
-
right: (
|
|
180
|
-
bottom: (
|
|
181
|
-
left: (
|
|
193
|
+
top: (marginMm.data.top ?? DEFAULT_MARGIN_MM.top) * POINTS_PER_MM,
|
|
194
|
+
right: (marginMm.data.right ?? DEFAULT_MARGIN_MM.right) * POINTS_PER_MM,
|
|
195
|
+
bottom: (marginMm.data.bottom ?? DEFAULT_MARGIN_MM.bottom) * POINTS_PER_MM,
|
|
196
|
+
left: (marginMm.data.left ?? DEFAULT_MARGIN_MM.left) * POINTS_PER_MM,
|
|
182
197
|
};
|
|
183
198
|
const contentWidth = pageSize.width - margin.left - margin.right;
|
|
199
|
+
if (contentWidth < MIN_COLUMN_WIDTH * doc.columns.length) {
|
|
200
|
+
throw new RendererError("margins leave no room for table columns", "invalid_payload");
|
|
201
|
+
}
|
|
184
202
|
const contentBottom = margin.bottom + FOOTER_RESERVE;
|
|
185
203
|
const columnWidths = resolveColumnWidths(doc.columns, contentWidth);
|
|
186
204
|
const bodyLineHeight = lineHeightFor(BODY_SIZE);
|
|
@@ -200,6 +218,48 @@ export async function renderTablePdf(
|
|
|
200
218
|
);
|
|
201
219
|
const headerHeight =
|
|
202
220
|
Math.max(...headerLines.map((lines) => lines.length)) * bodyLineHeight + 2 * CELL_PADDING_Y;
|
|
221
|
+
// A header this tall would leave every continuation page without room for a row.
|
|
222
|
+
if (rowSpaceOnEmptyPage - headerHeight < bodyLineHeight + 2 * CELL_PADDING_Y) {
|
|
223
|
+
throw new RendererError("table header leaves no room for rows", "invalid_payload");
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
pageSize,
|
|
227
|
+
margin,
|
|
228
|
+
contentWidth,
|
|
229
|
+
contentBottom,
|
|
230
|
+
columnWidths,
|
|
231
|
+
bodyLineHeight,
|
|
232
|
+
headerLines,
|
|
233
|
+
headerHeight,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function renderTablePdf(
|
|
238
|
+
doc: PdfTableDocument,
|
|
239
|
+
options?: PdfTableOptions,
|
|
240
|
+
): Promise<PdfTableResult> {
|
|
241
|
+
if (doc.columns.length === 0) {
|
|
242
|
+
throw new RendererError("table needs at least one column", "invalid_payload");
|
|
243
|
+
}
|
|
244
|
+
assertRowShape(doc);
|
|
245
|
+
|
|
246
|
+
const pdf = await PDFDocument.create({ updateMetadata: false });
|
|
247
|
+
const fonts: Fonts = {
|
|
248
|
+
regular: await pdf.embedFont(StandardFonts.Helvetica),
|
|
249
|
+
bold: await pdf.embedFont(StandardFonts.HelveticaBold),
|
|
250
|
+
};
|
|
251
|
+
const clean = createEncodableText(fonts.regular);
|
|
252
|
+
|
|
253
|
+
const {
|
|
254
|
+
pageSize,
|
|
255
|
+
margin,
|
|
256
|
+
contentWidth,
|
|
257
|
+
contentBottom,
|
|
258
|
+
columnWidths,
|
|
259
|
+
bodyLineHeight,
|
|
260
|
+
headerLines,
|
|
261
|
+
headerHeight,
|
|
262
|
+
} = resolveTableLayout(doc, options, fonts, clean);
|
|
203
263
|
|
|
204
264
|
let page: PDFPage = pdf.addPage([pageSize.width, pageSize.height]);
|
|
205
265
|
let cursorY = pageSize.height - margin.top;
|
|
@@ -227,29 +287,50 @@ export async function renderTablePdf(
|
|
|
227
287
|
});
|
|
228
288
|
}
|
|
229
289
|
|
|
290
|
+
function startBlankPage(): void {
|
|
291
|
+
page = pdf.addPage([pageSize.width, pageSize.height]);
|
|
292
|
+
cursorY = pageSize.height - margin.top;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function ensureRoom(height: number): void {
|
|
296
|
+
if (cursorY - height < contentBottom) startBlankPage();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Line by line, so a block taller than one page continues on the next.
|
|
300
|
+
function flowLines(
|
|
301
|
+
lines: readonly string[],
|
|
302
|
+
font: PDFFont,
|
|
303
|
+
size: number,
|
|
304
|
+
left: number,
|
|
305
|
+
width: number,
|
|
306
|
+
color = TEXT_COLOR,
|
|
307
|
+
): void {
|
|
308
|
+
const lineHeight = lineHeightFor(size);
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
ensureRoom(lineHeight);
|
|
311
|
+
drawLines([line], font, size, left, width, cursorY, "left", color);
|
|
312
|
+
cursorY -= lineHeight;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
230
316
|
function drawIntroBlock(): void {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
317
|
+
flowLines(
|
|
318
|
+
wrapText(clean(doc.title), fonts.bold, TITLE_SIZE, contentWidth),
|
|
319
|
+
fonts.bold,
|
|
320
|
+
TITLE_SIZE,
|
|
321
|
+
margin.left,
|
|
322
|
+
contentWidth,
|
|
323
|
+
);
|
|
234
324
|
|
|
235
325
|
if (doc.subtitle !== undefined) {
|
|
236
|
-
|
|
237
|
-
clean(doc.subtitle),
|
|
238
|
-
fonts.regular,
|
|
239
|
-
SUBTITLE_SIZE,
|
|
240
|
-
contentWidth,
|
|
241
|
-
);
|
|
242
|
-
drawLines(
|
|
243
|
-
subtitleLines,
|
|
326
|
+
flowLines(
|
|
327
|
+
wrapText(clean(doc.subtitle), fonts.regular, SUBTITLE_SIZE, contentWidth),
|
|
244
328
|
fonts.regular,
|
|
245
329
|
SUBTITLE_SIZE,
|
|
246
330
|
margin.left,
|
|
247
331
|
contentWidth,
|
|
248
|
-
cursorY,
|
|
249
|
-
"left",
|
|
250
332
|
MUTED_COLOR,
|
|
251
333
|
);
|
|
252
|
-
cursorY -= subtitleLines.length * lineHeightFor(SUBTITLE_SIZE);
|
|
253
334
|
}
|
|
254
335
|
|
|
255
336
|
const meta = doc.meta ?? [];
|
|
@@ -266,6 +347,7 @@ export async function renderTablePdf(
|
|
|
266
347
|
BODY_SIZE,
|
|
267
348
|
contentWidth - labelWidth,
|
|
268
349
|
);
|
|
350
|
+
ensureRoom(bodyLineHeight);
|
|
269
351
|
drawLines(
|
|
270
352
|
[labels[index] ?? ""],
|
|
271
353
|
fonts.bold,
|
|
@@ -275,16 +357,13 @@ export async function renderTablePdf(
|
|
|
275
357
|
cursorY,
|
|
276
358
|
"left",
|
|
277
359
|
);
|
|
278
|
-
|
|
360
|
+
flowLines(
|
|
279
361
|
valueLines,
|
|
280
362
|
fonts.regular,
|
|
281
363
|
BODY_SIZE,
|
|
282
364
|
margin.left + labelWidth,
|
|
283
365
|
contentWidth - labelWidth,
|
|
284
|
-
cursorY,
|
|
285
|
-
"left",
|
|
286
366
|
);
|
|
287
|
-
cursorY -= valueLines.length * bodyLineHeight;
|
|
288
367
|
});
|
|
289
368
|
}
|
|
290
369
|
cursorY -= BLOCK_GAP;
|
|
@@ -372,6 +451,7 @@ export async function renderTablePdf(
|
|
|
372
451
|
}
|
|
373
452
|
|
|
374
453
|
drawIntroBlock();
|
|
454
|
+
ensureRoom(headerHeight + bodyLineHeight + 2 * CELL_PADDING_Y);
|
|
375
455
|
drawTableHeader();
|
|
376
456
|
|
|
377
457
|
if (doc.rows.length === 0 && doc.emptyText !== undefined) {
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "secrets delete no longer requires a declared key",
|
|
6
|
+
"detail": "secrets:write:delete used to answer 404 unknownKey for a key that no feature declares, so a row stored before the declaration gate existed could never be removed. Delete now skips the declaration check; writeRoles of a declared key or namespace still apply, and set keeps the gate."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.364.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "SecretsContext.withRunner(tx) runs secret access on a given transaction",
|
|
12
|
+
"detail": "The OAuth refresh now reads and writes the refresh token on its advisory-lock transaction, so a refresh holds one pool connection instead of two and parallel refreshes cannot starve the pool. Custom SecretsContext implementations need a `withRunner` method."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.343.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -83,21 +83,22 @@ export function requireSecretsContext(
|
|
|
83
83
|
"createSecretsContext({db, masterKeyProvider, registry}) via extraContext.secrets at boot.",
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
|
-
const raw = ctx.secrets;
|
|
87
86
|
const userId = ctx._userId;
|
|
88
87
|
if (!userId) {
|
|
89
88
|
throw new InternalError({
|
|
90
89
|
message: `[${handlerName}] ctx._userId missing — cannot audit secret reads without a caller identity.`,
|
|
91
90
|
});
|
|
92
91
|
}
|
|
93
|
-
|
|
92
|
+
const withAudit = (raw: SecretsContext): SecretsContext => ({
|
|
93
|
+
withRunner: (runner) => withAudit(raw.withRunner(runner)),
|
|
94
94
|
get: (tenantId, key, overrideAudit) =>
|
|
95
95
|
raw.get(tenantId, key, overrideAudit ?? { userId, handlerName }),
|
|
96
96
|
// No audit injection: has() is metadata-only and never logs a read.
|
|
97
97
|
has: raw.has.bind(raw),
|
|
98
98
|
set: raw.set.bind(raw),
|
|
99
99
|
delete: raw.delete.bind(raw),
|
|
100
|
-
};
|
|
100
|
+
});
|
|
101
|
+
return withAudit(ctx.secrets);
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
export type SecretsFeatureOptions = {
|
|
@@ -18,7 +18,9 @@ export function createDeleteHandler(access: AccessRule = DEFAULT_SECRETS_ACCESS)
|
|
|
18
18
|
access,
|
|
19
19
|
handler: async (event, ctx) => {
|
|
20
20
|
const secrets = requireSecretsContext(ctx, "secrets:write:delete");
|
|
21
|
-
const gate = checkSecretKeyWrite(ctx.registry, event.user.roles, event.payload.key
|
|
21
|
+
const gate = checkSecretKeyWrite(ctx.registry, event.user.roles, event.payload.key, {
|
|
22
|
+
allowUndeclared: true,
|
|
23
|
+
});
|
|
22
24
|
if (!gate.ok) return gate.failure;
|
|
23
25
|
const removed = await secrets.delete(event.user.tenantId, event.payload.key, {
|
|
24
26
|
deletedBy: event.user.id,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
createEventStoreExecutor,
|
|
18
18
|
createTenantDb,
|
|
19
19
|
type DbConnection,
|
|
20
|
+
type DbRunner,
|
|
20
21
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
21
22
|
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
23
|
import { InternalError, type WriteErrorInfo } from "@cosmicdrift/kumiko-framework/errors";
|
|
@@ -91,8 +92,20 @@ function resolveKey(keyOrHandle: string | { readonly name: string }): string {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export function createSecretsContext(opts: SecretsContextOptions): SecretsContext {
|
|
94
|
-
const {
|
|
95
|
+
const { masterKeyProvider } = opts;
|
|
95
96
|
const provider = withDekCache(masterKeyProvider, opts.dekCache ?? createDekCache());
|
|
97
|
+
return buildSecretsContext(opts, provider, opts.db, false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// `runnerIsTransaction`: the runner is a caller-owned transaction, so audited reads join it
|
|
101
|
+
// instead of opening a second connection (which would deadlock a small pool).
|
|
102
|
+
function buildSecretsContext(
|
|
103
|
+
opts: SecretsContextOptions,
|
|
104
|
+
provider: MasterKeyProvider,
|
|
105
|
+
db: DbRunner,
|
|
106
|
+
runnerIsTransaction: boolean,
|
|
107
|
+
): SecretsContext {
|
|
108
|
+
const { masterKeyProvider } = opts;
|
|
96
109
|
|
|
97
110
|
type SecretLookupRow = {
|
|
98
111
|
readonly id: string;
|
|
@@ -105,6 +118,10 @@ export function createSecretsContext(opts: SecretsContextOptions): SecretsContex
|
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
return {
|
|
121
|
+
withRunner(runner) {
|
|
122
|
+
return buildSecretsContext(opts, provider, runner, true);
|
|
123
|
+
},
|
|
124
|
+
|
|
108
125
|
async get(tenantId, keyOrHandle, auditCtx) {
|
|
109
126
|
const key = resolveKey(keyOrHandle);
|
|
110
127
|
// Atomic audit + read: a decrypt that "escaped" the audit trail
|
|
@@ -120,7 +137,7 @@ export function createSecretsContext(opts: SecretsContextOptions): SecretsContex
|
|
|
120
137
|
return createSecret(plaintext);
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
const
|
|
140
|
+
const readAndAudit = async (tx: DbRunner): Promise<string | undefined> => {
|
|
124
141
|
// Inline select inside the TX via raw client — fetchOne's connection
|
|
125
142
|
// type doesn't widen to the transaction object cleanly.
|
|
126
143
|
const envelope = await selectTenantSecretEnvelope(tx, tenantId, key);
|
|
@@ -154,7 +171,10 @@ export function createSecretsContext(opts: SecretsContextOptions): SecretsContex
|
|
|
154
171
|
metadata,
|
|
155
172
|
});
|
|
156
173
|
return pt;
|
|
157
|
-
}
|
|
174
|
+
};
|
|
175
|
+
const plaintext = runnerIsTransaction
|
|
176
|
+
? await readAndAudit(db)
|
|
177
|
+
: await transaction(db, readAndAudit);
|
|
158
178
|
|
|
159
179
|
if (plaintext === undefined) return undefined;
|
|
160
180
|
// Brand the plaintext only after audit committed. The response
|
|
@@ -37,15 +37,21 @@ function namespaceAcceptsKey(namespace: SecretNamespaceDefinition, key: string):
|
|
|
37
37
|
// Only keys declared via r.secret() or under a declared r.secretNamespace()
|
|
38
38
|
// are writable through the API, so a tenant admin cannot plant arbitrary rows
|
|
39
39
|
// next to the feature-owned ones. writeRoles narrows the handler access
|
|
40
|
-
// further for a key or a whole namespace.
|
|
40
|
+
// further for a key or a whole namespace. Delete passes allowUndeclared: it
|
|
41
|
+
// creates nothing, and legacy rows under since-undeclared keys must stay removable.
|
|
41
42
|
export function checkSecretKeyWrite(
|
|
42
43
|
registry: Registry,
|
|
43
44
|
userRoles: readonly string[],
|
|
44
45
|
key: string,
|
|
46
|
+
options: { readonly allowUndeclared?: boolean } = {},
|
|
45
47
|
): SecretKeyWriteCheck {
|
|
46
48
|
const keyDef = registry.getSecretKey(key);
|
|
47
49
|
const namespace = keyDef ? undefined : registry.findSecretNamespace(key);
|
|
48
|
-
if (
|
|
50
|
+
if (
|
|
51
|
+
!keyDef &&
|
|
52
|
+
!(namespace && namespaceAcceptsKey(namespace, key)) &&
|
|
53
|
+
options.allowUndeclared !== true
|
|
54
|
+
) {
|
|
49
55
|
return unknownKeyFailure(key);
|
|
50
56
|
}
|
|
51
57
|
const writeRoles = keyDef?.writeRoles ?? namespace?.writeRoles;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Mail template body helpers exported as `./shared/mail-template-body`",
|
|
6
|
+
"detail": "Apps that register their own notification templates can build the body with the same helpers the framework uses: `mailParagraph`, `mailHeading`, `mailRepeatedRows`, `mailTextWithSlotsHtml`, plus the new `mailButton`, `mailParagraphWithLink` and `mailSubject`. The subpath only depends on kumiko-headless and is safe on the server. Apps that copied these helpers can import them instead."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.335.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -33,6 +33,9 @@ export type ChunkedMigrationOptions<Row> = {
|
|
|
33
33
|
readonly maxFailures: number;
|
|
34
34
|
// Epoch ms; Number.POSITIVE_INFINITY for no time bound.
|
|
35
35
|
readonly deadlineAt: number;
|
|
36
|
+
// Batches processed even when the deadline already passed. Callers whose setup before the
|
|
37
|
+
// loop can eat the whole budget set 1, so every run makes progress instead of timing out empty.
|
|
38
|
+
readonly minBatches?: number;
|
|
36
39
|
readonly signal?: AbortSignal | undefined;
|
|
37
40
|
readonly onRowError?: (row: Row, err: unknown) => void;
|
|
38
41
|
};
|
|
@@ -51,7 +54,7 @@ export async function runChunkedMigration<Row>(
|
|
|
51
54
|
stoppedReason = "signal";
|
|
52
55
|
break;
|
|
53
56
|
}
|
|
54
|
-
if (Date.now() >= opts.deadlineAt) {
|
|
57
|
+
if (batchesProcessed >= (opts.minBatches ?? 0) && Date.now() >= opts.deadlineAt) {
|
|
55
58
|
stoppedReason = "timeout";
|
|
56
59
|
break;
|
|
57
60
|
}
|
|
@@ -40,6 +40,14 @@ export {
|
|
|
40
40
|
} from "./redact.js";
|
|
41
41
|
export { REDACTED_PII_PLACEHOLDER, redactPiiCiphertext } from "./redact-pii-ciphertext.js";
|
|
42
42
|
export { requireForTenant } from "./require-for-tenant.js";
|
|
43
|
+
export {
|
|
44
|
+
extensionForResourceContentType,
|
|
45
|
+
isResourceImageContentType,
|
|
46
|
+
isResourceImageFormat,
|
|
47
|
+
RESOURCE_IMAGE_FORMATS,
|
|
48
|
+
type ResourceImageContentType,
|
|
49
|
+
type ResourceImageFormat,
|
|
50
|
+
} from "./resource-image-formats.js";
|
|
43
51
|
export {
|
|
44
52
|
type RowBoundGrantResult,
|
|
45
53
|
redeemRowBoundGrant,
|
|
@@ -19,7 +19,24 @@ export function mailHeading(text: string): string {
|
|
|
19
19
|
return `<h2>${mailTextWithSlotsHtml(text)}</h2>`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function mailButton(label: string, linkVariable: string): string {
|
|
23
|
+
return `<p><a data-button href="{{${escapeHtmlAttr(linkVariable)}}}">${escapeHtml(label)}</a></p>`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mailParagraphWithLink(
|
|
27
|
+
text: string,
|
|
28
|
+
linkLabel: string,
|
|
29
|
+
linkVariable: string,
|
|
30
|
+
): string {
|
|
31
|
+
return `<p>${mailTextWithSlotsHtml(text)} <a href="{{${escapeHtmlAttr(linkVariable)}}}">${escapeHtml(linkLabel)}</a></p>`;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
// One table row per list entry; the cell shows the entry's `field`.
|
|
23
35
|
export function mailRepeatedRows(listName: string, field: string): string {
|
|
24
36
|
return `<table><tbody><tr data-each="${escapeHtmlAttr(listName)}"><td><p><span data-variable="${escapeHtmlAttr(field)}"></span></p></td></tr></tbody></table>`;
|
|
25
37
|
}
|
|
38
|
+
|
|
39
|
+
// Subjects are Handlebars text, so the same `{name}` slots become `{{name}}`.
|
|
40
|
+
export function mailSubject(text: string): string {
|
|
41
|
+
return text.replace(SLOT_PATTERN, "{{$1}}");
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Single source for the raster formats a template resource image may have: the normalizer, the
|
|
2
|
+
// stored file extension, the data-URI embedding and the inline mail attachment name all derive
|
|
3
|
+
// from it, so a new format cannot be accepted in one place and dropped in another.
|
|
4
|
+
export const RESOURCE_IMAGE_FORMATS = {
|
|
5
|
+
png: { contentType: "image/png", extension: "png" },
|
|
6
|
+
jpeg: { contentType: "image/jpeg", extension: "jpg" },
|
|
7
|
+
gif: { contentType: "image/gif", extension: "gif" },
|
|
8
|
+
webp: { contentType: "image/webp", extension: "webp" },
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type ResourceImageFormat = keyof typeof RESOURCE_IMAGE_FORMATS;
|
|
12
|
+
export type ResourceImageContentType =
|
|
13
|
+
(typeof RESOURCE_IMAGE_FORMATS)[ResourceImageFormat]["contentType"];
|
|
14
|
+
|
|
15
|
+
export function isResourceImageFormat(format: string | undefined): format is ResourceImageFormat {
|
|
16
|
+
return format !== undefined && Object.hasOwn(RESOURCE_IMAGE_FORMATS, format);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatForContentType(contentType: string) {
|
|
20
|
+
return Object.values(RESOURCE_IMAGE_FORMATS).find((entry) => entry.contentType === contentType);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isResourceImageContentType(
|
|
24
|
+
contentType: string,
|
|
25
|
+
): contentType is ResourceImageContentType {
|
|
26
|
+
return formatForContentType(contentType) !== undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function extensionForResourceContentType(contentType: string): string | undefined {
|
|
30
|
+
return formatForContentType(contentType)?.extension;
|
|
31
|
+
}
|