@cosmicdrift/kumiko-bundled-features 0.371.1 → 0.372.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.
Files changed (92) hide show
  1. package/dist/auth-mfa/db/queries.d.ts +3 -2
  2. package/dist/auth-mfa/db/queries.js +2 -5
  3. package/dist/auth-mfa/schema/user-mfa.d.ts +10 -10
  4. package/dist/billing-foundation/entities.d.ts +10 -10
  5. package/dist/billing-foundation/projection.d.ts +10 -10
  6. package/dist/cap-counter/entity.d.ts +4 -4
  7. package/dist/cap-overview/web/cap-cards-panel.js +2 -1
  8. package/dist/compliance-profiles/schema/profile-selection.d.ts +4 -4
  9. package/dist/config/table.d.ts +6 -6
  10. package/dist/config/web/index.d.ts +1 -0
  11. package/dist/config/web/index.js +1 -0
  12. package/dist/crypto-shredding/handlers/forget-subject.write.js +55 -30
  13. package/dist/crypto-shredding/resolve-file-bytes-deleter.d.ts +3 -0
  14. package/dist/crypto-shredding/resolve-file-bytes-deleter.js +24 -0
  15. package/dist/custom-fields/entity.d.ts +9 -7
  16. package/dist/custom-fields/executor.d.ts +19 -1
  17. package/dist/data-retention/index.d.ts +1 -0
  18. package/dist/data-retention/index.js +1 -0
  19. package/dist/data-retention/run-retention-cleanup.js +27 -159
  20. package/dist/data-retention/schema/tenant-retention-override.d.ts +6 -6
  21. package/dist/delivery/delivery-service.d.ts +2 -1
  22. package/dist/delivery/delivery-service.js +3 -2
  23. package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +2 -2
  24. package/dist/delivery/index.d.ts +1 -1
  25. package/dist/delivery/index.js +1 -1
  26. package/dist/delivery/jobs.js +21 -7
  27. package/dist/delivery/recipient-address-seal.d.ts +13 -0
  28. package/dist/delivery/recipient-address-seal.js +53 -0
  29. package/dist/delivery/tables.d.ts +8 -7
  30. package/dist/delivery/testing.d.ts +2 -1
  31. package/dist/delivery/testing.js +1 -0
  32. package/dist/delivery/unsubscribe.d.ts +13 -2
  33. package/dist/delivery/unsubscribe.js +9 -1
  34. package/dist/document-ingest-foundation/entity.d.ts +8 -8
  35. package/dist/document-ingest-foundation/executor.d.ts +16 -1
  36. package/dist/file-foundation/read-file-ref-content.js +2 -2
  37. package/dist/folders/entity.d.ts +8 -8
  38. package/dist/folders/executor.d.ts +29 -2
  39. package/dist/form-draft/entity.d.ts +3 -3
  40. package/dist/form-draft/executor.d.ts +15 -1
  41. package/dist/inbound-mail-foundation/entities.d.ts +31 -31
  42. package/dist/inbound-mail-foundation/projection.d.ts +24 -24
  43. package/dist/jobs/job-run-table.d.ts +10 -10
  44. package/dist/ledger/entity.d.ts +21 -21
  45. package/dist/ledger/executor.d.ts +57 -3
  46. package/dist/managed-pages/table.d.ts +16 -14
  47. package/dist/notes-history/entity.d.ts +14 -14
  48. package/dist/notes-history/executor.d.ts +31 -2
  49. package/dist/personal-access-tokens/schema/api-token.d.ts +9 -9
  50. package/dist/personal-access-tokens/screens.js +14 -9
  51. package/dist/secrets/table.d.ts +2 -2
  52. package/dist/sessions/schema/user-session.d.ts +8 -8
  53. package/dist/shared/index.d.ts +2 -0
  54. package/dist/shared/index.js +2 -0
  55. package/dist/shared/purge-files-reaching-subject.d.ts +25 -0
  56. package/dist/shared/purge-files-reaching-subject.js +146 -0
  57. package/dist/shared/purge-row-file-refs.d.ts +33 -0
  58. package/dist/shared/purge-row-file-refs.js +152 -0
  59. package/dist/tags/entity.d.ts +9 -9
  60. package/dist/tags/executor.d.ts +30 -2
  61. package/dist/template-resolver/handlers/shared.d.ts +28 -1
  62. package/dist/template-resolver/table.d.ts +32 -32
  63. package/dist/template-resolver/template-preview.js +13 -4
  64. package/dist/template-resolver/user-content-table.d.ts +16 -16
  65. package/dist/template-resolver/web/client-plugin.js +2 -1
  66. package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -1
  67. package/dist/template-resolver/web/document-editor-extensions.js +2 -5
  68. package/dist/template-resolver/web/placeholder-line-editor.js +2 -2
  69. package/dist/tenant/handlers/list.query.d.ts +21 -1
  70. package/dist/tenant/invitation-table.d.ts +14 -14
  71. package/dist/tenant/membership-table.d.ts +4 -4
  72. package/dist/tenant/schema/tenant.d.ts +20 -18
  73. package/dist/tier-engine/entity.d.ts +2 -2
  74. package/dist/tier-engine/tier-resolver.d.ts +2 -2
  75. package/dist/user/handlers/me.query.d.ts +21 -0
  76. package/dist/user/schema/user.d.ts +24 -22
  77. package/dist/user-data-rights/run-export-jobs.d.ts +20 -1
  78. package/dist/user-data-rights/run-forget-cleanup.js +35 -1
  79. package/dist/user-data-rights/schema/download-attempt.d.ts +16 -16
  80. package/dist/user-data-rights/schema/download-token.d.ts +16 -16
  81. package/dist/user-data-rights/schema/export-job.d.ts +20 -20
  82. package/dist/user-data-rights/web/privacy-center-screen.js +2 -1
  83. package/dist/waitlist/entity.d.ts +24 -24
  84. package/dist/waitlist/lib.d.ts +24 -1
  85. package/package.json +11 -11
  86. package/src/crypto-shredding/changes.json +6 -0
  87. package/src/data-retention/changes.json +6 -0
  88. package/src/delivery/changes.json +13 -0
  89. package/src/files/changes.json +7 -0
  90. package/src/personal-access-tokens/changes.json +12 -0
  91. package/src/template-resolver/changes.json +6 -0
  92. package/src/user-data-rights/changes.json +6 -0
