@cosmicdrift/kumiko-bundled-features 0.356.0 → 0.357.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/auth-mfa/i18n.js +1 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/screens.js +8 -1
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-counter/feature.js +7 -1
- package/dist/cap-overview/screens.js +3 -1
- package/dist/config/table.d.ts +6 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +15 -3
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/chat-webhook-sender.js +1 -1
- package/dist/delivery/constants.d.ts +1 -0
- package/dist/delivery/constants.js +4 -0
- package/dist/delivery/delivery-service.js +62 -25
- package/dist/delivery/feature.js +7 -1
- package/dist/delivery/jobs.js +25 -1
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/testing.d.ts +1 -0
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/folders/entity.d.ts +8 -0
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/ledger/entity.d.ts +13 -0
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/entity.js +7 -6
- package/dist/notes-history-user-data/hooks.d.ts +10 -1
- package/dist/notes-history-user-data/hooks.js +78 -61
- package/dist/notes-history-user-data/index.d.ts +1 -0
- package/dist/notes-history-user-data/index.js +2 -1
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/screens.js +2 -0
- package/dist/secrets/table.d.ts +1 -0
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/screens.js +2 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/template-resolver/table.d.ts +10 -0
- package/dist/template-resolver/user-content-table.d.ts +10 -0
- package/dist/tenant/constants.d.ts +1 -0
- package/dist/tenant/constants.js +6 -3
- package/dist/tenant/feature.js +2 -0
- package/dist/tenant/handlers/member-role-availability.query.d.ts +7 -0
- package/dist/tenant/handlers/member-role-availability.query.js +18 -0
- package/dist/tenant/invitation-table.d.ts +6 -0
- package/dist/tenant/member-role-options.d.ts +2 -0
- package/dist/tenant/member-role-options.js +4 -0
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +6 -0
- package/dist/tenant/screens.d.ts +4 -2
- package/dist/tenant/screens.js +10 -6
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/schema/user.d.ts +12 -0
- package/dist/user/screens.js +8 -1
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +6 -0
- package/dist/user-data-rights/screens.js +8 -1
- package/dist/waitlist/entity.d.ts +14 -0
- package/dist/waitlist/screens.js +8 -1
- package/package.json +9 -9
- package/src/auth-mfa/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +6 -0
- package/src/tenant/changes.json +6 -0
package/dist/auth-mfa/i18n.js
CHANGED
|
@@ -19,6 +19,7 @@ export const AUTH_MFA_FEATURE_I18N = {
|
|
|
19
19
|
"mfa.enable.reveal.qr": { en: "Scan this QR code" },
|
|
20
20
|
"mfa.enable.reveal.secret": { en: "Or enter this code manually" },
|
|
21
21
|
"mfa.enable.reveal.recoveryCodes": { en: "Recovery codes" },
|
|
22
|
+
"mfa.enable.reveal.savedCodes": { en: "I have saved my recovery codes." },
|
|
22
23
|
"mfa.enable.confirm.submit": { en: "Enable" },
|
|
23
24
|
"mfa.enable.confirm.done": { en: "Two-factor authentication is now enabled." },
|
|
24
25
|
"screen:auth-mfa-disable.title": { en: "Disable two-factor authentication" },
|
|
@@ -4,6 +4,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -102,6 +105,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
102
105
|
readonly description?: string;
|
|
103
106
|
readonly maxLength?: number;
|
|
104
107
|
readonly minLength?: number;
|
|
108
|
+
readonly autoComplete?: string;
|
|
105
109
|
readonly required?: boolean;
|
|
106
110
|
readonly searchable?: boolean;
|
|
107
111
|
readonly sortable?: boolean;
|
|
@@ -124,6 +128,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
124
128
|
readonly description?: string;
|
|
125
129
|
readonly maxLength?: number;
|
|
126
130
|
readonly minLength?: number;
|
|
131
|
+
readonly autoComplete?: string;
|
|
127
132
|
readonly required?: boolean;
|
|
128
133
|
readonly searchable?: boolean;
|
|
129
134
|
readonly sortable?: boolean;
|
|
@@ -146,6 +151,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
146
151
|
readonly description?: string;
|
|
147
152
|
readonly maxLength?: number;
|
|
148
153
|
readonly minLength?: number;
|
|
154
|
+
readonly autoComplete?: string;
|
|
149
155
|
readonly required?: boolean;
|
|
150
156
|
readonly searchable?: boolean;
|
|
151
157
|
readonly sortable?: boolean;
|
package/dist/auth-mfa/screens.js
CHANGED
|
@@ -23,6 +23,7 @@ export const mfaEnableScreen = {
|
|
|
23
23
|
reveal: {
|
|
24
24
|
title: i18nKey("mfa.enable.reveal.title"),
|
|
25
25
|
warning: i18nKey("mfa.enable.reveal.warning"),
|
|
26
|
+
acknowledge: i18nKey("mfa.enable.reveal.savedCodes"),
|
|
26
27
|
fields: [
|
|
27
28
|
{
|
|
28
29
|
field: "otpauthUri",
|
|
@@ -47,7 +48,13 @@ export const mfaEnableScreen = {
|
|
|
47
48
|
confirm: {
|
|
48
49
|
handler: AuthMfaHandlers.enableConfirm,
|
|
49
50
|
fields: {
|
|
50
|
-
code: {
|
|
51
|
+
code: {
|
|
52
|
+
type: "text",
|
|
53
|
+
required: true,
|
|
54
|
+
minLength: 6,
|
|
55
|
+
maxLength: 6,
|
|
56
|
+
autoComplete: "one-time-code",
|
|
57
|
+
},
|
|
51
58
|
},
|
|
52
59
|
layout: { sections: [{ fields: ["code"] }] },
|
|
53
60
|
carry: ["setupToken"],
|
|
@@ -4,6 +4,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -70,6 +73,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
70
73
|
readonly description?: string;
|
|
71
74
|
readonly maxLength?: number;
|
|
72
75
|
readonly minLength?: number;
|
|
76
|
+
readonly autoComplete?: string;
|
|
73
77
|
readonly required?: boolean;
|
|
74
78
|
readonly searchable?: boolean;
|
|
75
79
|
readonly sortable?: boolean;
|
|
@@ -92,6 +96,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
92
96
|
readonly description?: string;
|
|
93
97
|
readonly maxLength?: number;
|
|
94
98
|
readonly minLength?: number;
|
|
99
|
+
readonly autoComplete?: string;
|
|
95
100
|
readonly required?: boolean;
|
|
96
101
|
readonly searchable?: boolean;
|
|
97
102
|
readonly sortable?: boolean;
|
|
@@ -147,6 +152,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
147
152
|
readonly description?: string;
|
|
148
153
|
readonly maxLength?: number;
|
|
149
154
|
readonly minLength?: number;
|
|
155
|
+
readonly autoComplete?: string;
|
|
150
156
|
readonly required?: boolean;
|
|
151
157
|
readonly searchable?: boolean;
|
|
152
158
|
readonly sortable?: boolean;
|
|
@@ -169,6 +175,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
169
175
|
readonly description?: string;
|
|
170
176
|
readonly maxLength?: number;
|
|
171
177
|
readonly minLength?: number;
|
|
178
|
+
readonly autoComplete?: string;
|
|
172
179
|
readonly required?: boolean;
|
|
173
180
|
readonly searchable?: boolean;
|
|
174
181
|
readonly sortable?: boolean;
|
|
@@ -191,6 +198,7 @@ export declare const paymentEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
191
198
|
readonly description?: string;
|
|
192
199
|
readonly maxLength?: number;
|
|
193
200
|
readonly minLength?: number;
|
|
201
|
+
readonly autoComplete?: string;
|
|
194
202
|
readonly required?: boolean;
|
|
195
203
|
readonly searchable?: boolean;
|
|
196
204
|
readonly sortable?: boolean;
|
|
@@ -4,6 +4,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -70,6 +73,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
70
73
|
readonly description?: string;
|
|
71
74
|
readonly maxLength?: number;
|
|
72
75
|
readonly minLength?: number;
|
|
76
|
+
readonly autoComplete?: string;
|
|
73
77
|
readonly required?: boolean;
|
|
74
78
|
readonly searchable?: boolean;
|
|
75
79
|
readonly sortable?: boolean;
|
|
@@ -92,6 +96,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
92
96
|
readonly description?: string;
|
|
93
97
|
readonly maxLength?: number;
|
|
94
98
|
readonly minLength?: number;
|
|
99
|
+
readonly autoComplete?: string;
|
|
95
100
|
readonly required?: boolean;
|
|
96
101
|
readonly searchable?: boolean;
|
|
97
102
|
readonly sortable?: boolean;
|
|
@@ -146,6 +151,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
|
|
|
146
151
|
readonly description?: string;
|
|
147
152
|
readonly maxLength?: number;
|
|
148
153
|
readonly minLength?: number;
|
|
154
|
+
readonly autoComplete?: string;
|
|
149
155
|
readonly required?: boolean;
|
|
150
156
|
readonly searchable?: boolean;
|
|
151
157
|
readonly sortable?: boolean;
|
|
@@ -168,6 +174,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
|
|
|
168
174
|
readonly description?: string;
|
|
169
175
|
readonly maxLength?: number;
|
|
170
176
|
readonly minLength?: number;
|
|
177
|
+
readonly autoComplete?: string;
|
|
171
178
|
readonly required?: boolean;
|
|
172
179
|
readonly searchable?: boolean;
|
|
173
180
|
readonly sortable?: boolean;
|
|
@@ -190,6 +197,7 @@ export declare const paymentsProjectionTable: import("@cosmicdrift/kumiko-framew
|
|
|
190
197
|
readonly description?: string;
|
|
191
198
|
readonly maxLength?: number;
|
|
192
199
|
readonly minLength?: number;
|
|
200
|
+
readonly autoComplete?: string;
|
|
193
201
|
readonly required?: boolean;
|
|
194
202
|
readonly searchable?: boolean;
|
|
195
203
|
readonly sortable?: boolean;
|
|
@@ -4,6 +4,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -94,7 +94,13 @@ export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
|
94
94
|
id: CAP_COUNTER_LIST_SCREEN_ID,
|
|
95
95
|
type: "entityList",
|
|
96
96
|
entity: "cap-counter",
|
|
97
|
-
columns: [
|
|
97
|
+
columns: [
|
|
98
|
+
"tenantId",
|
|
99
|
+
"capName",
|
|
100
|
+
"value",
|
|
101
|
+
"periodStart",
|
|
102
|
+
{ field: "lastSoftWarnedAt", hideOnNarrow: true },
|
|
103
|
+
],
|
|
98
104
|
defaultSort: { field: "capName", dir: "asc" },
|
|
99
105
|
// Search resolves against the caller's own tenant stream, so on this
|
|
100
106
|
// cross-tenant operator list it would only ever hit the SystemAdmin's tenant.
|
|
@@ -15,10 +15,12 @@ export function createTenantCapListScreen(caps, listCaps, tiers) {
|
|
|
15
15
|
{ field: "name", label: "cap-overview.list.col.name" },
|
|
16
16
|
{ field: "tier", label: "cap-overview.list.col.tier" },
|
|
17
17
|
{ field: "billing", label: "cap-overview.list.col.billing" },
|
|
18
|
-
|
|
18
|
+
// Name is the title; tier, billing and the first cap fill the card's three meta rows.
|
|
19
|
+
...listedCaps.map((cap, index) => ({
|
|
19
20
|
field: capFieldName(cap.id),
|
|
20
21
|
label: cap.label,
|
|
21
22
|
renderer: { react: { __component: CAP_USAGE_CELL_COMPONENT } },
|
|
23
|
+
...(index > 0 && { hideOnNarrow: true }),
|
|
22
24
|
})),
|
|
23
25
|
],
|
|
24
26
|
searchable: true,
|
package/dist/config/table.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const configValueEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -72,6 +75,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
|
|
|
72
75
|
readonly description?: string;
|
|
73
76
|
readonly maxLength?: number;
|
|
74
77
|
readonly minLength?: number;
|
|
78
|
+
readonly autoComplete?: string;
|
|
75
79
|
readonly required?: boolean;
|
|
76
80
|
readonly searchable?: boolean;
|
|
77
81
|
readonly sortable?: boolean;
|
|
@@ -94,6 +98,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
|
|
|
94
98
|
readonly description?: string;
|
|
95
99
|
readonly maxLength?: number;
|
|
96
100
|
readonly minLength?: number;
|
|
101
|
+
readonly autoComplete?: string;
|
|
97
102
|
readonly required?: boolean;
|
|
98
103
|
readonly searchable?: boolean;
|
|
99
104
|
readonly sortable?: boolean;
|
|
@@ -116,6 +121,7 @@ export declare const configValuesTable: import("@cosmicdrift/kumiko-framework/db
|
|
|
116
121
|
readonly description?: string;
|
|
117
122
|
readonly maxLength?: number;
|
|
118
123
|
readonly minLength?: number;
|
|
124
|
+
readonly autoComplete?: string;
|
|
119
125
|
readonly required?: boolean;
|
|
120
126
|
readonly searchable?: boolean;
|
|
121
127
|
readonly sortable?: boolean;
|
|
@@ -10,6 +10,7 @@ import { purgeSearchDocumentsForSubject } from "@cosmicdrift/kumiko-framework/se
|
|
|
10
10
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
11
11
|
import * as z from "zod";
|
|
12
12
|
import { resolveRetentionPolicyForTenant } from "../../data-retention/index.js";
|
|
13
|
+
import { eraseNoteBodiesReachingSubject } from "../../notes-history-user-data/index.js";
|
|
13
14
|
import { revokeAllPatTokensForUser } from "../../personal-access-tokens/index.js";
|
|
14
15
|
import { USER_STATUS } from "../../user/index.js";
|
|
15
16
|
import { denyIfTargetOutsideAdminTenant, isSystemAdminActor, updateUserLifecycle, } from "../../user-data-rights/index.js";
|
|
@@ -213,7 +214,7 @@ function sweepErrorLabel(err) {
|
|
|
213
214
|
// The key is already erased when this runs, so a failing sweep must not skip the
|
|
214
215
|
// audit event: the failure is returned as a label for the event instead of thrown.
|
|
215
216
|
// The savepoint keeps a database error from aborting the handler transaction.
|
|
216
|
-
async function sweepDerivedSubjectData(ctx, subject, subjectKey, crossTenantRunner) {
|
|
217
|
+
async function sweepDerivedSubjectData(ctx, subject, subjectKey, crossTenantRunner, audit) {
|
|
217
218
|
try {
|
|
218
219
|
await runInSavepointIfSupported(crossTenantRunner, async (crossTenantSubjectRunner) => {
|
|
219
220
|
// Blind-index sweep (#818): nulls bidx columns now so the deterministic
|
|
@@ -224,6 +225,16 @@ async function sweepDerivedSubjectData(ctx, subject, subjectKey, crossTenantRunn
|
|
|
224
225
|
if (ctx.searchAdapter) {
|
|
225
226
|
await purgeSearchDocumentsForSubject(crossTenantSubjectRunner, ctx.registry.features, ctx.searchAdapter, subjectKey, subject);
|
|
226
227
|
}
|
|
228
|
+
// Note bodies are keyed per note, so the subject's own key never reaches them.
|
|
229
|
+
if (ctx.registry.features.has("notes-history")) {
|
|
230
|
+
await eraseNoteBodiesReachingSubject({
|
|
231
|
+
db: crossTenantSubjectRunner,
|
|
232
|
+
registry: ctx.registry,
|
|
233
|
+
subject,
|
|
234
|
+
erasedBy: audit.erasedBy,
|
|
235
|
+
requestId: audit.requestId,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
227
238
|
});
|
|
228
239
|
return undefined;
|
|
229
240
|
}
|
|
@@ -335,12 +346,13 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
335
346
|
// but no event exists — a retry is a no-op erase plus the event. The
|
|
336
347
|
// reverse order could leave an audit trail claiming a shred that never
|
|
337
348
|
// happened.
|
|
349
|
+
const requestId = requestContext.get()?.requestId ?? "crypto-shredding:forget-subject";
|
|
338
350
|
await kms.eraseKey(subject, {
|
|
339
|
-
requestId
|
|
351
|
+
requestId,
|
|
340
352
|
userId: event.user.id,
|
|
341
353
|
eraseReason: event.payload.reason,
|
|
342
354
|
});
|
|
343
|
-
const cleanupError = await sweepDerivedSubjectData(ctx, subject, subjectKey, ctx.db.unsafeRaw());
|
|
355
|
+
const cleanupError = await sweepDerivedSubjectData(ctx, subject, subjectKey, ctx.db.unsafeRaw(), { requestId, erasedBy: event.user.id });
|
|
344
356
|
// User subject: close the login door. DEK-erase makes the passwordHash
|
|
345
357
|
// ciphertext unreadable, but status + PATs are standalone credentials —
|
|
346
358
|
// the PAT resolver only checks revokedAt/expiresAt, NOT user.status
|
|
@@ -4,6 +4,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -95,6 +98,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
|
|
|
95
98
|
readonly description?: string;
|
|
96
99
|
readonly maxLength?: number;
|
|
97
100
|
readonly minLength?: number;
|
|
101
|
+
readonly autoComplete?: string;
|
|
98
102
|
readonly required?: boolean;
|
|
99
103
|
readonly searchable?: boolean;
|
|
100
104
|
readonly sortable?: boolean;
|
|
@@ -4,6 +4,7 @@ export declare const tenantRetentionOverrideEntity: import("@cosmicdrift/kumiko-
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -42,6 +43,7 @@ export declare const tenantRetentionOverrideEntity: import("@cosmicdrift/kumiko-
|
|
|
42
43
|
readonly description?: string;
|
|
43
44
|
readonly maxLength?: number;
|
|
44
45
|
readonly minLength?: number;
|
|
46
|
+
readonly autoComplete?: string;
|
|
45
47
|
readonly required?: boolean;
|
|
46
48
|
readonly searchable?: boolean;
|
|
47
49
|
readonly sortable?: boolean;
|
|
@@ -66,6 +68,7 @@ export declare const tenantRetentionOverrideTable: import("@cosmicdrift/kumiko-f
|
|
|
66
68
|
readonly description?: string;
|
|
67
69
|
readonly maxLength?: number;
|
|
68
70
|
readonly minLength?: number;
|
|
71
|
+
readonly autoComplete?: string;
|
|
69
72
|
readonly required?: boolean;
|
|
70
73
|
readonly searchable?: boolean;
|
|
71
74
|
readonly sortable?: boolean;
|
|
@@ -104,6 +107,7 @@ export declare const tenantRetentionOverrideTable: import("@cosmicdrift/kumiko-f
|
|
|
104
107
|
readonly description?: string;
|
|
105
108
|
readonly maxLength?: number;
|
|
106
109
|
readonly minLength?: number;
|
|
110
|
+
readonly autoComplete?: string;
|
|
107
111
|
readonly required?: boolean;
|
|
108
112
|
readonly searchable?: boolean;
|
|
109
113
|
readonly sortable?: boolean;
|
|
@@ -17,7 +17,7 @@ export const chatConnectionNameSchema = z
|
|
|
17
17
|
function policyFor(request, target) {
|
|
18
18
|
return request.requireHttps
|
|
19
19
|
? { kind: "external" }
|
|
20
|
-
: { kind: "internal",
|
|
20
|
+
: { kind: "internal", allowOrigins: [target.origin] };
|
|
21
21
|
}
|
|
22
22
|
function hostMatches(hostname, allowedHost) {
|
|
23
23
|
const allowed = allowedHost.toLowerCase();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
export declare const DELIVERY_CHANNEL_EXTENSION: "deliveryChannel";
|
|
3
3
|
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
|
|
4
|
+
export declare function deliveryIdempotencyRedisKey(tenantId: string, key: string): string;
|
|
4
5
|
export declare const deliveryPriorityRank: Record<NotifyPriority, number>;
|
|
5
6
|
export declare const DeliveryJobs: {
|
|
6
7
|
readonly render: string;
|
|
@@ -4,6 +4,10 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
|
|
|
4
4
|
// channel-in-app, channel-push, ...).
|
|
5
5
|
export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel";
|
|
6
6
|
export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
|
|
7
|
+
// Single source for claim (delivery-service) and terminal-failure release (jobs).
|
|
8
|
+
export function deliveryIdempotencyRedisKey(tenantId, key) {
|
|
9
|
+
return `delivery:idem:${tenantId}:${key}`;
|
|
10
|
+
}
|
|
7
11
|
// notify() priority → BullMQ job priority. Lower number = processed first; all
|
|
8
12
|
// > 0 so prioritised delivery jobs never mix with BullMQ's "0 = unprioritised
|
|
9
13
|
// FIFO" bucket. critical jobs jump ahead of normal/low in the worker queue.
|
|
@@ -8,7 +8,7 @@ import { redactErrorText } from "../shared/index.js";
|
|
|
8
8
|
import { hashUnsubscribeAddress } from "./address-opt-out.js";
|
|
9
9
|
import { appendAttemptEvent, logAttempt } from "./attempt-log.js";
|
|
10
10
|
import { buildChannelContext } from "./channel-context.js";
|
|
11
|
-
import { DELIVERY_CHANNEL_EXTENSION, DeliveryJobs, deliveryPriorityRank } from "./constants.js";
|
|
11
|
+
import { DELIVERY_CHANNEL_EXTENSION, DeliveryJobs, deliveryIdempotencyRedisKey, deliveryPriorityRank, } from "./constants.js";
|
|
12
12
|
import { addressOptOutKey, selectOptedOutAddresses, } from "./db/queries/address-opt-outs.js";
|
|
13
13
|
import { selectNotificationPreferences } from "./db/queries/preferences.js";
|
|
14
14
|
import { isDeliveryChannelPlugin, } from "./types.js";
|
|
@@ -74,10 +74,28 @@ export function createDeliveryService(options) {
|
|
|
74
74
|
if (!idemRedis) {
|
|
75
75
|
throw new Error("Delivery idempotencyKey requires options.idempotencyRedis (or rateLimit.redis) to be configured");
|
|
76
76
|
}
|
|
77
|
-
const
|
|
78
|
-
const res = await idemRedis.set(k, "1", "EX", ttlSec, "NX");
|
|
77
|
+
const res = await idemRedis.set(deliveryIdempotencyRedisKey(tenantId, key), "1", "EX", ttlSec, "NX");
|
|
79
78
|
return res === "OK";
|
|
80
79
|
}
|
|
80
|
+
function isDeliveredOrQueued(delivery) {
|
|
81
|
+
return delivery.status === "sent" || delivery.status === "queued";
|
|
82
|
+
}
|
|
83
|
+
// A key that guarded nothing must not turn the caller's retry into a skipped duplicate.
|
|
84
|
+
// Partial success keeps it: a retry would re-send to those who got it.
|
|
85
|
+
async function releaseIdempotencyIfNothingDelivered(tenantId, key, run) {
|
|
86
|
+
// skip: no key claimed, or a recipient already got the notification
|
|
87
|
+
if (!key || run.deliveries.some(isDeliveredOrQueued))
|
|
88
|
+
return;
|
|
89
|
+
await idemRedis?.del(deliveryIdempotencyRedisKey(tenantId, key));
|
|
90
|
+
}
|
|
91
|
+
// Only a single-channel route lets the queued job free the key on terminal failure
|
|
92
|
+
// without risking a duplicate send on another channel.
|
|
93
|
+
function jobIdempotencyKeyFor(idempotencyKey, route) {
|
|
94
|
+
if (!idempotencyKey || !route)
|
|
95
|
+
return undefined;
|
|
96
|
+
const routedChannelCount = channels.filter((channel) => route[channel.name]).length;
|
|
97
|
+
return routedChannelCount === 1 ? idempotencyKey : undefined;
|
|
98
|
+
}
|
|
81
99
|
async function resolveUserIdsForTenant(tenantId) {
|
|
82
100
|
if (!tenantUserIdsQuery) {
|
|
83
101
|
throw new Error("Tenant broadcast requires tenantUserIdsQuery in DeliveryServiceOptions");
|
|
@@ -191,6 +209,7 @@ export function createDeliveryService(options) {
|
|
|
191
209
|
deliveryAttemptId,
|
|
192
210
|
priority,
|
|
193
211
|
message,
|
|
212
|
+
...(run.jobIdempotencyKey !== undefined && { idempotencyKey: run.jobIdempotencyKey }),
|
|
194
213
|
}, { priority: deliveryPriorityRank[priority] });
|
|
195
214
|
recordDelivery(run, queuedEntry, deliveryAttemptId);
|
|
196
215
|
}
|
|
@@ -517,14 +536,42 @@ export function createDeliveryService(options) {
|
|
|
517
536
|
}
|
|
518
537
|
}
|
|
519
538
|
}
|
|
539
|
+
async function dispatchNotification(notificationType, options, tenantId, priority, run) {
|
|
540
|
+
const { to, route, data } = options;
|
|
541
|
+
if (route) {
|
|
542
|
+
await deliverDirect(route, notificationType, data, tenantId, priority, options.recipientId ?? null, run);
|
|
543
|
+
// skip: explicit route is delivered directly, no recipient resolution needed
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (to !== undefined) {
|
|
547
|
+
let userIds;
|
|
548
|
+
if (typeof to === "string") {
|
|
549
|
+
userIds = [to];
|
|
550
|
+
}
|
|
551
|
+
else if ("tenant" in to) {
|
|
552
|
+
userIds = await resolveUserIdsForTenant(to.tenant);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
userIds = to;
|
|
556
|
+
}
|
|
557
|
+
const prefetchedAddresses = priority === "critical"
|
|
558
|
+
? new Map()
|
|
559
|
+
: await prefetchUserAddresses(userIds, tenantId, notificationType);
|
|
560
|
+
const suppressed = await loadSuppressedAddresses(tenantId, notificationType, priority, [...prefetchedAddresses.values()].filter((address) => !!address));
|
|
561
|
+
for (const userId of userIds) {
|
|
562
|
+
await deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
520
566
|
return {
|
|
521
567
|
async notify(notificationType, options, _user, tenantId, jobDispatcher) {
|
|
522
|
-
const {
|
|
568
|
+
const { route, idempotencyKey } = options;
|
|
523
569
|
const priority = options.priority ?? "normal";
|
|
524
570
|
const run = {
|
|
525
571
|
deliveries: [],
|
|
526
572
|
jobDispatcher: options.immediate ? undefined : (jobDispatcher ?? jobRunner),
|
|
527
573
|
locale: options.locale,
|
|
574
|
+
jobIdempotencyKey: jobIdempotencyKeyFor(idempotencyKey, route),
|
|
528
575
|
};
|
|
529
576
|
if (idempotencyKey) {
|
|
530
577
|
const first = await claimIdempotency(tenantId, idempotencyKey);
|
|
@@ -542,29 +589,19 @@ export function createDeliveryService(options) {
|
|
|
542
589
|
return { deliveries: run.deliveries };
|
|
543
590
|
}
|
|
544
591
|
}
|
|
545
|
-
|
|
546
|
-
await
|
|
547
|
-
return { deliveries: run.deliveries };
|
|
592
|
+
try {
|
|
593
|
+
await dispatchNotification(notificationType, options, tenantId, priority, run);
|
|
548
594
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
else {
|
|
558
|
-
userIds = to;
|
|
559
|
-
}
|
|
560
|
-
const prefetchedAddresses = priority === "critical"
|
|
561
|
-
? new Map()
|
|
562
|
-
: await prefetchUserAddresses(userIds, tenantId, notificationType);
|
|
563
|
-
const suppressed = await loadSuppressedAddresses(tenantId, notificationType, priority, [...prefetchedAddresses.values()].filter((address) => !!address));
|
|
564
|
-
for (const userId of userIds) {
|
|
565
|
-
await deliverToUser(userId, notificationType, data, tenantId, priority, run, prefetchedAddresses, suppressed);
|
|
566
|
-
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
// A failing release must not replace the dispatch error the caller retries on.
|
|
597
|
+
await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run).catch((releaseErr) => {
|
|
598
|
+
logError.warn(`idempotency key release failed: ${redactedMessageOf(releaseErr)}`, {
|
|
599
|
+
notificationType,
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
throw err;
|
|
567
603
|
}
|
|
604
|
+
await releaseIdempotencyIfNothingDelivered(tenantId, idempotencyKey, run);
|
|
568
605
|
return { deliveries: run.deliveries };
|
|
569
606
|
},
|
|
570
607
|
};
|
package/dist/delivery/feature.js
CHANGED
|
@@ -179,15 +179,21 @@ export function createDeliveryFeature(options) {
|
|
|
179
179
|
label: i18nKey("delivery.log.col.channel"),
|
|
180
180
|
renderer: { react: { __component: DELIVERY_CHANNEL_CELL_COMPONENT } },
|
|
181
181
|
},
|
|
182
|
-
{
|
|
182
|
+
{
|
|
183
|
+
field: "recipientLabel",
|
|
184
|
+
label: i18nKey("delivery.log.col.recipient"),
|
|
185
|
+
hideOnNarrow: true,
|
|
186
|
+
},
|
|
183
187
|
{
|
|
184
188
|
field: "status",
|
|
185
189
|
label: i18nKey("delivery.log.col.status"),
|
|
190
|
+
hideOnNarrow: true,
|
|
186
191
|
renderer: { react: { __component: DELIVERY_STATUS_CELL_COMPONENT } },
|
|
187
192
|
},
|
|
188
193
|
{
|
|
189
194
|
field: "error",
|
|
190
195
|
label: i18nKey("delivery.log.col.error"),
|
|
196
|
+
hideOnNarrow: true,
|
|
191
197
|
renderer: { react: { __component: DELIVERY_ERROR_CELL_COMPONENT } },
|
|
192
198
|
},
|
|
193
199
|
],
|
package/dist/delivery/jobs.js
CHANGED
|
@@ -8,7 +8,7 @@ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import { appendAttemptEvent } from "./attempt-log.js";
|
|
10
10
|
import { buildChannelContext } from "./channel-context.js";
|
|
11
|
-
import { DeliveryJobs, deliveryPriorityRank } from "./constants.js";
|
|
11
|
+
import { DeliveryJobs, deliveryIdempotencyRedisKey, deliveryPriorityRank } from "./constants.js";
|
|
12
12
|
import { collectChannels, redactedMessageOf } from "./delivery-service.js";
|
|
13
13
|
const channelMessageSchema = z.object({
|
|
14
14
|
notificationType: z.string(),
|
|
@@ -26,6 +26,7 @@ const renderJobPayloadSchema = z.object({
|
|
|
26
26
|
deliveryAttemptId: z.string(),
|
|
27
27
|
priority: z.enum(["critical", "normal", "low"]),
|
|
28
28
|
message: channelMessageSchema,
|
|
29
|
+
idempotencyKey: z.string().optional(),
|
|
29
30
|
});
|
|
30
31
|
const sendJobPayloadSchema = renderJobPayloadSchema.extend({
|
|
31
32
|
rendered: z
|
|
@@ -75,6 +76,28 @@ function entryFor(p, status, error, address, confirmed) {
|
|
|
75
76
|
...(confirmed === false && { confirmed }),
|
|
76
77
|
};
|
|
77
78
|
}
|
|
79
|
+
// Only the final retry frees the key: earlier failures are retried by the runner and
|
|
80
|
+
// must still dedup. Assumes ctx.redis is the handle the service claimed on; runProdApp
|
|
81
|
+
// wires the same instance into both.
|
|
82
|
+
async function releaseIdempotencyOnTerminalFailure(ctx, p) {
|
|
83
|
+
// skip: nothing to release before the last attempt or without an idempotency key
|
|
84
|
+
if (!ctx.finalAttempt || !p.idempotencyKey)
|
|
85
|
+
return;
|
|
86
|
+
if (!ctx.redis) {
|
|
87
|
+
ctx.log.warn("delivery: idempotency key not released, job context has no redis", {
|
|
88
|
+
notificationType: p.notificationType,
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await ctx.redis.del(deliveryIdempotencyRedisKey(p.tenantId, p.idempotencyKey));
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
ctx.log.warn(`delivery: idempotency key release failed: ${redactedMessageOf(err)}`, {
|
|
97
|
+
notificationType: p.notificationType,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
78
101
|
// The job runner stores err.message in the run row, and BullMQ keeps failedReason and
|
|
79
102
|
// the stack trace in Redis in plain text, so the rethrown error carries only the code.
|
|
80
103
|
// The redacted message goes to the log.
|
|
@@ -85,6 +108,7 @@ async function failAttempt(ctx, db, registry, p, code, err) {
|
|
|
85
108
|
channel: p.channelName,
|
|
86
109
|
});
|
|
87
110
|
await appendAttemptEvent(db, registry, p.deliveryAttemptId, entryFor(p, "failed", code, p.address));
|
|
111
|
+
await releaseIdempotencyOnTerminalFailure(ctx, p);
|
|
88
112
|
throw new Error(code);
|
|
89
113
|
}
|
|
90
114
|
// Render the message and hand off to delivery.send. On failure: record the
|