@cosmicdrift/kumiko-bundled-features 0.382.0 → 0.383.1

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 (57) hide show
  1. package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
  2. package/dist/audit/run-escape-hatch-retention.js +54 -27
  3. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -6
  4. package/dist/billing-foundation/entities.d.ts +8 -8
  5. package/dist/billing-foundation/projection.d.ts +8 -8
  6. package/dist/cap-counter/entity.d.ts +1 -1
  7. package/dist/cap-overview/handlers/tenant-caps-list.query.js +21 -6
  8. package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +6 -4
  9. package/dist/config/table.d.ts +6 -6
  10. package/dist/custom-fields/entity.d.ts +4 -4
  11. package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -4
  12. package/dist/delivery/attempt-log.d.ts +2 -0
  13. package/dist/delivery/attempt-log.js +12 -1
  14. package/dist/delivery/delivery-service.js +8 -3
  15. package/dist/delivery/feature.js +7 -9
  16. package/dist/delivery/mask-recipient-address.d.ts +1 -0
  17. package/dist/delivery/mask-recipient-address.js +16 -0
  18. package/dist/delivery/tables.d.ts +6 -6
  19. package/dist/document-ingest-foundation/entity.d.ts +4 -4
  20. package/dist/folders/entity.d.ts +8 -8
  21. package/dist/form-draft/entity.d.ts +2 -2
  22. package/dist/inbound-mail-foundation/entities.d.ts +25 -25
  23. package/dist/inbound-mail-foundation/projection.d.ts +20 -20
  24. package/dist/jobs/job-run-logger.d.ts +1 -1
  25. package/dist/jobs/job-run-logger.js +20 -0
  26. package/dist/jobs/job-run-table.d.ts +6 -6
  27. package/dist/ledger/entity.d.ts +13 -13
  28. package/dist/managed-pages/table.d.ts +12 -12
  29. package/dist/notes-history/entity.d.ts +12 -12
  30. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -6
  31. package/dist/secrets/table.d.ts +1 -1
  32. package/dist/sessions/schema/user-session.d.ts +4 -4
  33. package/dist/sessions/session-callbacks.js +23 -16
  34. package/dist/tags/entity.d.ts +9 -9
  35. package/dist/template-resolver/table.d.ts +10 -10
  36. package/dist/template-resolver/user-content-table.d.ts +10 -10
  37. package/dist/tenant/decrypt-tenant-names.d.ts +4 -0
  38. package/dist/tenant/decrypt-tenant-names.js +15 -0
  39. package/dist/tenant/handlers/tenant-directory.query.js +6 -7
  40. package/dist/tenant/invitation-table.d.ts +6 -6
  41. package/dist/tenant/membership-table.d.ts +4 -4
  42. package/dist/tenant/schema/tenant.d.ts +6 -6
  43. package/dist/tier-engine/entity.d.ts +1 -1
  44. package/dist/tier-engine/tier-resolver.d.ts +1 -1
  45. package/dist/user/schema/user.d.ts +12 -12
  46. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -14
  47. package/dist/user-data-rights/schema/download-token.d.ts +8 -8
  48. package/dist/user-data-rights/schema/export-job.d.ts +6 -6
  49. package/dist/waitlist/entity.d.ts +14 -14
  50. package/package.json +9 -9
  51. package/src/audit/changes.json +6 -0
  52. package/src/auth-email-password/changes.json +6 -0
  53. package/src/cap-overview/changes.json +6 -0
  54. package/src/delivery/changes.json +6 -0
  55. package/src/jobs/changes.json +7 -0
  56. package/src/sessions/changes.json +6 -0
  57. package/src/tenant/changes.json +6 -0
@@ -7,6 +7,7 @@ export declare function runEscapeHatchRetention(args: {
7
7
  readonly configResolver: ConfigResolver | undefined;
8
8
  readonly userId: string;
9
9
  readonly batchSize?: number;
10
+ readonly scanPageSize?: number;
10
11
  }): Promise<{
11
12
  readonly deletedCount: number;
12
13
  }>;
