@cosmicdrift/kumiko-bundled-features 0.371.0 → 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
@@ -1,6 +1,6 @@
1
1
  import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare function createNoteEntryEntity(access?: EntityDefinition["access"], parents?: readonly string[]): EntityDefinition<{
3
- entityType: {
3
+ readonly entityType: {
4
4
  readonly type: "text";
5
5
  readonly description?: string;
6
6
  readonly maxLength?: number;
@@ -23,7 +23,7 @@ export declare function createNoteEntryEntity(access?: EntityDefinition["access"
23
23
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
24
24
  required: true;
25
25
  };
26
- entityId: {
26
+ readonly entityId: {
27
27
  readonly type: "text";
28
28
  readonly description?: string;
29
29
  readonly maxLength?: number;
@@ -46,7 +46,7 @@ export declare function createNoteEntryEntity(access?: EntityDefinition["access"
46
46
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
47
47
  required: true;
48
48
  };
49
- authorId: {
49
+ readonly authorId: {
50
50
  readonly type: "text";
51
51
  readonly description?: string;
52
52
  readonly maxLength?: number;
@@ -69,7 +69,7 @@ export declare function createNoteEntryEntity(access?: EntityDefinition["access"
69
69
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
70
70
  required: false;
71
71
  };
72
- authorName: {
72
+ readonly authorName: {
73
73
  readonly type: "text";
74
74
  readonly description?: string;
75
75
  readonly maxLength?: number;
@@ -92,7 +92,7 @@ export declare function createNoteEntryEntity(access?: EntityDefinition["access"
92
92
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
93
93
  required: false;
94
94
  };
95
- body: {
95
+ readonly body: {
96
96
  readonly type: "longText";
97
97
  readonly description?: string;
98
98
  readonly maxLength?: number;
@@ -110,7 +110,7 @@ export declare function createNoteEntryEntity(access?: EntityDefinition["access"
110
110
  };
111
111
  }, "tenant">;
112
112
  export declare const noteEntryEntity: EntityDefinition<{
113
- entityType: {
113
+ readonly entityType: {
114
114
  readonly type: "text";
115
115
  readonly description?: string;
116
116
  readonly maxLength?: number;
@@ -133,7 +133,7 @@ export declare const noteEntryEntity: EntityDefinition<{
133
133
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
134
134
  required: true;
135
135
  };
136
- entityId: {
136
+ readonly entityId: {
137
137
  readonly type: "text";
138
138
  readonly description?: string;
139
139
  readonly maxLength?: number;
@@ -156,7 +156,7 @@ export declare const noteEntryEntity: EntityDefinition<{
156
156
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
157
157
  required: true;
158
158
  };
159
- authorId: {
159
+ readonly authorId: {
160
160
  readonly type: "text";
161
161
  readonly description?: string;
162
162
  readonly maxLength?: number;
@@ -179,7 +179,7 @@ export declare const noteEntryEntity: EntityDefinition<{
179
179
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
180
180
  required: false;
181
181
  };
182
- authorName: {
182
+ readonly authorName: {
183
183
  readonly type: "text";
184
184
  readonly description?: string;
185
185
  readonly maxLength?: number;
@@ -202,7 +202,7 @@ export declare const noteEntryEntity: EntityDefinition<{
202
202
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
203
203
  required: false;
204
204
  };
205
- body: {
205
+ readonly body: {
206
206
  readonly type: "longText";
207
207
  readonly description?: string;
208
208
  readonly maxLength?: number;
@@ -220,7 +220,7 @@ export declare const noteEntryEntity: EntityDefinition<{
220
220
  };
221
221
  }, "tenant">;
222
222
  export declare function createNoteMentionEntity(): EntityDefinition<{
223
- noteId: {
223
+ readonly noteId: {
224
224
  readonly type: "text";
225
225
  readonly description?: string;
226
226
  readonly maxLength?: number;
@@ -243,7 +243,7 @@ export declare function createNoteMentionEntity(): EntityDefinition<{
243
243
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
244
244
  required: true;
245
245
  };
246
- subjectId: {
246
+ readonly subjectId: {
247
247
  readonly type: "text";
248
248
  readonly description?: string;
249
249
  readonly maxLength?: number;
@@ -268,7 +268,7 @@ export declare function createNoteMentionEntity(): EntityDefinition<{
268
268
  };
269
269
  }, "tenant">;
270
270
  export declare const noteMentionEntity: EntityDefinition<{
271
- noteId: {
271
+ readonly noteId: {
272
272
  readonly type: "text";
273
273
  readonly description?: string;
274
274
  readonly maxLength?: number;
@@ -291,7 +291,7 @@ export declare const noteMentionEntity: EntityDefinition<{
291
291
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
292
292
  required: true;
293
293
  };
294
- subjectId: {
294
+ readonly subjectId: {
295
295
  readonly type: "text";
296
296
  readonly description?: string;
297
297
  readonly maxLength?: number;
@@ -1,4 +1,33 @@
1
1
  import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
- export declare const noteEntryExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor, noteEntryTable: import("@cosmicdrift/kumiko-framework/db").EntityTable;
2
+ export declare const noteEntryExecutor: 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 authorId: string | null;
14
+ readonly authorName: string | null;
15
+ readonly body: string;
16
+ readonly entityId: string;
17
+ readonly entityType: string;
18
+ }>, noteEntryTable: import("@cosmicdrift/kumiko-framework/db").EntityTable;
3
19
  export declare function createNoteEntryExecutor(ownership: EntityDefinition["access"] | undefined): typeof noteEntryExecutor;
4
- export declare const noteMentionExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor, noteMentionTable: import("@cosmicdrift/kumiko-framework/db").EntityTable;
20
+ export declare const noteMentionExecutor: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor<{
21
+ readonly id: string;
22
+ readonly tenantId: string;
23
+ readonly version: number;
24
+ readonly insertedAt: Temporal.Instant;
25
+ readonly modifiedAt: Temporal.Instant | null;
26
+ readonly insertedById: string | null;
27
+ readonly modifiedById: string | null;
28
+ readonly isDeleted?: boolean | undefined;
29
+ readonly deletedAt?: Temporal.Instant | null | undefined;
30
+ readonly deletedById?: string | null | undefined;
31
+ readonly noteId: string;
32
+ readonly subjectId: string;
33
+ }>, noteMentionTable: import("@cosmicdrift/kumiko-framework/db").EntityTable;
@@ -1,5 +1,5 @@
1
1
  export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
2
- userId: {
2
+ readonly userId: {
3
3
  readonly type: "text";
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
@@ -22,7 +22,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
22
22
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
23
23
  required: true;
24
24
  };
25
- tenantId: {
25
+ readonly tenantId: {
26
26
  readonly type: "text";
27
27
  readonly description?: string;
28
28
  readonly maxLength?: number;
@@ -45,7 +45,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
45
45
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
46
46
  required: true;
47
47
  };
48
- name: {
48
+ readonly name: {
49
49
  readonly type: "text";
50
50
  readonly description?: string;
51
51
  readonly maxLength?: number;
@@ -68,7 +68,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
68
68
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
69
69
  required: true;
70
70
  };
71
- tokenHash: {
71
+ readonly tokenHash: {
72
72
  readonly type: "text";
73
73
  readonly description?: string;
74
74
  readonly maxLength?: number;
@@ -91,7 +91,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
91
91
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
92
92
  required: true;
93
93
  };
94
- prefix: {
94
+ readonly prefix: {
95
95
  readonly type: "text";
96
96
  readonly description?: string;
97
97
  readonly maxLength?: number;
@@ -114,7 +114,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
114
114
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
115
115
  required: true;
116
116
  };
117
- scopes: {
117
+ readonly scopes: {
118
118
  readonly type: "text";
119
119
  readonly description?: string;
120
120
  readonly maxLength?: number;
@@ -137,7 +137,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
137
137
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
138
138
  required: true;
139
139
  };
140
- createdAt: {
140
+ readonly createdAt: {
141
141
  readonly type: "timestamp";
142
142
  readonly description?: string;
143
143
  readonly required?: boolean;
@@ -152,7 +152,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
152
152
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
153
153
  required: true;
154
154
  };
155
- expiresAt: {
155
+ readonly expiresAt: {
156
156
  readonly type: "timestamp";
157
157
  readonly description?: string;
158
158
  readonly required?: boolean;
@@ -167,7 +167,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
167
167
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
168
168
  required: false;
169
169
  };
170
- revokedAt: {
170
+ readonly revokedAt: {
171
171
  readonly type: "timestamp";
172
172
  readonly description?: string;
173
173
  readonly required?: boolean;
@@ -13,6 +13,14 @@ const PAT_SCREEN_VISIBLE_WHEN = {
13
13
  field: "enabled",
14
14
  eq: true,
15
15
  };
16
+ const PAT_CREATE_ACTION = {
17
+ kind: "navigate",
18
+ id: "create",
19
+ label: i18nKey("pat.create.title"),
20
+ screen: PAT_MINT_SCREEN_ID,
21
+ style: "primary",
22
+ icon: "plus",
23
+ };
16
24
  export const patListScreen = {
17
25
  id: PAT_SCREEN_ID,
18
26
  type: "projectionList",
@@ -63,15 +71,12 @@ export const patListScreen = {
63
71
  visible: { field: "status", ne: "revoked" },
64
72
  },
65
73
  ],
66
- toolbarActions: [
67
- {
68
- kind: "navigate",
69
- id: "create",
70
- label: i18nKey("pat.create.title"),
71
- screen: PAT_MINT_SCREEN_ID,
72
- style: "primary",
73
- },
74
- ],
74
+ toolbarActions: [PAT_CREATE_ACTION],
75
+ emptyState: {
76
+ title: i18nKey("kumiko.list.empty.title"),
77
+ description: i18nKey("kumiko.list.empty.hint"),
78
+ action: PAT_CREATE_ACTION,
79
+ },
75
80
  access: {
76
81
  openToAll: {
77
82
  reason: "each signed-in user views only their own personal access tokens, mirroring the list handler",
@@ -4,7 +4,7 @@ export type StoredMetadata = {
4
4
  readonly hint?: string;
5
5
  };
6
6
  export declare const tenantSecretEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
7
- key: {
7
+ readonly key: {
8
8
  readonly type: "text";
9
9
  readonly description?: string;
10
10
  readonly maxLength?: number;
@@ -27,7 +27,7 @@ export declare const tenantSecretEntity: import("@cosmicdrift/kumiko-types/field
27
27
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
28
28
  required: true;
29
29
  };
30
- kekVersion: {
30
+ readonly kekVersion: {
31
31
  readonly type: "number";
32
32
  readonly description?: string;
33
33
  readonly required?: boolean;
@@ -1,5 +1,5 @@
1
1
  export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
2
- userId: {
2
+ readonly userId: {
3
3
  readonly type: "text";
4
4
  readonly description?: string;
5
5
  readonly maxLength?: number;
@@ -22,7 +22,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
22
22
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
23
23
  required: true;
24
24
  };
25
- tenantId: {
25
+ readonly tenantId: {
26
26
  readonly type: "text";
27
27
  readonly description?: string;
28
28
  readonly maxLength?: number;
@@ -45,7 +45,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
45
45
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
46
46
  required: true;
47
47
  };
48
- createdAt: {
48
+ readonly createdAt: {
49
49
  readonly type: "timestamp";
50
50
  readonly description?: string;
51
51
  readonly required?: boolean;
@@ -60,7 +60,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
60
60
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
61
61
  required: true;
62
62
  };
63
- expiresAt: {
63
+ readonly expiresAt: {
64
64
  readonly type: "timestamp";
65
65
  readonly description?: string;
66
66
  readonly required?: boolean;
@@ -75,7 +75,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
75
75
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
76
76
  required: true;
77
77
  };
78
- revokedAt: {
78
+ readonly revokedAt: {
79
79
  readonly type: "timestamp";
80
80
  readonly description?: string;
81
81
  readonly required?: boolean;
@@ -90,7 +90,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
90
90
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
91
91
  required: false;
92
92
  };
93
- lastSeenAt: {
93
+ readonly lastSeenAt: {
94
94
  readonly type: "timestamp";
95
95
  readonly description?: string;
96
96
  readonly required?: boolean;
@@ -105,7 +105,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
105
105
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
106
106
  required: false;
107
107
  };
108
- ip: {
108
+ readonly ip: {
109
109
  readonly type: "text";
110
110
  readonly description?: string;
111
111
  readonly maxLength?: number;
@@ -128,7 +128,7 @@ export declare const userSessionEntity: import("@cosmicdrift/kumiko-types/fields
128
128
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
129
129
  required: false;
130
130
  };
131
- userAgent: {
131
+ readonly userAgent: {
132
132
  readonly type: "text";
133
133
  readonly description?: string;
134
134
  readonly maxLength?: number;
@@ -14,6 +14,8 @@ export { mailHeading, mailParagraph, mailRepeatedRows } from "./mail-template-bo
14
14
  export { mapWithConcurrency } from "./map-with-concurrency.ts";
15
15
  export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.ts";
16
16
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.ts";
17
+ export { type DeleteFileBytes, FileSweepError, type MissingStoragePolicy, purgeFilesReachingSubject, } from "./purge-files-reaching-subject.ts";
18
+ export { collectSingleFileHolders, type FileFieldPlan, type FileRefPurgeOutcome, planFileFields, purgeRowFileRefs, type SingleFileHolder, } from "./purge-row-file-refs.ts";
17
19
  export { rateLimitedTextResponse } from "./rate-limited-text-response.ts";
18
20
  export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.ts";
19
21
  export { REDACTED_PII_PLACEHOLDER, redactPiiCiphertext } from "./redact-pii-ciphertext.ts";
@@ -14,6 +14,8 @@ export { mailHeading, mailParagraph, mailRepeatedRows } from "./mail-template-bo
14
14
  export { mapWithConcurrency } from "./map-with-concurrency.js";
15
15
  export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
16
16
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
17
+ export { FileSweepError, purgeFilesReachingSubject, } from "./purge-files-reaching-subject.js";
18
+ export { collectSingleFileHolders, planFileFields, purgeRowFileRefs, } from "./purge-row-file-refs.js";
17
19
  export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
18
20
  export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
19
21
  export { REDACTED_PII_PLACEHOLDER, redactPiiCiphertext } from "./redact-pii-ciphertext.js";
@@ -0,0 +1,25 @@
1
+ import { type SubjectId } from "@cosmicdrift/kumiko-framework/crypto";
2
+ import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
+ import { type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { type FileRefPurgeOutcome } from "./purge-row-file-refs.ts";
5
+ export declare class FileSweepError extends Error {
6
+ readonly code: Exclude<FileRefPurgeOutcome, "purged">;
7
+ readonly entityName: string;
8
+ constructor(code: Exclude<FileRefPurgeOutcome, "purged">, entityName: string);
9
+ }
10
+ export type DeleteFileBytes = (storageKey: string) => Promise<readonly string[]>;
11
+ export type MissingStoragePolicy = {
12
+ readonly mode: "fail";
13
+ } | {
14
+ readonly mode: "skip";
15
+ readonly warn: (tenantId: TenantId) => void;
16
+ };
17
+ export declare function purgeFilesReachingSubject(args: {
18
+ readonly registry: Registry;
19
+ readonly db: DbRunner;
20
+ readonly subject: SubjectId;
21
+ readonly subjectKey: string;
22
+ readonly recordTenantId?: TenantId;
23
+ readonly resolveDeleteBytes: (tenantId: TenantId) => Promise<DeleteFileBytes | undefined>;
24
+ readonly missingStorage: MissingStoragePolicy;
25
+ }): Promise<void>;
@@ -0,0 +1,146 @@
1
+ import { resolveSubjectForField, SubjectResolutionError, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
2
+ import { createEventStoreExecutor, createTenantDb, selectMany, } from "@cosmicdrift/kumiko-framework/db";
3
+ import { createSystemUser, parseTenantId, } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { fileRefEntity, fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
5
+ import { collectSingleFileHolders, planFileFields, purgeRowFileRefs, } from "./purge-row-file-refs.js";
6
+ // Callers' error labels read err.code, so audit events name the failure class.
7
+ export class FileSweepError extends Error {
8
+ code;
9
+ entityName;
10
+ constructor(code, entityName) {
11
+ super(`[files-purge] ${code} while purging files of entity ${entityName}`);
12
+ this.code = code;
13
+ this.entityName = entityName;
14
+ this.name = "FileSweepError";
15
+ }
16
+ }
17
+ function collectFileEntities(registry) {
18
+ const found = [];
19
+ for (const proj of registry.getAllProjections().values()) {
20
+ if (proj.isImplicit !== true || typeof proj.source !== "string" || !proj.table)
21
+ continue;
22
+ const entity = registry.getEntity(proj.source);
23
+ if (!entity)
24
+ continue;
25
+ const { singleFields, multiFields } = planFileFields(entity.fields);
26
+ const fileFields = [...singleFields, ...multiFields];
27
+ if (fileFields.length === 0)
28
+ continue;
29
+ found.push({ entityName: proj.source, entity, table: proj.table, fileFields });
30
+ }
31
+ return found;
32
+ }
33
+ // Candidate rows only; the exact per-field subject is verified with resolveSubjectForField,
34
+ // the same resolver that encrypted the field.
35
+ function candidateWheres(subject, recordTenantId, { entityName, entity, fileFields }) {
36
+ if (subject.kind === "record") {
37
+ // Without the owning tenant the record cannot be told apart from a same-id row elsewhere.
38
+ return entityName === subject.entity && recordTenantId !== undefined
39
+ ? [{ id: subject.id, tenantId: recordTenantId }]
40
+ : [];
41
+ }
42
+ if (subject.kind !== "user")
43
+ return [];
44
+ const wheres = [];
45
+ for (const fieldName of fileFields) {
46
+ const field = entity.fields[fieldName];
47
+ if (field && "userOwned" in field && field.userOwned !== undefined) {
48
+ wheres.push({ [field.userOwned.ownerField]: subject.userId });
49
+ }
50
+ else if (field && "pii" in field && field.pii === true && entity.idType !== "serial") {
51
+ wheres.push({ id: subject.userId });
52
+ }
53
+ }
54
+ return wheres;
55
+ }
56
+ function fieldsResolvingToSubject(fileEntity, row, rowTenantId, subjectKey) {
57
+ return fileEntity.fileFields.filter((fieldName) => {
58
+ try {
59
+ const resolved = resolveSubjectForField(fileEntity.entity, fieldName, row, {
60
+ tenantId: rowTenantId,
61
+ entityName: fileEntity.entityName,
62
+ });
63
+ return resolved !== null && subjectIdToKey(resolved) === subjectKey;
64
+ }
65
+ catch (err) {
66
+ // skip: a field whose owner cannot be resolved on this row never encrypted under this subject
67
+ if (err instanceof SubjectResolutionError)
68
+ return false;
69
+ throw err;
70
+ }
71
+ });
72
+ }
73
+ const fileRefExecutor = createEventStoreExecutor(fileRefsTable, fileRefEntity, {
74
+ entityName: "fileRef",
75
+ });
76
+ async function deleteBytesFailingClosed(deleteBytes, storageKey) {
77
+ try {
78
+ return await deleteBytes(storageKey);
79
+ }
80
+ catch {
81
+ // skip: an unsafe key or missing list permission fails this file closed, not the whole sweep
82
+ return [storageKey];
83
+ }
84
+ }
85
+ // Forgetting a subject erases keys; the files behind a personal file field are not ciphertext,
86
+ // so they need their own purge (bytes, derivatives, fileRef rows). The entity row stays:
87
+ // forgetting is crypto-shredding, not deletion. Tenant subjects are covered by
88
+ // files-tenant-data on tenant destroy. The storage is resolved per owning tenant because a
89
+ // subject's rows can live in any tenant.
90
+ export async function purgeFilesReachingSubject(args) {
91
+ const { registry, db, subject, subjectKey, recordTenantId, resolveDeleteBytes, missingStorage } = args;
92
+ // skip: tenant subjects are covered by files-tenant-data on tenant destroy
93
+ if (subject.kind === "tenant")
94
+ return;
95
+ const fileHolders = collectSingleFileHolders(registry);
96
+ const storageByTenant = new Map();
97
+ let firstFailure;
98
+ for (const fileEntity of collectFileEntities(registry)) {
99
+ const seenRowIds = new Set();
100
+ for (const where of candidateWheres(subject, recordTenantId, fileEntity)) {
101
+ const rows = await selectMany(db, fileEntity.table, where);
102
+ for (const row of rows) {
103
+ const rowId = String(row["id"]);
104
+ const tenantId = parseTenantId(row["tenantId"]);
105
+ // skip: a row without a tenant cannot own tenant-scoped file refs
106
+ if (seenRowIds.has(rowId) || tenantId === null)
107
+ continue;
108
+ seenRowIds.add(rowId);
109
+ const fieldNames = fieldsResolvingToSubject(fileEntity, row, tenantId, subjectKey);
110
+ // skip: none of the row's file fields belongs to this subject
111
+ if (fieldNames.length === 0)
112
+ continue;
113
+ let storagePromise = storageByTenant.get(tenantId);
114
+ if (storagePromise === undefined) {
115
+ storagePromise = resolveDeleteBytes(tenantId);
116
+ storageByTenant.set(tenantId, storagePromise);
117
+ }
118
+ const deleteBytes = await storagePromise;
119
+ const systemUser = createSystemUser(tenantId);
120
+ const tdb = createTenantDb({ db, tenantId, mode: "system" });
121
+ const outcome = await purgeRowFileRefs({
122
+ db,
123
+ tenantId,
124
+ entityName: fileEntity.entityName,
125
+ entity: fileEntity.entity,
126
+ table: fileEntity.table,
127
+ fileHolders,
128
+ row,
129
+ fieldNames,
130
+ deleteBytes: deleteBytes
131
+ ? (key) => deleteBytesFailingClosed(deleteBytes, key)
132
+ : undefined,
133
+ fileRefForget: async (fileRefId) => (await fileRefExecutor.forget({ id: fileRefId }, systemUser, tdb)).isSuccess,
134
+ });
135
+ if (outcome === "missing_file_storage" && missingStorage.mode === "skip") {
136
+ missingStorage.warn(tenantId);
137
+ }
138
+ else if (outcome !== "purged") {
139
+ firstFailure ??= new FileSweepError(outcome, fileEntity.entityName);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ if (firstFailure)
145
+ throw firstFailure;
146
+ }
@@ -0,0 +1,33 @@
1
+ import { type DbRunner, selectMany } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { EntityDefinition, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
+ type EntityFields = EntityDefinition["fields"];
4
+ type EntityTable = Parameters<typeof selectMany>[1];
5
+ export type FileFieldPlan = {
6
+ /** type file|image: the fileRef-id lives directly in the entity column. */
7
+ readonly singleFields: readonly string[];
8
+ /** type files|images: resolved via file_refs (bound by entityType/entityId/fieldName). */
9
+ readonly multiFields: readonly string[];
10
+ };
11
+ export type SingleFileHolder = {
12
+ readonly table: EntityTable;
13
+ readonly tableHasTenantId: boolean;
14
+ readonly singleFields: readonly string[];
15
+ };
16
+ export type FileRefPurgeOutcome = "purged" | "missing_file_storage" | "file_delete_failed";
17
+ export declare function planFileFields(fields: EntityFields, fieldNames?: readonly string[]): FileFieldPlan;
18
+ export declare function collectSingleFileHolders(registry: Registry): readonly SingleFileHolder[];
19
+ export declare function purgeRowFileRefs(args: {
20
+ readonly db: DbRunner;
21
+ readonly tenantId: TenantId;
22
+ readonly entityName: string;
23
+ readonly entity: EntityDefinition;
24
+ readonly table: EntityTable;
25
+ readonly fileHolders: readonly SingleFileHolder[];
26
+ readonly row: Record<string, unknown>;
27
+ readonly fieldNames?: readonly string[];
28
+ /** Returns the keys whose delete failed; undefined = no storage reachable. */
29
+ readonly deleteBytes: ((storageKey: string) => Promise<readonly string[]>) | undefined;
30
+ readonly fileRefForget: (fileRefId: string) => Promise<boolean>;
31
+ readonly onBytesDeleteFailed?: (failedKeys: readonly string[]) => void;
32
+ }): Promise<FileRefPurgeOutcome>;
33
+ export {};