@@ -0,0 +1,152 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
+ import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
3
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
4
+ const NO_FILE_FIELDS = { singleFields: [], multiFields: [] };
5
+ // Same set as boot-validator/entity-handler.ts FILE_FIELD_TYPES; the single/multi split
6
+ // matters here because only plural fields resolve through file_refs.
7
+ export function planFileFields(fields, fieldNames) {
8
+ const singleFields = [];
9
+ const multiFields = [];
10
+ for (const [name, field] of Object.entries(fields)) {
11
+ if (fieldNames !== undefined && !fieldNames.includes(name))
12
+ continue;
13
+ if (field.type === "file" || field.type === "image")
14
+ singleFields.push(name);
15
+ else if (field.type === "files" || field.type === "images")
16
+ multiFields.push(name);
17
+ }
18
+ return singleFields.length > 0 || multiFields.length > 0
19
+ ? { singleFields, multiFields }
20
+ : NO_FILE_FIELDS;
21
+ }
22
+ // Every implicit entity table with file/image fields: an unbound fileRef
23
+ // (POST /files without entityType/entityId) can sit in single fields of
24
+ // several entity types, so the sharing check must look beyond the purged
25
+ // row's own table.
26
+ export function collectSingleFileHolders(registry) {
27
+ const holders = [];
28
+ for (const proj of registry.getAllProjections().values()) {
29
+ if (proj.isImplicit !== true || typeof proj.source !== "string" || !proj.table)
30
+ continue;
31
+ const entity = registry.getEntity(proj.source);
32
+ if (!entity)
33
+ continue;
34
+ const { singleFields } = planFileFields(entity.fields);
35
+ if (singleFields.length === 0)
36
+ continue;
37
+ holders.push({
38
+ table: proj.table,
39
+ tableHasTenantId: propertyOf(proj.table, "tenantId") !== undefined,
40
+ singleFields,
41
+ });
42
+ }
43
+ return holders;
44
+ }
45
+ // Every fileRef-id a row's file-fields reference: single fields hold the id in
46
+ // their own column, plural fields resolve via file_refs (tenantId, entityType,
47
+ // entityId, fieldName).
48
+ async function collectFileRefIds(db, tenantId, entityName, row, plan) {
49
+ const ids = new Set();
50
+ for (const field of plan.singleFields) {
51
+ const value = row[field];
52
+ if (typeof value === "string" && value.length > 0)
53
+ ids.add(value);
54
+ }
55
+ if (plan.multiFields.length > 0) {
56
+ const refRows = await selectMany(db, fileRefsTable, {
57
+ tenantId,
58
+ entityType: entityName,
59
+ entityId: String(row["id"]),
60
+ fieldName: { in: plan.multiFields },
61
+ });
62
+ for (const r of refRows)
63
+ ids.add(r.id);
64
+ }
65
+ return Array.from(ids);
66
+ }
67
+ // A fileRef is bound to a DIFFERENT entity/record (or, when only some fields are
68
+ // purged, to another field of this row) than the one being purged — its binding,
69
+ // not the row's own single-field column, decides ownership.
70
+ function isFileRefBoundElsewhere(fileRef, entityName, rowId, fieldNames) {
71
+ const entityType = fileRef["entityType"];
72
+ const entityId = fileRef["entityId"];
73
+ const fieldName = fileRef["fieldName"];
74
+ if (typeof entityType === "string" && entityType !== entityName)
75
+ return true;
76
+ if (typeof entityId === "string" && entityId !== rowId)
77
+ return true;
78
+ if (fieldNames !== undefined &&
79
+ typeof fieldName === "string" &&
80
+ !fieldNames.includes(fieldName)) {
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ // A single-field fileRef-id can be shared by more than one row, in the SAME
86
+ // table or in another entity's table; with a partial field set, also by another
87
+ // field of the same row. Purging its bytes would corrupt the holder that stays.
88
+ async function isFileRefSharedByOtherHolder(db, holders, ownTable, tenantId, rowId, fileRefId, fieldNames) {
89
+ for (const holder of holders) {
90
+ for (const field of holder.singleFields) {
91
+ const where = { [field]: fileRefId };
92
+ if (holder.table === ownTable) {
93
+ const fieldIsPurged = fieldNames === undefined || fieldNames.includes(field);
94
+ where["id"] = fieldIsPurged ? { ne: rowId } : rowId;
95
+ }
96
+ if (holder.tableHasTenantId)
97
+ where["tenantId"] = tenantId;
98
+ const others = await selectMany(db, holder.table, where, { limit: 1 });
99
+ if (others.length > 0)
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+ // Deletes the un-shared fileRefs a row's file-fields reference: bytes (incl.
106
+ // derivatives) first, then the fileRef rows — bytes before rows so a crash
107
+ // mid-way leaves an orphaned fileRef pointing at already-gone bytes (a later
108
+ // run's delete is idempotent) rather than a gone fileRef whose bytes leak forever.
109
+ // The entity row itself is the caller's business. `fieldNames` narrows the purge
110
+ // to those fields; without it every file-field of the entity is purged.
111
+ export async function purgeRowFileRefs(args) {
112
+ const rowId = String(args.row["id"]);
113
+ const plan = planFileFields(args.entity.fields, args.fieldNames);
114
+ const fileRefIds = await collectFileRefIds(args.db, args.tenantId, args.entityName, args.row, plan);
115
+ if (fileRefIds.length === 0)
116
+ return "purged";
117
+ const fileRefRows = await selectMany(args.db, fileRefsTable, {
118
+ id: { in: fileRefIds },
119
+ tenantId: args.tenantId,
120
+ });
121
+ const toDelete = [];
122
+ for (const fileRef of fileRefRows) {
123
+ if (isFileRefBoundElsewhere(fileRef, args.entityName, rowId, args.fieldNames))
124
+ continue;
125
+ const shared = await isFileRefSharedByOtherHolder(args.db, args.fileHolders, args.table, args.tenantId, rowId, String(fileRef["id"]), args.fieldNames);
126
+ if (shared)
127
+ continue;
128
+ toDelete.push(fileRef);
129
+ }
130
+ if (toDelete.length === 0)
131
+ return "purged";
132
+ if (!args.deleteBytes)
133
+ return "missing_file_storage";
134
+ let anyFailed = false;
135
+ for (const fileRef of toDelete) {
136
+ const key = fileRef["storageKey"];
137
+ if (typeof key !== "string" || key.length === 0)
138
+ continue;
139
+ const failedKeys = await args.deleteBytes(key);
140
+ if (failedKeys.length > 0) {
141
+ anyFailed = true;
142
+ args.onBytesDeleteFailed?.(failedKeys);
143
+ }
144
+ }
145
+ if (anyFailed)
146
+ return "file_delete_failed";
147
+ for (const fileRef of toDelete) {
148
+ if (!(await args.fileRefForget(String(fileRef["id"]))))
149
+ return "file_delete_failed";
150
+ }
151
+ return "purged";
152
+ }
@@ -1,6 +1,6 @@
1
1
  import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare const tagEntity: EntityDefinition<{
3
- name: {
3
+ readonly name: {
4
4
  readonly type: "text";
5
5
  readonly description?: string;
6
6
  readonly maxLength?: number;
@@ -23,7 +23,7 @@ export declare const tagEntity: EntityDefinition<{
23
23
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
24
24
  required: true;
25
25
  };
26
- color: {
26
+ readonly color: {
27
27
  readonly type: "text";
28
28
  readonly description?: string;
29
29
  readonly maxLength?: number;
@@ -46,7 +46,7 @@ export declare const tagEntity: EntityDefinition<{
46
46
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
47
47
  required: false;
48
48
  };
49
- scope: {
49
+ readonly scope: {
50
50
  readonly type: "text";
51
51
  readonly description?: string;
52
52
  readonly maxLength?: number;
@@ -71,7 +71,7 @@ export declare const tagEntity: EntityDefinition<{
71
71
  };
72
72
  }, "tenant">;
73
73
  export declare function createTagAssignmentEntity(access?: EntityDefinition["access"], parents?: readonly string[]): EntityDefinition<{
74
- tagId: {
74
+ readonly tagId: {
75
75
  readonly type: "text";
76
76
  readonly description?: string;
77
77
  readonly maxLength?: number;
@@ -94,7 +94,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
94
94
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
95
95
  required: true;
96
96
  };
97
- entityType: {
97
+ readonly entityType: {
98
98
  readonly type: "text";
99
99
  readonly description?: string;
100
100
  readonly maxLength?: number;
@@ -117,7 +117,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
117
117
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
118
118
  required: true;
119
119
  };
120
- entityId: {
120
+ readonly entityId: {
121
121
  readonly type: "text";
122
122
  readonly description?: string;
123
123
  readonly maxLength?: number;
@@ -142,7 +142,7 @@ export declare function createTagAssignmentEntity(access?: EntityDefinition["acc
142
142
  };
143
143
  }, "tenant">;
144
144
  export declare const tagAssignmentEntity: EntityDefinition<{
145
- tagId: {
145
+ readonly tagId: {
146
146
  readonly type: "text";
147
147
  readonly description?: string;
148
148
  readonly maxLength?: number;
@@ -165,7 +165,7 @@ export declare const tagAssignmentEntity: EntityDefinition<{
165
165
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
166
166
  required: true;
167
167
  };
168
- entityType: {
168
+ readonly entityType: {
169
169
  readonly type: "text";
170
170
  readonly description?: string;
171
171
  readonly maxLength?: number;
@@ -188,7 +188,7 @@ export declare const tagAssignmentEntity: EntityDefinition<{
188
188
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
189
189
  required: true;
190
190
  };
191
- entityId: {
191
+ readonly entityId: {
192
192
  readonly type: "text";
193
193
  readonly description?: string;
194
194
  readonly maxLength?: number;
@@ -1,4 +1,32 @@
1
1
  import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
- export declare const tagExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor;
3
- export declare const tagAssignmentExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor;
2
+ export declare const tagExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor<{
3
+ readonly id: string;
4
+ readonly tenantId: string;
5
+ readonly version: number;
6
+ readonly insertedAt: Temporal.Instant;
7
+ readonly modifiedAt: Temporal.Instant | null;
8
+ readonly insertedById: string | null;
9
+ readonly modifiedById: string | null;
10
+ readonly isDeleted?: boolean | undefined;
11
+ readonly deletedAt?: Temporal.Instant | null | undefined;
12
+ readonly deletedById?: string | null | undefined;
13
+ readonly color: string | null;
14
+ readonly name: string;
15
+ readonly scope: string | null;
16
+ }>;
17
+ export declare const tagAssignmentExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor<{
18
+ readonly id: string;
19
+ readonly tenantId: string;
20
+ readonly version: number;
21
+ readonly insertedAt: Temporal.Instant;
22
+ readonly modifiedAt: Temporal.Instant | null;
23
+ readonly insertedById: string | null;
24
+ readonly modifiedById: string | null;
25
+ readonly isDeleted?: boolean | undefined;
26
+ readonly deletedAt?: Temporal.Instant | null | undefined;
27
+ readonly deletedById?: string | null | undefined;
28
+ readonly entityId: string;
29
+ readonly entityType: string;
30
+ readonly tagId: string;
31
+ }>;
4
32
  export declare function createTagAssignmentExecutor(ownership: EntityDefinition["access"] | undefined): typeof tagAssignmentExecutor;
@@ -1,6 +1,33 @@
1
1
  import * as z from "zod";
2
2
  import { templateSlugSchema } from "../template-meta.ts";
3
- export declare const executor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor;
3
+ export declare const executor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor<{
4
+ readonly id: string;
5
+ readonly tenantId: string;
6
+ readonly version: number;
7
+ readonly insertedAt: Temporal.Instant;
8
+ readonly modifiedAt: Temporal.Instant | null;
9
+ readonly insertedById: string | null;
10
+ readonly modifiedById: string | null;
11
+ readonly isDeleted?: boolean | undefined;
12
+ readonly deletedAt?: Temporal.Instant | null | undefined;
13
+ readonly deletedById?: string | null | undefined;
14
+ readonly activeSince: Temporal.Instant | null;
15
+ readonly activeVersion: number | null;
16
+ readonly content: string | null;
17
+ readonly contentFormat: string;
18
+ readonly draft: string | null;
19
+ readonly folder: string | null;
20
+ readonly kind: string;
21
+ readonly linkedResources: string | null;
22
+ readonly locale: string;
23
+ readonly meta: string | null;
24
+ readonly parentTemplateId: string | null;
25
+ readonly scope: string;
26
+ readonly slug: string;
27
+ readonly status: string;
28
+ readonly title: string | null;
29
+ readonly variableSchema: string | null;
30
+ }>;
4
31
  export declare const slugSchema: z.ZodString;
5
32
  export { templateSlugSchema };
6
33
  export declare const localeSchema: z.ZodString;
@@ -1,6 +1,6 @@
1
1
  import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
3
- slug: {
3
+ readonly slug: {
4
4
  readonly type: "text";
5
5
  readonly description?: string;
6
6
  readonly maxLength?: number;
@@ -23,7 +23,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
23
23
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
24
24
  required: true;
25
25
  };
26
- kind: {
26
+ readonly kind: {
27
27
  readonly type: "select";
28
28
  readonly description?: string;
29
29
  readonly options: readonly ["mail", "document-pdf", "image-snapshot", "notification", "text-block", "snippet", "ai-prompt", "mail-layout", "pdf-layout"];
@@ -52,7 +52,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
52
52
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
53
53
  required: true;
54
54
  };
55
- locale: {
55
+ readonly locale: {
56
56
  readonly type: "text";
57
57
  readonly description?: string;
58
58
  readonly maxLength?: number;
@@ -75,7 +75,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
75
75
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
76
76
  required: true;
77
77
  };
78
- title: {
78
+ readonly title: {
79
79
  readonly type: "text";
80
80
  readonly description?: string;
81
81
  readonly maxLength?: number;
@@ -98,7 +98,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
98
98
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
99
99
  required: false;
100
100
  };
101
- folder: {
101
+ readonly folder: {
102
102
  readonly type: "text";
103
103
  readonly description?: string;
104
104
  readonly maxLength?: number;
@@ -121,7 +121,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
121
121
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
122
122
  required: false;
123
123
  };
124
- content: {
124
+ readonly content: {
125
125
  readonly type: "longText";
126
126
  readonly description?: string;
127
127
  readonly maxLength?: number;
@@ -137,7 +137,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
137
137
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
138
138
  required: false;
139
139
  };
140
- contentFormat: {
140
+ readonly contentFormat: {
141
141
  readonly type: "select";
142
142
  readonly description?: string;
143
143
  readonly options: readonly ["markdown", "mjml", "html", "plain"];
@@ -161,7 +161,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
161
161
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
162
162
  required: true;
163
163
  };
164
- variableSchema: {
164
+ readonly variableSchema: {
165
165
  readonly type: "longText";
166
166
  readonly description?: string;
167
167
  readonly maxLength?: number;
@@ -177,7 +177,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
177
177
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
178
178
  required: false;
179
179
  };
180
- linkedResources: {
180
+ readonly linkedResources: {
181
181
  readonly type: "longText";
182
182
  readonly description?: string;
183
183
  readonly maxLength?: number;
@@ -193,7 +193,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
193
193
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
194
194
  required: false;
195
195
  };
196
- meta: {
196
+ readonly meta: {
197
197
  readonly type: "longText";
198
198
  readonly description?: string;
199
199
  readonly maxLength?: number;
@@ -209,7 +209,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
209
209
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
210
210
  required: false;
211
211
  };
212
- scope: {
212
+ readonly scope: {
213
213
  readonly type: "select";
214
214
  readonly description?: string;
215
215
  readonly options: readonly ["system", "tenant"];
@@ -231,7 +231,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
231
231
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
232
232
  required: true;
233
233
  };
234
- parentTemplateId: {
234
+ readonly parentTemplateId: {
235
235
  readonly type: "text";
236
236
  readonly description?: string;
237
237
  readonly maxLength?: number;
@@ -254,7 +254,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
254
254
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
255
255
  required: false;
256
256
  };
257
- status: {
257
+ readonly status: {
258
258
  readonly type: "select";
259
259
  readonly description?: string;
260
260
  readonly options: readonly ["draft", "active", "archived"];
@@ -277,7 +277,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
277
277
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
278
278
  required: true;
279
279
  };
280
- draft: {
280
+ readonly draft: {
281
281
  readonly type: "longText";
282
282
  readonly description?: string;
283
283
  readonly maxLength?: number;
@@ -293,7 +293,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
293
293
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
294
294
  required: false;
295
295
  };
296
- activeVersion: {
296
+ readonly activeVersion: {
297
297
  readonly type: "number";
298
298
  readonly description?: string;
299
299
  readonly required?: boolean;
@@ -312,7 +312,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
312
312
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
313
313
  required: false;
314
314
  };
315
- activeSince: {
315
+ readonly activeSince: {
316
316
  readonly type: "timestamp";
317
317
  readonly description?: string;
318
318
  readonly required?: boolean;
@@ -329,7 +329,7 @@ export declare const templateResourceEntity: import("@cosmicdrift/kumiko-types/f
329
329
  };
330
330
  }, "tenant">;
331
331
  export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framework/db").EntityTable<import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
332
- slug: {
332
+ readonly slug: {
333
333
  readonly type: "text";
334
334
  readonly description?: string;
335
335
  readonly maxLength?: number;
@@ -352,7 +352,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
352
352
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
353
353
  required: true;
354
354
  };
355
- kind: {
355
+ readonly kind: {
356
356
  readonly type: "select";
357
357
  readonly description?: string;
358
358
  readonly options: readonly ["mail", "document-pdf", "image-snapshot", "notification", "text-block", "snippet", "ai-prompt", "mail-layout", "pdf-layout"];
@@ -381,7 +381,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
381
381
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
382
382
  required: true;
383
383
  };
384
- locale: {
384
+ readonly locale: {
385
385
  readonly type: "text";
386
386
  readonly description?: string;
387
387
  readonly maxLength?: number;
@@ -404,7 +404,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
404
404
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
405
405
  required: true;
406
406
  };
407
- title: {
407
+ readonly title: {
408
408
  readonly type: "text";
409
409
  readonly description?: string;
410
410
  readonly maxLength?: number;
@@ -427,7 +427,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
427
427
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
428
428
  required: false;
429
429
  };
430
- folder: {
430
+ readonly folder: {
431
431
  readonly type: "text";
432
432
  readonly description?: string;
433
433
  readonly maxLength?: number;
@@ -450,7 +450,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
450
450
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
451
451
  required: false;
452
452
  };
453
- content: {
453
+ readonly content: {
454
454
  readonly type: "longText";
455
455
  readonly description?: string;
456
456
  readonly maxLength?: number;
@@ -466,7 +466,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
466
466
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
467
467
  required: false;
468
468
  };
469
- contentFormat: {
469
+ readonly contentFormat: {
470
470
  readonly type: "select";
471
471
  readonly description?: string;
472
472
  readonly options: readonly ["markdown", "mjml", "html", "plain"];
@@ -490,7 +490,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
490
490
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
491
491
  required: true;
492
492
  };
493
- variableSchema: {
493
+ readonly variableSchema: {
494
494
  readonly type: "longText";
495
495
  readonly description?: string;
496
496
  readonly maxLength?: number;
@@ -506,7 +506,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
506
506
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
507
507
  required: false;
508
508
  };
509
- linkedResources: {
509
+ readonly linkedResources: {
510
510
  readonly type: "longText";
511
511
  readonly description?: string;
512
512
  readonly maxLength?: number;
@@ -522,7 +522,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
522
522
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
523
523
  required: false;
524
524
  };
525
- meta: {
525
+ readonly meta: {
526
526
  readonly type: "longText";
527
527
  readonly description?: string;
528
528
  readonly maxLength?: number;
@@ -538,7 +538,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
538
538
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
539
539
  required: false;
540
540
  };
541
- scope: {
541
+ readonly scope: {
542
542
  readonly type: "select";
543
543
  readonly description?: string;
544
544
  readonly options: readonly ["system", "tenant"];
@@ -560,7 +560,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
560
560
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
561
561
  required: true;
562
562
  };
563
- parentTemplateId: {
563
+ readonly parentTemplateId: {
564
564
  readonly type: "text";
565
565
  readonly description?: string;
566
566
  readonly maxLength?: number;
@@ -583,7 +583,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
583
583
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
584
584
  required: false;
585
585
  };
586
- status: {
586
+ readonly status: {
587
587
  readonly type: "select";
588
588
  readonly description?: string;
589
589
  readonly options: readonly ["draft", "active", "archived"];
@@ -606,7 +606,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
606
606
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
607
607
  required: true;
608
608
  };
609
- draft: {
609
+ readonly draft: {
610
610
  readonly type: "longText";
611
611
  readonly description?: string;
612
612
  readonly maxLength?: number;
@@ -622,7 +622,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
622
622
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
623
623
  required: false;
624
624
  };
625
- activeVersion: {
625
+ readonly activeVersion: {
626
626
  readonly type: "number";
627
627
  readonly description?: string;
628
628
  readonly required?: boolean;
@@ -641,7 +641,7 @@ export declare const templateResourcesTable: import("@cosmicdrift/kumiko-framewo
641
641
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
642
642
  required: false;
643
643
  };
644
- activeSince: {
644
+ readonly activeSince: {
645
645
  readonly type: "timestamp";
646
646
  readonly description?: string;
647
647
  readonly required?: boolean;
@@ -20,9 +20,15 @@ import { isMailTemplateMeta, parseTemplateMeta, readPdfTemplateMeta, } from "./t
20
20
  const SAMPLE_LINK = "https://example.com";
21
21
  const SAMPLE_DATE = "2026-01-15";
22
22
  const SAMPLE_LIST_ROWS = 3;
23
+ function isTranslatableExample(def, example) {
24
+ return (def.type === "text" || def.type === "richText") && typeof example === "string";
25
+ }
26
+ function exampleFor(def, example, labelText) {
27
+ return isTranslatableExample(def, example) ? labelText(example) : example;
28
+ }
23
29
  function sampleScalar(name, def, labelText) {
24
30
  if (def.example !== undefined)
25
- return def.example;
31
+ return exampleFor(def, def.example, labelText);
26
32
  const label = def.label === undefined ? name : labelText(def.label);
27
33
  switch (def.type) {
28
34
  case "text":
@@ -41,9 +47,12 @@ function sampleScalar(name, def, labelText) {
41
47
  }
42
48
  function sampleRow(item, labelText, given = {}) {
43
49
  const row = {};
44
- for (const [name, def] of Object.entries(item))
45
- row[name] = sampleScalar(name, def, labelText);
46
- return { ...row, ...given };
50
+ for (const [name, def] of Object.entries(item)) {
51
+ row[name] = Object.hasOwn(given, name)
52
+ ? exampleFor(def, given[name], labelText)
53
+ : sampleScalar(name, def, labelText);
54
+ }
55
+ return { ...given, ...row };
47
56
  }
48
57
  function sampleList(def, labelText) {
49
58
  const rows = (def.example ?? [])