@@ -32,6 +32,7 @@ async function resolveExplicitRetentionDays(args) {
32
32
  const COMPLIANCE_PROFILE_ENTITY = "tenant-compliance-profile";
33
33
  const PRUNE_CHUNK_SIZE = 1000;
34
34
  const PRUNE_BATCH_SIZE = 5000;
35
+ const SCAN_PAGE_SIZE = 2000;
35
36
  // Overrides may only lengthen retention: the earlier cutoff (same `now`) is the longer period.
36
37
  function earlierInstant(a, b) {
37
38
  return Temporal.Instant.compare(a, b) <= 0 ? a : b;
@@ -49,14 +50,32 @@ function targetTenantOf(payload) {
49
50
  const parsed = escapeHatchUsedSchema.safeParse(payload);
50
51
  return parsed.success ? parseTenantId(parsed.data.targetTenantId) : null;
51
52
  }
52
- async function storingTenantIds(db) {
53
- const groups = await aggregateWhere(db, eventsTable, { measure: { fn: "count" }, groupBy: [{ field: "tenantId" }] }, { aggregateType: ESCAPE_HATCH_USE_AGGREGATE_TYPE });
53
+ // No tenant's cutoff can be later than the shortest built-in retention: overrides only lengthen it.
54
+ function latestPossibleCutoff(now) {
55
+ const cutoffs = Object.values(COMPLIANCE_PROFILES).map((profile) => subtractRetentionSpec(now, profile.auditLog.retention));
56
+ return cutoffs.reduce((latest, cutoff) => Temporal.Instant.compare(cutoff, latest) > 0 ? cutoff : latest);
57
+ }
58
+ async function storingTenantIds(db, expirableBefore) {
59
+ const groups = await aggregateWhere(db, eventsTable, { measure: { fn: "count" }, groupBy: [{ field: "tenantId" }] }, { aggregateType: ESCAPE_HATCH_USE_AGGREGATE_TYPE, createdAt: { lt: expirableBefore } });
54
60
  return groups.flatMap((group) => {
55
61
  const tenantId = parseTenantId(group.keys[0]);
56
62
  return tenantId ? [tenantId] : [];
57
63
  });
58
64
  }
59
- async function pruneByTenantProfiles(db, operatorRetentionDays) {
65
+ async function pruneAggregates(db, aggregateIds) {
66
+ const now = getTemporal().Now.instant();
67
+ let deletedCount = 0;
68
+ for (let i = 0; i < aggregateIds.length; i += PRUNE_CHUNK_SIZE) {
69
+ const result = await pruneEvents(db, {
70
+ aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
71
+ aggregateIds: aggregateIds.slice(i, i + PRUNE_CHUNK_SIZE),
72
+ olderThan: now,
73
+ });
74
+ deletedCount += result.deletedCount;
75
+ }
76
+ return deletedCount;
77
+ }
78
+ async function pruneByTenantProfiles(db, operatorRetentionDays, scanPageSize) {
60
79
  const now = getTemporal().Now.instant();
61
80
  const operatorFloor = operatorRetentionDays === undefined
62
81
  ? undefined
@@ -72,33 +91,39 @@ async function pruneByTenantProfiles(db, operatorRetentionDays) {
72
91
  };
73
92
  // The effective cutoff is never later than the storing tenant's, so only rows older than
74
93
  // that one can expire; everything younger stays in the database.
75
- const expiredAggregateIds = [];
76
- for (const storingTenantId of await storingTenantIds(db)) {
94
+ let deletedCount = 0;
95
+ for (const storingTenantId of await storingTenantIds(db, latestPossibleCutoff(now))) {
77
96
  const storeCutoff = await cutoffOf(storingTenantId);
78
- const candidates = await selectMany(db, eventsTable, {
79
- aggregateType: ESCAPE_HATCH_USE_AGGREGATE_TYPE,
80
- tenantId: storingTenantId,
81
- createdAt: { lt: storeCutoff },
82
- });
83
- for (const row of candidates) {
84
- const targetTenantId = targetTenantOf(row.payload);
85
- const cutoff = targetTenantId
86
- ? earlierInstant(storeCutoff, await cutoffOf(targetTenantId))
87
- : storeCutoff;
88
- if (Temporal.Instant.compare(row.createdAt, cutoff) < 0) {
89
- expiredAggregateIds.push(row.aggregateId);
97
+ // Keyset on id: pruning a page only removes rows at or below the cursor.
98
+ let afterId;
99
+ for (;;) {
100
+ const page = await selectMany(db, eventsTable, {
101
+ aggregateType: ESCAPE_HATCH_USE_AGGREGATE_TYPE,
102
+ tenantId: storingTenantId,
103
+ createdAt: { lt: storeCutoff },
104
+ ...(afterId !== undefined && { id: { gt: afterId } }),
105
+ }, {
106
+ fields: ["id", "aggregateId", "createdAt", "payload"],
107
+ orderBy: { col: "id", direction: "asc" },
108
+ limit: scanPageSize,
109
+ });
110
+ const expiredAggregateIds = [];
111
+ for (const row of page) {
112
+ const targetTenantId = targetTenantOf(row.payload);
113
+ const cutoff = targetTenantId
114
+ ? earlierInstant(storeCutoff, await cutoffOf(targetTenantId))
115
+ : storeCutoff;
116
+ if (Temporal.Instant.compare(row.createdAt, cutoff) < 0) {
117
+ expiredAggregateIds.push(row.aggregateId);
118
+ }
90
119
  }
120
+ deletedCount += await pruneAggregates(db, expiredAggregateIds);
121
+ const lastRow = page[page.length - 1];
122
+ if (lastRow === undefined || page.length < scanPageSize)
123
+ break;
124
+ afterId = lastRow.id;
91
125
  }
92
126
  }
93
- let deletedCount = 0;
94
- for (let i = 0; i < expiredAggregateIds.length; i += PRUNE_CHUNK_SIZE) {
95
- const result = await pruneEvents(db, {
96
- aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
97
- aggregateIds: expiredAggregateIds.slice(i, i + PRUNE_CHUNK_SIZE),
98
- olderThan: now,
99
- });
100
- deletedCount += result.deletedCount;
101
- }
102
127
  return deletedCount;
103
128
  }
104
129
  async function pruneInBatches(db, olderThanDays, batchSize = PRUNE_BATCH_SIZE) {
@@ -127,5 +152,7 @@ export async function runEscapeHatchRetention(args) {
127
152
  return { deletedCount: await pruneInBatches(db, olderThanDays, args.batchSize) };
128
153
  }
129
154
  const operatorRetentionDays = await resolveExplicitRetentionDays({ ...args, db });
130
- return { deletedCount: await pruneByTenantProfiles(db, operatorRetentionDays) };
155
+ return {
156
+ deletedCount: await pruneByTenantProfiles(db, operatorRetentionDays, args.scanPageSize ?? SCAN_PAGE_SIZE),
157
+ };
131
158
  }
@@ -13,7 +13,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
13
13
  readonly sensitive?: boolean;
14
14
  readonly writeOnly?: boolean;
15
15
  readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month";
16
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
17
  readonly default?: string;
18
18
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
19
  readonly multiline?: boolean | {
@@ -36,7 +36,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
36
36
  readonly sensitive?: boolean;
37
37
  readonly writeOnly?: boolean;
38
38
  readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password" | "month";
39
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
40
40
  readonly default?: string;
41
41
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
42
  readonly multiline?: boolean | {
@@ -59,7 +59,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
59
59
  readonly sensitive?: boolean;
60
60
  readonly writeOnly?: boolean;
61
61
  readonly readAsInstruction?: boolean;
62
- readonly format?: "email" | "url" | "phone" | "password" | "month";
62
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
63
63
  readonly default?: string;
64
64
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
65
65
  readonly multiline?: boolean | {
@@ -114,7 +114,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
114
114
  readonly sensitive?: boolean;
115
115
  readonly writeOnly?: boolean;
116
116
  readonly readAsInstruction?: boolean;
117
- readonly format?: "email" | "url" | "phone" | "password" | "month";
117
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
118
118
  readonly default?: string;
119
119
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
120
120
  readonly multiline?: boolean | {
@@ -137,7 +137,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
137
137
  readonly sensitive?: boolean;
138
138
  readonly writeOnly?: boolean;
139
139
  readonly readAsInstruction?: boolean;
140
- readonly format?: "email" | "url" | "phone" | "password" | "month";
140
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
141
141
  readonly default?: string;
142
142
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
143
143
  readonly multiline?: boolean | {
@@ -160,7 +160,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
160
160
  readonly sensitive?: boolean;
161
161
  readonly writeOnly?: boolean;
162
162
  readonly readAsInstruction?: boolean;
163
- readonly format?: "email" | "url" | "phone" | "password" | "month";
163
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
164
164
  readonly default?: string;
165
165
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
166
166
  readonly multiline?: boolean | {
@@ -13,7 +13,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
13
13
  readonly sensitive?: boolean;
14
14
  readonly writeOnly?: boolean;
15
15
  readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month";
16
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
17
  readonly default?: string;
18
18
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
19
  readonly multiline?: boolean | {
@@ -36,7 +36,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
36
36
  readonly sensitive?: boolean;
37
37
  readonly writeOnly?: boolean;
38
38
  readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password" | "month";
39
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
40
40
  readonly default?: string;
41
41
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
42
  readonly multiline?: boolean | {
@@ -59,7 +59,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
59
59
  readonly sensitive?: boolean;
60
60
  readonly writeOnly?: boolean;
61
61
  readonly readAsInstruction?: boolean;
62
- readonly format?: "email" | "url" | "phone" | "password" | "month";
62
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
63
63
  readonly default?: string;
64
64
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
65
65
  readonly multiline?: boolean | {
@@ -82,7 +82,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
82
82
  readonly sensitive?: boolean;
83
83
  readonly writeOnly?: boolean;
84
84
  readonly readAsInstruction?: boolean;
85
- readonly format?: "email" | "url" | "phone" | "password" | "month";
85
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
86
86
  readonly default?: string;
87
87
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
88
88
  readonly multiline?: boolean | {
@@ -105,7 +105,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
105
105
  readonly sensitive?: boolean;
106
106
  readonly writeOnly?: boolean;
107
107
  readonly readAsInstruction?: boolean;
108
- readonly format?: "email" | "url" | "phone" | "password" | "month";
108
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
109
109
  readonly default?: string;
110
110
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
111
111
  readonly multiline?: boolean | {
@@ -161,7 +161,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
161
161
  readonly sensitive?: boolean;
162
162
  readonly writeOnly?: boolean;
163
163
  readonly readAsInstruction?: boolean;
164
- readonly format?: "email" | "url" | "phone" | "password" | "month";
164
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
165
165
  readonly default?: string;
166
166
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
167
167
  readonly multiline?: boolean | {
@@ -184,7 +184,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
184
184
  readonly sensitive?: boolean;
185
185
  readonly writeOnly?: boolean;
186
186
  readonly readAsInstruction?: boolean;
187
- readonly format?: "email" | "url" | "phone" | "password" | "month";
187
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
188
188
  readonly default?: string;
189
189
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
190
190
  readonly multiline?: boolean | {
@@ -207,7 +207,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
207
207
  readonly sensitive?: boolean;
208
208
  readonly writeOnly?: boolean;
209
209
  readonly readAsInstruction?: boolean;
210
- readonly format?: "email" | "url" | "phone" | "password" | "month";
210
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
211
211
  readonly default?: string;
212
212
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
213
213
  readonly multiline?: boolean | {
@@ -13,7 +13,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
13
13
  readonly sensitive?: boolean;
14
14
  readonly writeOnly?: boolean;
15
15
  readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month";
16
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
17
  readonly default?: string;
18
18
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
19
  readonly multiline?: boolean | {
@@ -36,7 +36,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
36
36
  readonly sensitive?: boolean;
37
37
  readonly writeOnly?: boolean;
38
38
  readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password" | "month";
39
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
40
40
  readonly default?: string;
41
41
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
42
  readonly multiline?: boolean | {
@@ -59,7 +59,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
59
59
  readonly sensitive?: boolean;
60
60
  readonly writeOnly?: boolean;
61
61
  readonly readAsInstruction?: boolean;
62
- readonly format?: "email" | "url" | "phone" | "password" | "month";
62
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
63
63
  readonly default?: string;
64
64
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
65
65
  readonly multiline?: boolean | {
@@ -82,7 +82,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
82
82
  readonly sensitive?: boolean;
83
83
  readonly writeOnly?: boolean;
84
84
  readonly readAsInstruction?: boolean;
85
- readonly format?: "email" | "url" | "phone" | "password" | "month";
85
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
86
86
  readonly default?: string;
87
87
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
88
88
  readonly multiline?: boolean | {
@@ -105,7 +105,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
105
105
  readonly sensitive?: boolean;
106
106
  readonly writeOnly?: boolean;
107
107
  readonly readAsInstruction?: boolean;
108
- readonly format?: "email" | "url" | "phone" | "password" | "month";
108
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
109
109
  readonly default?: string;
110
110
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
111
111
  readonly multiline?: boolean | {
@@ -160,7 +160,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
160
160
  readonly sensitive?: boolean;
161
161
  readonly writeOnly?: boolean;
162
162
  readonly readAsInstruction?: boolean;
163
- readonly format?: "email" | "url" | "phone" | "password" | "month";
163
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
164
164
  readonly default?: string;
165
165
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
166
166
  readonly multiline?: boolean | {
@@ -183,7 +183,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
183
183
  readonly sensitive?: boolean;
184
184
  readonly writeOnly?: boolean;
185
185
  readonly readAsInstruction?: boolean;
186
- readonly format?: "email" | "url" | "phone" | "password" | "month";
186
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
187
187
  readonly default?: string;
188
188
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
189
189
  readonly multiline?: boolean | {
@@ -206,7 +206,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
206
206
  readonly sensitive?: boolean;
207
207
  readonly writeOnly?: boolean;
208
208
  readonly readAsInstruction?: boolean;
209
- readonly format?: "email" | "url" | "phone" | "password" | "month";
209
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
210
210
  readonly default?: string;
211
211
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
212
212
  readonly multiline?: boolean | {
@@ -13,7 +13,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
13
13
  readonly sensitive?: boolean;
14
14
  readonly writeOnly?: boolean;
15
15
  readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month";
16
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
17
  readonly default?: string;
18
18
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
19
  readonly multiline?: boolean | {
@@ -20,6 +20,12 @@ function billingLabel(subscription, tierSource) {
20
20
  }
21
21
  const FILTER_OP = z.enum(["eq", "ne", "lt", "gt", "in"]);
22
22
  const SORTABLE_FIELDS = ["name", "tier", "billing"];
23
+ const DEFAULT_SORT_FIELD = "name";
24
+ async function withDecryptedPageNames(db, pageRows) {
25
+ const decrypted = await decryptTenantNames(await selectMany(db, tenantTable, { id: { in: pageRows.map((row) => row.tenantId) } }, { fields: ["id", "name"] }), "cap-overview:tenant-caps:list");
26
+ const nameById = new Map(decrypted.map((tenant) => [tenant.id, tenant.name ?? ""]));
27
+ return pageRows.map((row) => ({ ...row, name: nameById.get(row.tenantId) ?? "" }));
28
+ }
23
29
  function tenantCapKey(tenantId, capId) {
24
30
  return JSON.stringify([tenantId, capId]);
25
31
  }
@@ -105,9 +111,15 @@ export function createTenantCapsListQuery(caps, listCaps) {
105
111
  });
106
112
  }
107
113
  const db = ctx.systemDb.acknowledgeCrossTenant("cap-overview:tenant-caps:list — SystemAdmin platform-wide tenant overview");
108
- const tenants = await decryptTenantNames(
109
- // Destroyed tenants decrypt to the erased sentinel, which a picker or list would show verbatim.
110
- await selectMany(db, tenantTable, { destroyedAt: null }), "cap-overview:tenant-caps:list");
114
+ // Names are ciphertext at rest: only a name sort or search has to decrypt every tenant;
115
+ // otherwise rows stay name-less and just the page is decrypted afterwards.
116
+ const needsAllNames = (query.payload.sort ?? DEFAULT_SORT_FIELD) === "name" ||
117
+ (query.payload.search?.trim() ?? "") !== "";
118
+ const tenants = needsAllNames
119
+ ? await decryptTenantNames(
120
+ // Destroyed tenants decrypt to the erased sentinel, which a picker or list would show verbatim.
121
+ await selectMany(db, tenantTable, { destroyedAt: null }), "cap-overview:tenant-caps:list")
122
+ : await selectMany(db, tenantTable, { destroyedAt: null }, { fields: ["id"] });
111
123
  const assignments = await selectMany(db, tierAssignmentTable, {});
112
124
  const subscriptions = await selectMany(db, subscriptionsProjectionTable, {});
113
125
  const assignmentByTenant = new Map(assignments.map((row) => [row.tenantId, row]));
@@ -119,7 +131,7 @@ export function createTenantCapsListQuery(caps, listCaps) {
119
131
  return {
120
132
  id: tenant.id,
121
133
  tenantId: tenant.id,
122
- name: tenant.name,
134
+ name: tenant.name ?? "",
123
135
  tier: assignment?.tier ?? "",
124
136
  billing: billingLabel(subscriptionByTenant.get(tenant.id), assignment?.source ?? null),
125
137
  };
@@ -130,10 +142,13 @@ export function createTenantCapsListQuery(caps, listCaps) {
130
142
  // tenant/team-list.query.ts. Holds up to tier-engine's documented
131
143
  // "single-pass scan" scale (a few thousand tenants); past that the fix
132
144
  // is a real combined read-projection.
133
- const { rows: page, total } = pageRows(merged, query.payload, {
145
+ const { rows: pageWithoutNames, total } = pageRows(merged, query.payload, {
134
146
  searchFields: ["name"],
135
- defaultSort: "name",
147
+ defaultSort: DEFAULT_SORT_FIELD,
136
148
  });
149
+ const page = needsAllNames
150
+ ? pageWithoutNames
151
+ : await withDecryptedPageNames(db, pageWithoutNames);
137
152
  // Usage is computed only for this page's tenant ids, never for the
138
153
  // full tenant set; caps without usageBatch still cost one query per
139
154
  // tenant of the page, so those run in parallel.
@@ -28,13 +28,15 @@ export const tenantsMissingProfileQuery = defineQueryHandler({
28
28
  });
29
29
  }
30
30
  const db = ctx.systemDb.acknowledgeCrossTenant("platform operator scans every tenant for a missing compliance-profile selection");
31
- const tenants = await decryptTenantNames(await db.selectMany(tenantTable, { isEnabled: true }), "compliance-profiles-ops:tenants-missing-profile");
31
+ const tenantRows = await db.selectMany(tenantTable, {
32
+ isEnabled: true,
33
+ });
32
34
  const profileRows = await db.selectMany(tenantComplianceProfileTable, {});
33
35
  const tenantIdsWithProfile = new Set(profileRows.map((row) => row.tenantId));
36
+ // Only tenants that stay in the answer need their name decrypted.
37
+ const tenants = await decryptTenantNames(tenantRows.filter((tenant) => !tenantIdsWithProfile.has(tenant.id)), "compliance-profiles-ops:tenants-missing-profile");
34
38
  return {
35
- tenants: tenants
36
- .filter((tenant) => !tenantIdsWithProfile.has(tenant.id))
37
- .map((tenant) => ({ id: tenant.id, name: tenant.name })),
39
+ tenants: tenants.map((tenant) => ({ id: tenant.id, name: tenant.name })),
38
40
  };
39
41
  },
40
42
  });
@@ -13,7 +13,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
13
13
  readonly sensitive?: boolean;
14
14
  readonly writeOnly?: boolean;
15
15
  readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month";
16
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
17
  readonly default?: string;
18
18
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
19
  readonly multiline?: boolean | {
@@ -36,7 +36,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
36
36
  readonly sensitive?: boolean;
37
37
  readonly writeOnly?: boolean;
38
38
  readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password" | "month";
39
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
40
40
  readonly default?: string;
41
41
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
42
  readonly multiline?: boolean | {
@@ -59,7 +59,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
59
59
  readonly sensitive?: boolean;
60
60
  readonly writeOnly?: boolean;
61
61
  readonly readAsInstruction?: boolean;
62
- readonly format?: "email" | "url" | "phone" | "password" | "month";
62
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
63
63
  readonly default?: string;
64
64
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
65
65
  readonly multiline?: boolean | {
@@ -84,7 +84,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
84
84
  readonly sensitive?: boolean;
85
85
  readonly writeOnly?: boolean;
86
86
  readonly readAsInstruction?: boolean;
87
- readonly format?: "email" | "url" | "phone" | "password" | "month";
87
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
88
88
  readonly default?: string;
89
89
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
90
90
  readonly multiline?: boolean | {
@@ -107,7 +107,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
107
107
  readonly sensitive?: boolean;
108
108
  readonly writeOnly?: boolean;
109
109
  readonly readAsInstruction?: boolean;
110
- readonly format?: "email" | "url" | "phone" | "password" | "month";
110
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
111
111
  readonly default?: string;
112
112
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
113
113
  readonly multiline?: boolean | {
@@ -130,7 +130,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
130
130
  readonly sensitive?: boolean;
131
131
  readonly writeOnly?: boolean;
132
132
  readonly readAsInstruction?: boolean;
133
- readonly format?: "email" | "url" | "phone" | "password" | "month";
133
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
134
134
  readonly default?: string;
135
135
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
136
136
  readonly multiline?: boolean | {
@@ -13,7 +13,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
13
13
  readonly sensitive?: boolean;
14
14
  readonly writeOnly?: boolean;
15
15
  readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month";
16
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
17
  readonly default?: string;
18
18
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
19
  readonly multiline?: boolean | {
@@ -36,7 +36,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
36
36
  readonly sensitive?: boolean;
37
37
  readonly writeOnly?: boolean;
38
38
  readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password" | "month";
39
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
40
40
  readonly default?: string;
41
41
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
42
  readonly multiline?: boolean | {
@@ -59,7 +59,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
59
59
  readonly sensitive?: boolean;
60
60
  readonly writeOnly?: boolean;
61
61
  readonly readAsInstruction?: boolean;
62
- readonly format?: "email" | "url" | "phone" | "password" | "month";
62
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
63
63
  readonly default?: string;
64
64
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
65
65
  readonly multiline?: boolean | {
@@ -109,7 +109,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
109
109
  readonly sensitive?: boolean;
110
110
  readonly writeOnly?: boolean;
111
111
  readonly readAsInstruction?: boolean;
112
- readonly format?: "email" | "url" | "phone" | "password" | "month";
112
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
113
113
  readonly default?: string;
114
114
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
115
115
  readonly multiline?: boolean | {
@@ -13,7 +13,7 @@ export declare const tenantRetentionOverrideEntity: import("@cosmicdrift/kumiko-
13
13
  readonly sensitive?: boolean;
14
14
  readonly writeOnly?: boolean;
15
15
  readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month";
16
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
17
  readonly default?: string;
18
18
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
19
  readonly multiline?: boolean | {
@@ -52,7 +52,7 @@ export declare const tenantRetentionOverrideEntity: import("@cosmicdrift/kumiko-
52
52
  readonly sensitive?: boolean;
53
53
  readonly writeOnly?: boolean;
54
54
  readonly readAsInstruction?: boolean;
55
- readonly format?: "email" | "url" | "phone" | "password" | "month";
55
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
56
56
  readonly default?: string;
57
57
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
58
58
  readonly multiline?: boolean | {
@@ -77,7 +77,7 @@ export declare const tenantRetentionOverrideTable: import("@cosmicdrift/kumiko-f
77
77
  readonly sensitive?: boolean;
78
78
  readonly writeOnly?: boolean;
79
79
  readonly readAsInstruction?: boolean;
80
- readonly format?: "email" | "url" | "phone" | "password" | "month";
80
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
81
81
  readonly default?: string;
82
82
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
83
83
  readonly multiline?: boolean | {
@@ -116,7 +116,7 @@ export declare const tenantRetentionOverrideTable: import("@cosmicdrift/kumiko-f
116
116
  readonly sensitive?: boolean;
117
117
  readonly writeOnly?: boolean;
118
118
  readonly readAsInstruction?: boolean;
119
- readonly format?: "email" | "url" | "phone" | "password" | "month";
119
+ readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
120
120
  readonly default?: string;
121
121
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
122
122
  readonly multiline?: boolean | {
@@ -1,5 +1,7 @@
1
1
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
2
  import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { type DeliveryLogEntry } from "./types.ts";
4
+ export declare function rememberConnectionNameChannel(connectionNameChannels: Set<string>, channelName: string, channelOptions: unknown): void;
5
+ export declare function projectedAttemptAddress(connectionNameChannels: ReadonlySet<string>, channel: string, recipientAddress: string | null): string | null;
4
6
  export declare function appendAttemptEvent(db: DbConnection, registry: Registry, attemptId: string, entry: DeliveryLogEntry): Promise<void>;
5
7
  export declare function logAttempt(db: DbConnection, registry: Registry, entry: DeliveryLogEntry): Promise<string>;
@@ -7,7 +7,7 @@ import { runProjectionsForEvent } from "@cosmicdrift/kumiko-framework/pipeline";
7
7
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
8
8
  import { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
9
9
  import { deliveryAttemptSchema } from "./events.js";
10
- import { maskRecipientAddress } from "./mask-recipient-address.js";
10
+ import { maskRecipientAddress, maskRecipientAddressOnce } from "./mask-recipient-address.js";
11
11
  import { isDeliveryChannelPlugin } from "./types.js";
12
12
  // Unknown channels count as personal: masking is the fail-closed default.
13
13
  function addressIsConnectionName(registry, channelName) {
@@ -22,6 +22,17 @@ function loggedAddress(registry, entry) {
22
22
  ? entry.recipientAddress
23
23
  : maskRecipientAddress(entry.recipientAddress);
24
24
  }
25
+ // Projection-side counterpart of loggedAddress: legacy events were appended with the full address.
26
+ export function rememberConnectionNameChannel(connectionNameChannels, channelName, channelOptions) {
27
+ if (isDeliveryChannelPlugin(channelOptions) && channelOptions.addressKind === "connection-name") {
28
+ connectionNameChannels.add(channelName);
29
+ }
30
+ }
31
+ export function projectedAttemptAddress(connectionNameChannels, channel, recipientAddress) {
32
+ return connectionNameChannels.has(channel)
33
+ ? recipientAddress
34
+ : maskRecipientAddressOnce(recipientAddress);
35
+ }
25
36
  // Shared append + inline-projection write (low-level append() does not
26
37
  // auto-fire projections — only the dispatcher/executor paths do).
27
38
  async function writeAttemptEvent(db, registry, attemptId, expectedVersion, entry) {
@@ -1,6 +1,6 @@
1
1
  import { createSystemDbView, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { assertNotificationVariables, bridgeStub, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
3
- import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
3
+ import { createFallbackLogger, scrubLogText, } from "@cosmicdrift/kumiko-framework/logging";
4
4
  import { assertNotifyTargetsOwnTenant, createEscapeHatchReporter, } from "@cosmicdrift/kumiko-framework/pipeline";
5
5
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
6
6
  import { fileContextFromResolver } from "../file-foundation/index.js";
@@ -374,8 +374,13 @@ export function createDeliveryService(options) {
374
374
  continue;
375
375
  addresses.set(key, await channel.resolve(userId, channelCtx));
376
376
  }
377
- catch {
378
- // skip: re-resolved (and logged) by deliverToUser
377
+ catch (err) {
378
+ // The per-recipient delivery path resolves again and decides the outcome; this only surfaces the early failure.
379
+ logError.warn("channel pre-resolve failed", {
380
+ channel: channel.name,
381
+ notificationType,
382
+ error: scrubLogText(err instanceof Error ? err.message : String(err)),
383
+ });
379
384
  }
380
385
  }
381
386
  }