@cosmicdrift/kumiko-bundled-features 0.381.0 → 0.383.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
- package/dist/audit/run-escape-hatch-retention.js +54 -27
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -6
- package/dist/billing-foundation/entities.d.ts +8 -8
- package/dist/billing-foundation/projection.d.ts +8 -8
- package/dist/cap-counter/entity.d.ts +1 -1
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +21 -6
- package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +6 -4
- package/dist/config/table.d.ts +6 -6
- package/dist/custom-fields/entity.d.ts +4 -4
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -4
- package/dist/delivery/attempt-log.d.ts +2 -0
- package/dist/delivery/attempt-log.js +12 -1
- package/dist/delivery/delivery-service.js +8 -3
- package/dist/delivery/feature.js +7 -9
- package/dist/delivery/mask-recipient-address.d.ts +1 -0
- package/dist/delivery/mask-recipient-address.js +16 -0
- package/dist/delivery/tables.d.ts +6 -6
- package/dist/document-ingest-foundation/entity.d.ts +4 -4
- package/dist/folders/entity.d.ts +8 -8
- package/dist/form-draft/entity.d.ts +2 -2
- package/dist/inbound-mail-foundation/entities.d.ts +25 -25
- package/dist/inbound-mail-foundation/projection.d.ts +20 -20
- package/dist/jobs/job-run-logger.d.ts +1 -1
- package/dist/jobs/job-run-logger.js +20 -0
- package/dist/jobs/job-run-table.d.ts +6 -6
- package/dist/ledger/entity.d.ts +13 -13
- package/dist/managed-pages/table.d.ts +12 -12
- package/dist/notes-history/entity.d.ts +12 -12
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -6
- package/dist/secrets/table.d.ts +1 -1
- package/dist/sessions/schema/user-session.d.ts +4 -4
- package/dist/sessions/session-callbacks.js +23 -16
- package/dist/tags/entity.d.ts +9 -9
- package/dist/template-resolver/table.d.ts +10 -10
- package/dist/template-resolver/user-content-table.d.ts +10 -10
- package/dist/tenant/decrypt-tenant-names.d.ts +4 -0
- package/dist/tenant/decrypt-tenant-names.js +15 -0
- package/dist/tenant/handlers/tenant-directory.query.js +6 -7
- package/dist/tenant/invitation-table.d.ts +6 -6
- package/dist/tenant/membership-table.d.ts +4 -4
- package/dist/tenant/schema/tenant.d.ts +6 -6
- package/dist/tier-engine/entity.d.ts +1 -1
- package/dist/tier-engine/tier-resolver.d.ts +1 -1
- package/dist/user/schema/user.d.ts +12 -12
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -14
- package/dist/user-data-rights/schema/download-token.d.ts +8 -8
- package/dist/user-data-rights/schema/export-job.d.ts +6 -6
- package/dist/waitlist/entity.d.ts +14 -14
- package/package.json +9 -9
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +6 -0
- package/src/cap-overview/changes.json +6 -0
- package/src/delivery/changes.json +6 -0
- package/src/jobs/changes.json +7 -0
- package/src/sessions/changes.json +6 -0
- package/src/tenant/changes.json +6 -0
|
@@ -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
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
:
|
|
88
|
-
|
|
89
|
-
|
|
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 {
|
|
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
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
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:
|
|
145
|
+
const { rows: pageWithoutNames, total } = pageRows(merged, query.payload, {
|
|
134
146
|
searchFields: ["name"],
|
|
135
|
-
defaultSort:
|
|
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
|
|
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
|
});
|
package/dist/config/table.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
}
|