@cosmicdrift/kumiko-samples 0.364.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 +1 -1
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +1 -3
- package/packages/bundled-features/src/auth-email-password/changes.json +18 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- 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/switch-tenant-mfa-gate.write.ts +2 -2
- 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/billing-foundation/changes.json +12 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +8 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +6 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +10 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +13 -0
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -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 +30 -20
- 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/crypto-shredding/handlers/forget-subject.write.ts +7 -1
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +18 -1
- package/packages/bundled-features/src/delivery/changes.json +12 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- 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/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/jobs/changes.json +12 -0
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -2
- 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/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/index.ts +8 -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/template-resolver/changes.json +81 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +78 -12
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +11 -1
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +7 -1
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +12 -0
- 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 +111 -61
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +1 -0
- 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/overview.query.ts +12 -2
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +48 -9
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +6 -1
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +25 -9
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +2 -0
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-meta.ts +8 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-resources.ts +2 -43
- package/packages/bundled-features/src/template-resolver/template-validation.ts +15 -9
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +34 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +3 -2
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +56 -11
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +6 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +1 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +14 -5
- 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 +12 -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/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/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -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/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/workflow-runner/feature.ts +1 -1
- package/samples/apps/ui-walkthrough/src/run-config.ts +14 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +3 -3
- 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
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
8
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
9
9
|
import {
|
|
10
|
+
type CacheSyncBus,
|
|
10
11
|
type ClosableCacheSyncBus,
|
|
11
12
|
createDefaultCacheSyncBus,
|
|
12
13
|
} from "@cosmicdrift/kumiko-framework/redis";
|
|
@@ -40,9 +41,11 @@ export type ToggleSyncSignal = {
|
|
|
40
41
|
// per-instance behavior for the only process there is.
|
|
41
42
|
export class GlobalFeatureToggleRuntime {
|
|
42
43
|
private snapshot = new Map<string, boolean>();
|
|
43
|
-
//
|
|
44
|
-
// started before
|
|
45
|
-
private
|
|
44
|
+
// One buffer per requested load, open from the call until its SELECT finished: the SELECT
|
|
45
|
+
// may have started before a flip committed, so its rows can be older than the flips collected.
|
|
46
|
+
private readonly pendingFlipBuffers = new Set<Map<string, boolean>>();
|
|
47
|
+
// Loads run one at a time so the older rows of one cannot overwrite the result of another.
|
|
48
|
+
private loadInFlight: Promise<void> = Promise.resolve();
|
|
46
49
|
|
|
47
50
|
constructor(
|
|
48
51
|
private readonly db: DbConnection,
|
|
@@ -69,10 +72,16 @@ export class GlobalFeatureToggleRuntime {
|
|
|
69
72
|
await this.ownedBus?.close();
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
type Row = { featureName: string; enabled: boolean };
|
|
75
|
+
initialize(): Promise<void> {
|
|
74
76
|
const pendingFlips = new Map<string, boolean>();
|
|
75
|
-
this.
|
|
77
|
+
this.pendingFlipBuffers.add(pendingFlips);
|
|
78
|
+
const load = this.loadInFlight.then(() => this.loadSnapshot(pendingFlips));
|
|
79
|
+
this.loadInFlight = load.catch(() => undefined);
|
|
80
|
+
return load;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async loadSnapshot(pendingFlips: Map<string, boolean>): Promise<void> {
|
|
84
|
+
type Row = { featureName: string; enabled: boolean };
|
|
76
85
|
try {
|
|
77
86
|
const rows = await selectMany<Row>(this.db, globalFeatureStateTable);
|
|
78
87
|
this.snapshot = new Map([
|
|
@@ -80,7 +89,7 @@ export class GlobalFeatureToggleRuntime {
|
|
|
80
89
|
...pendingFlips,
|
|
81
90
|
]);
|
|
82
91
|
} finally {
|
|
83
|
-
this.
|
|
92
|
+
this.pendingFlipBuffers.delete(pendingFlips);
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -98,7 +107,7 @@ export class GlobalFeatureToggleRuntime {
|
|
|
98
107
|
// lock. Kept alongside refresh() so both options are explicit.
|
|
99
108
|
apply(featureName: string, enabled: boolean): void {
|
|
100
109
|
this.snapshot.set(featureName, enabled);
|
|
101
|
-
this.
|
|
110
|
+
for (const pendingFlips of this.pendingFlipBuffers) pendingFlips.set(featureName, enabled);
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
// Called by the toggle-cache-sync MSP handler (feature.ts) — the one
|
|
@@ -144,15 +153,20 @@ export class GlobalFeatureToggleRuntime {
|
|
|
144
153
|
|
|
145
154
|
// Factory for app-boot wiring: instantiate, initialize, return the runtime (for the
|
|
146
155
|
// set-handler to refresh; its effectiveFeatures feeds createDispatcher). Without an explicit
|
|
147
|
-
// syncSignal the
|
|
148
|
-
// REDIS_URL is set, process-local otherwise. close() releases
|
|
156
|
+
// syncSignal the signal runs on the given cacheSyncBus or, failing that, on a bus the runtime
|
|
157
|
+
// creates itself (Redis-backed when REDIS_URL is set, process-local otherwise). close() releases
|
|
158
|
+
// only a bus the runtime created.
|
|
149
159
|
export async function createFeatureToggleRuntime(
|
|
150
160
|
db: DbConnection,
|
|
151
161
|
registry: Registry,
|
|
152
162
|
syncSignal?: ToggleSyncSignal,
|
|
163
|
+
// The server's own bus (the one handed to the dispatcher): reusing it avoids a second set of
|
|
164
|
+
// Redis connections that nothing closes at shutdown.
|
|
165
|
+
cacheSyncBus?: CacheSyncBus,
|
|
153
166
|
): Promise<GlobalFeatureToggleRuntime> {
|
|
154
|
-
const ownedBus = syncSignal ? undefined : createDefaultCacheSyncBus();
|
|
155
|
-
const
|
|
167
|
+
const ownedBus = syncSignal || cacheSyncBus ? undefined : createDefaultCacheSyncBus();
|
|
168
|
+
const sharedBus = cacheSyncBus ?? ownedBus;
|
|
169
|
+
const signal = syncSignal ?? (sharedBus ? createCacheSyncToggleSignal(sharedBus) : undefined);
|
|
156
170
|
const runtime = new GlobalFeatureToggleRuntime(db, registry, signal, ownedBus);
|
|
157
171
|
try {
|
|
158
172
|
await runtime.initialize();
|
|
@@ -27,8 +27,10 @@ import { fileRefStorageDestroyHook, fileRefTenantDestroyHook } from "./hooks.js"
|
|
|
27
27
|
const SWEEP_ORPHANED_DERIVATIVES_ESCAPE_HATCH_REASON =
|
|
28
28
|
"iterates every tenant and checks fileRef owners per tenant";
|
|
29
29
|
|
|
30
|
+
export const FILES_TENANT_DATA_FEATURE = "files-tenant-data" as const;
|
|
31
|
+
|
|
30
32
|
export function createFilesTenantDataFeature(): FeatureDefinition {
|
|
31
|
-
return defineFeature(
|
|
33
|
+
return defineFeature(FILES_TENANT_DATA_FEATURE, (r) => {
|
|
32
34
|
r.describe(
|
|
33
35
|
"GDPR coverage for the `files` feature's `fileRef` entity: tenant-destroy row purge (EXT_TENANT_DATA) and full storage-prefix binary wipe (EXT_STORAGE_PROVIDER), plus a manual backfill/GC job that sweeps derivatives orphaned by a forget/tenant-destroy that ran before #2461 wired binary cleanup into those flows. Requires `tenant-lifecycle`, optionalRequires `files`.",
|
|
34
36
|
);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
instantToTimestamptzParam,
|
|
3
|
+
unsafeReadRetrying,
|
|
4
|
+
} from "@cosmicdrift/kumiko-framework/bun-db";
|
|
5
|
+
import { type DbRunner, toPostgresArrayLiteral } from "@cosmicdrift/kumiko-framework/db";
|
|
3
6
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
7
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
8
|
|
|
@@ -68,7 +71,13 @@ export async function filterOwnedFileRefs(
|
|
|
68
71
|
WHERE "tenant_id" = $1 AND "inserted_by_id" = $2
|
|
69
72
|
AND "storage_key" = ANY($3::text[]) AND "is_deleted" = false
|
|
70
73
|
AND ($5::boolean OR "inserted_at" > $4::timestamptz)`,
|
|
71
|
-
[
|
|
74
|
+
[
|
|
75
|
+
tenantId,
|
|
76
|
+
ownerId,
|
|
77
|
+
toPostgresArrayLiteral(candidateKeys),
|
|
78
|
+
instantToTimestamptzParam(draftInsertedAt),
|
|
79
|
+
isCreateMode,
|
|
80
|
+
],
|
|
72
81
|
);
|
|
73
82
|
return rows.map((row) => ({ id: row.id, storageKey: row.storage_key }));
|
|
74
83
|
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Finished tenant run reads its timestamp under the lock",
|
|
6
|
+
"detail": "The run that finishes last in lock order now carries the latest finishedAt, so a stale timestamp can no longer make the newest run the row that gets pruned."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Older migration notes that say \"kumiko migrate generate\" mean kumiko-schema generate",
|
|
12
|
+
"detail": "Migration notes of the delivery, jobs, metrics and sessions features name a command that does not exist, `kumiko migrate generate`. Run `kumiko-schema generate <name>` and then `kumiko-schema apply` instead. The published notes stay as they were."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.364.0",
|
|
4
16
|
"type": "fix",
|
|
@@ -341,13 +341,16 @@ async function recordTenantRunOutcome(
|
|
|
341
341
|
TENANT_JOB_RUN_LOCK_NAMESPACE,
|
|
342
342
|
JSON.stringify([key.tenantId, key.jobName, key.subject]),
|
|
343
343
|
);
|
|
344
|
+
// Read under the lock: the run that finishes last in lock order must also carry the
|
|
345
|
+
// latest finishedAt, otherwise a stale timestamp makes it the row that gets pruned.
|
|
346
|
+
const finishedAt = Temporal.Now.instant();
|
|
344
347
|
await upsertOnConflict(
|
|
345
348
|
tx,
|
|
346
349
|
tenantJobRunsTable,
|
|
347
|
-
{ ...key, bullJobId, status: result, finishedAt
|
|
350
|
+
{ ...key, bullJobId, status: result, finishedAt, updatedAt: finishedAt },
|
|
348
351
|
{
|
|
349
352
|
conflictKeys: [...TENANT_RUN_CONFLICT_KEYS],
|
|
350
|
-
update: { status: result, finishedAt
|
|
353
|
+
update: { status: result, finishedAt, updatedAt: finishedAt },
|
|
351
354
|
},
|
|
352
355
|
);
|
|
353
356
|
await deleteMany(tx, tenantJobRunsTable, {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Metric integer filters reject fractions and out-of-range values; feature-name constants exported",
|
|
6
|
+
"detail": "Filters on smallint, integer and bigint columns now fail validation for decimals and values outside the column range instead of a Postgres cast error. FILES_FEATURE and FILES_TENANT_DATA_FEATURE are exported and reused by compose-features."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.349.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -7,26 +7,34 @@ export const RESERVED_PAYLOAD_KEYS: readonly string[] = ["range", "timeZone", "t
|
|
|
7
7
|
|
|
8
8
|
// Dashboard select options always send strings.
|
|
9
9
|
const NUMERIC_STRING = /^-?\d+(\.\d+)?$/;
|
|
10
|
+
const INTEGER_STRING = /^-?\d+$/;
|
|
10
11
|
const MAX_FILTER_STRING_LENGTH = 200;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
const INTEGER_RANGES = {
|
|
13
|
+
smallint: { min: -32_768, max: 32_767 },
|
|
14
|
+
integer: { min: -2_147_483_648, max: 2_147_483_647 },
|
|
15
|
+
// bigint is bound as a JS number, so only the exactly representable range is accepted.
|
|
16
|
+
bigint: { min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
|
|
17
|
+
} as const;
|
|
18
|
+
const PG_INTEGER_KINDS: Readonly<Record<string, keyof typeof INTEGER_RANGES>> = {
|
|
19
|
+
smallint: "smallint",
|
|
20
|
+
int2: "smallint",
|
|
21
|
+
integer: "integer",
|
|
22
|
+
int4: "integer",
|
|
23
|
+
serial: "integer",
|
|
24
|
+
bigint: "bigint",
|
|
25
|
+
bigserial: "bigint",
|
|
26
|
+
};
|
|
21
27
|
|
|
22
|
-
export type FilterValueKind = "uuid" | "number" | "string";
|
|
28
|
+
export type FilterValueKind = "uuid" | "number" | "string" | keyof typeof INTEGER_RANGES;
|
|
23
29
|
|
|
24
30
|
/** Undefined = the column type cannot be filtered by a payload value without risking a Postgres cast error. */
|
|
25
31
|
export function filterValueKindOf(pgType: string | undefined): FilterValueKind | undefined {
|
|
26
32
|
if (pgType === undefined) return undefined;
|
|
27
33
|
if (pgType === "uuid") return "uuid";
|
|
28
34
|
if (pgType === "text") return "string";
|
|
29
|
-
|
|
35
|
+
const integerKind = PG_INTEGER_KINDS[pgType];
|
|
36
|
+
if (integerKind !== undefined) return integerKind;
|
|
37
|
+
if (pgType === "double precision" || pgType.startsWith("numeric")) return "number";
|
|
30
38
|
return undefined;
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -44,8 +52,16 @@ export function metricFilterEntries(metric: MetricDefinition): readonly MetricFi
|
|
|
44
52
|
);
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
function integerValueSchema(range: { readonly min: number; readonly max: number }): z.ZodType {
|
|
56
|
+
const bounded = z.number().int().min(range.min).max(range.max);
|
|
57
|
+
return z.union([bounded, z.string().regex(INTEGER_STRING).transform(Number).pipe(bounded)]);
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
function filterValueSchema(kind: FilterValueKind): z.ZodType {
|
|
48
61
|
if (kind === "uuid") return z.uuid();
|
|
62
|
+
if (kind === "smallint" || kind === "integer" || kind === "bigint") {
|
|
63
|
+
return integerValueSchema(INTEGER_RANGES[kind]);
|
|
64
|
+
}
|
|
49
65
|
if (kind === "number")
|
|
50
66
|
return z.union([z.number(), z.string().regex(NUMERIC_STRING).transform(Number)]);
|
|
51
67
|
return z.string().min(1).max(MAX_FILTER_STRING_LENGTH);
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "revokeAllPatTokensForUser detects begin() the same way as the transaction helpers",
|
|
6
|
+
"detail": "The transaction probe looked for begin() directly on the handle, so a pool wrapped by a legacy Drizzle connection fell back to a non-atomic update plus append. It now uses runnerCanBeginTransaction from bun-db, which unwraps the client first."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.347.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -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,10 @@
|
|
|
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
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.364.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -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,
|
|
@@ -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;
|
|
@@ -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,
|
|
@@ -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
|
+
}
|
|
@@ -5,22 +5,21 @@
|
|
|
5
5
|
// to be an OOM primitive for a single request.
|
|
6
6
|
export const MAX_INPUT_PIXELS = 100_000_000;
|
|
7
7
|
// sequentialRead trades random pixel access for lower peak memory on
|
|
8
|
-
// streamed formats
|
|
9
|
-
//
|
|
10
|
-
// against a single already-buffered region per call).
|
|
8
|
+
// streamed formats. Shared by every sharp caller since none of them need
|
|
9
|
+
// non-sequential access to the original bytes.
|
|
11
10
|
export const SHARP_INPUT_OPTIONS = {
|
|
12
11
|
limitInputPixels: MAX_INPUT_PIXELS,
|
|
13
12
|
sequentialRead: true,
|
|
14
13
|
} as const;
|
|
15
14
|
|
|
16
|
-
// Shared trust boundary for every image byte source
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
15
|
+
// Shared trust boundary for every image byte source decoded through sharp:
|
|
16
|
+
// uploads, overlay layers and template resources alike. A base64 string in a
|
|
17
|
+
// field declaration isn't more trustworthy than an upload just because it
|
|
18
|
+
// came from app code.
|
|
20
19
|
export function assertNotSvg(format: string | undefined): void {
|
|
21
20
|
if (format === "svg") {
|
|
22
21
|
throw new Error(
|
|
23
|
-
"
|
|
22
|
+
"SVG is not supported — rendering untrusted SVG through libvips is a trust boundary this code doesn't need to cross.",
|
|
24
23
|
);
|
|
25
24
|
}
|
|
26
25
|
}
|
|
@@ -118,8 +118,8 @@ export type WebhookDispatchDeps = {
|
|
|
118
118
|
readonly requestTimeoutMs?: number;
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
121
|
+
// Bounds a single hanging tenant-controlled receiver to this long. The consumer
|
|
122
|
+
// is sequential and shared, so N hanging requests still delay other tenants by N times this.
|
|
123
123
|
export const WEBHOOK_REQUEST_TIMEOUT_MS = 10_000;
|
|
124
124
|
|
|
125
125
|
// Never includes the secret name or value — spec.auth.secret is a
|
|
@@ -1,4 +1,85 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "copy-locale copies the system default's files in a beforeTransaction phase and can no longer run as a nested write",
|
|
6
|
+
"detail": "When a tenant copies a locale from the system default, the image and attachment files are now read and written to the tenant's storage in a beforeTransaction phase. The transaction only creates the fileRef rows, and the uploaded binaries are released when it does not commit. Before, the storage I/O held the database transaction open.",
|
|
7
|
+
"migration": "Dispatch copy-locale as a top-level command, or from a perCommand handler via ctx.write. A handler that called it nested moves that call into a client or job, or becomes a perCommand handler."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.365.0",
|
|
11
|
+
"type": "fix",
|
|
12
|
+
"title": "Opening a document or mail editor with an empty body no longer marks the form as changed",
|
|
13
|
+
"detail": "Switching the editor's editable state on mount emitted an update, so DocumentTiptapEditor called onChange(\"<p></p>\") before the user typed anything and the surrounding form counted as dirty."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.365.0",
|
|
17
|
+
"type": "breaking",
|
|
18
|
+
"title": "onEditorReady receives null when the editor is destroyed",
|
|
19
|
+
"detail": "DocumentTiptapEditor and PlaceholderLineEditor call onEditorReady(null) on destroy, so a held handle no longer points at a destroyed editor. The callback type is (handle: DocumentEditorHandle | null) => void. The document designer drops the handle of an unmounted field, so placeholder insertion no longer targets a destroyed editor.",
|
|
20
|
+
"migration": "A custom onEditorReady callback must accept null and drop its stored handle when it receives it."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"version": "0.365.0",
|
|
24
|
+
"type": "fix",
|
|
25
|
+
"title": "Document editor no longer blanks the app when late placeholders rebuild it",
|
|
26
|
+
"detail": "When the system placeholders or resource previews arrived in the same render as a new value (layout editor after opening or saving), the editor rebuilt itself and the value sync then read the destroyed editor. The TypeError unmounted the whole app and left a blank page. Both sync effects now skip a destroyed editor; the replacement editor picks up the value on its next render."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"version": "0.365.0",
|
|
30
|
+
"type": "improvement",
|
|
31
|
+
"title": "Steps block in mail and PDF templates",
|
|
32
|
+
"detail": "The document editor offers a steps block in the toolbar block menu. It stores a fixed list of stages (value=label, at most 8) and the variable that names the current stage. Mail and PDF render it as a row of bars and labels; stages up to the current one are coloured, the current label is bold. Invalid blocks are dropped by the sanitizer and render nothing. Layout regions do not offer the block."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"version": "0.365.0",
|
|
36
|
+
"type": "fix",
|
|
37
|
+
"title": "A paragraph or heading that only holds an empty placeholder is left out of the rendered document",
|
|
38
|
+
"detail": "compileDocumentBodyToHandlebars wraps a p or h1 to h4 whose only content is one placeholder in an if block, so an empty optional value no longer leaves a blank line in mails and PDFs. Zero numbers and money values still render. Blocks with static text, images or several placeholders are unchanged."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"version": "0.365.0",
|
|
42
|
+
"type": "improvement",
|
|
43
|
+
"title": "Mail layout buttons follow the branding accent colour",
|
|
44
|
+
"detail": "mailLayoutAppearance takes the tenant branding accent colour. When the layout uses the branding accent line, buttons in the mail body use that colour instead of the fixed dark teal. Invalid colours fall back to the previous default."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"version": "0.365.0",
|
|
48
|
+
"type": "fix",
|
|
49
|
+
"title": "A placeholder inside a button label is kept",
|
|
50
|
+
"detail": "The document schema allows placeholder chips inside a button label. Before, saving or seeding a body such as a button reading \"View {{pageName}}\" split the button: the label stopped at the placeholder and the rest landed in a new paragraph outside the button."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"version": "0.365.0",
|
|
54
|
+
"type": "improvement",
|
|
55
|
+
"title": "compileDocumentBodyToHandlebars takes a height cap for resource images",
|
|
56
|
+
"detail": "The new resourceImageMaxHeightPx option adds max-height and width:auto to resource images. The mail renderer caps images in a layout header at 64 px, so a square logo stays small while a wide wordmark keeps its 180 px width cap."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"version": "0.365.0",
|
|
60
|
+
"type": "fix",
|
|
61
|
+
"title": "resource-upload refuses key collisions between images and attachments and linking a non-active PDF template",
|
|
62
|
+
"detail": "Uploading an attachment under a key that holds an inline image, or an image under a key that holds an attachment or linked PDF, fails with template_attachment_invalid (code key_in_use) instead of silently replacing the entry. resource-link-pdf only accepts an active document-pdf template, so draft or archived templates no longer produce mails that fail at send time. When the link write fails after the file was stored, the orphaned storage object is deleted."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"version": "0.365.0",
|
|
66
|
+
"type": "fix",
|
|
67
|
+
"title": "collection kinds fail at boot; validate, diff-to-system, overview, seeding, bundle import and placeholder checks follow the write rules",
|
|
68
|
+
"detail": "createTemplateResolverFeature throws for a collection whose kind is mail, mail-layout or pdf-layout instead of failing on every save. The validate query ignores scope system for non-SystemAdmins. diff-to-system compares only against an active system template like reset-to-system. The overview reads rows in a stable order and reports truncated when its row limit is hit. Re-seeding notification templates reactivates an archived system row and applies a changed parentTemplateId. A bundle holds at most 50 locales. Dotted placeholders under a declared variable or list item field are reported as unknown because they never render a value. The document editor fallbacks are read-only and the placeholder line editor recolors chips when systemVariables change."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"version": "0.365.0",
|
|
72
|
+
"type": "fix",
|
|
73
|
+
"title": "Resource image formats come from one list",
|
|
74
|
+
"detail": "PNG, JPEG, GIF and WebP content types and extensions are defined once in the shared module and reused by the resource upload, the document body compiler and the email channel."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"version": "0.365.0",
|
|
78
|
+
"type": "breaking",
|
|
79
|
+
"title": "resource-upload, import-bundle and import-bundle-system upload in a beforeTransaction phase and can no longer run as nested writes",
|
|
80
|
+
"detail": "The three handlers now validate and upload their files before the write transaction opens and only create the fileRef and template rows inside it, so no database connection sits idle in a transaction during storage I/O. A rolled-back transaction releases the uploaded bytes. Because they now have a pre-transaction phase, the dispatcher rejects a nested ctx.write, ctx.writeAs or handler-to-handler call of them (\"has a pre-transaction phase\"). The exported storeTemplateResource is replaced by prepareTemplateResourceBeforeTransaction (beforeTransaction half) and storePreparedTemplateResource (in-transaction half).",
|
|
81
|
+
"migration": "Dispatch resource-upload, import-bundle and import-bundle-system as top-level commands, or from a perCommand handler via ctx.write. A handler that called them nested moves that call into a client or job, or becomes a perCommand handler. Code that called storeTemplateResource calls prepareTemplateResourceBeforeTransaction in a beforeTransaction phase and passes the result to storePreparedTemplateResource in the handler."
|
|
82
|
+
},
|
|
2
83
|
{
|
|
3
84
|
"version": "0.364.0",
|
|
4
85
|
"type": "fix",
|