@cosmicdrift/kumiko-bundled-features 0.355.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/compliance-profiles/handlers/set-profile.write.js +14 -22
- 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/custom-fields/handlers/define-system-field.write.js +8 -16
- package/dist/custom-fields/handlers/delete-system-field.write.js +5 -2
- package/dist/custom-fields/lib/define-or-resurrect.d.ts +2 -1
- package/dist/custom-fields/lib/define-or-resurrect.js +11 -4
- 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/handlers/set.write.js +6 -17
- 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/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/require-for-tenant.d.ts +2 -0
- package/dist/shared/require-for-tenant.js +9 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/template-resolver/handlers/collection-set.write.js +4 -4
- package/dist/template-resolver/handlers/set.write.js +6 -7
- package/dist/template-resolver/handlers/upsert-system.write.js +6 -6
- package/dist/template-resolver/handlers/upsert-tenant.write.js +6 -4
- package/dist/template-resolver/table.d.ts +10 -0
- package/dist/template-resolver/user-content-table.d.ts +10 -0
- package/dist/tenant/cancel-pending-invitation.d.ts +2 -2
- package/dist/tenant/cancel-pending-invitation.js +2 -2
- 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/handlers/remove-member.write.js +13 -10
- package/dist/tenant/handlers/update-member-roles.write.js +12 -9
- 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/handlers/set-tenant-tier.write.js +7 -18
- 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,
|
|
@@ -4,6 +4,7 @@ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
|
4
4
|
import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
5
|
import { UnprocessableError, validationErrorFromZod, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
6
|
import * as z from "zod";
|
|
7
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
7
8
|
import { tenantComplianceProfileEntity, tenantComplianceProfileTable, } from "../schema/profile-selection.js";
|
|
8
9
|
const crud = createEventStoreExecutor(tenantComplianceProfileTable, tenantComplianceProfileEntity, {
|
|
9
10
|
entityName: "tenant-compliance-profile",
|
|
@@ -13,24 +14,16 @@ const crud = createEventStoreExecutor(tenantComplianceProfileTable, tenantCompli
|
|
|
13
14
|
// nicht eine waehlbare Production-Option. Symmetrisch zu
|
|
14
15
|
// SELECTABLE_PROFILE_KEYS aus der framework/compliance-Liste.
|
|
15
16
|
const profileKeySchema = z.enum(SELECTABLE_PROFILE_KEYS);
|
|
16
|
-
//
|
|
17
|
+
// Upsert: the first call inserts, later calls update, so repeating the same values
|
|
18
|
+
// is idempotent apart from the extra audit events.
|
|
17
19
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
20
|
+
// A SystemAdmin may target another tenant via `tenantIdOverride` (operator setup,
|
|
21
|
+
// onboarding migrations); the dispatcher rejects the override for anyone else. The
|
|
22
|
+
// executor then writes into the target tenant's stream via streamTenantId while
|
|
23
|
+
// the operator stays the recorded actor.
|
|
21
24
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// Onboarding-Migrationen). TenantAdmin's Override-Versuch → 403.
|
|
25
|
-
// executorUser.tenantId muss = ziel-tenant sein damit der event-store-
|
|
26
|
-
// Stream-Lookup nicht miss → version_conflict gibt (Memory:
|
|
27
|
-
// feedback_event_store_tenant_consistency).
|
|
28
|
-
//
|
|
29
|
-
// Validation:
|
|
30
|
-
// - profileKey muss in SELECTABLE_PROFILE_KEYS sein (Zod-checked)
|
|
31
|
-
// - override (optional) muss valides JSON-Object sein
|
|
32
|
-
// - override Top-Level-Keys muessen in ALLOWED_OVERRIDE_KEYS sein
|
|
33
|
-
// — verhindert Tippfehler die deepMerge stillschweigend ignoriert
|
|
25
|
+
// Override top-level keys are restricted to ALLOWED_OVERRIDE_KEYS because deepMerge
|
|
26
|
+
// would silently ignore a typo.
|
|
34
27
|
export const setProfileWrite = defineWriteHandler({
|
|
35
28
|
name: "set-profile",
|
|
36
29
|
schema: z.object({
|
|
@@ -43,9 +36,8 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
43
36
|
access: { roles: access.admin },
|
|
44
37
|
description: "Sets or replaces a tenant's compliance profile key plus an optional JSON override, for onboarding or a later region change; a system admin may target a different tenant through tenantIdOverride.",
|
|
45
38
|
handler: async (event, ctx) => {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const executorUser = tenantOverride !== undefined ? { ...event.user, tenantId } : event.user;
|
|
39
|
+
const tenantId = (event.payload.tenantIdOverride ?? event.user.tenantId); // @cast-boundary engine-payload
|
|
40
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
49
41
|
// Override-Validation: muss parseables JSON-Object sein UND dem
|
|
50
42
|
// ComplianceProfileOverride-Schema entsprechen (S1.9 Z3 — strict-Zod
|
|
51
43
|
// mit Top-Level + Sub-Level-Whitelist via .strict()). Tippfehler
|
|
@@ -72,7 +64,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
72
64
|
}
|
|
73
65
|
}
|
|
74
66
|
// Upsert: existierenden Eintrag suchen
|
|
75
|
-
const existing = (await fetchOne(
|
|
67
|
+
const existing = (await fetchOne(db, tenantComplianceProfileTable, {
|
|
76
68
|
tenantId: tenantId,
|
|
77
69
|
})); // @cast-boundary db-runner
|
|
78
70
|
if (existing) {
|
|
@@ -83,7 +75,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
83
75
|
profileKey: event.payload.profileKey,
|
|
84
76
|
override: event.payload.override ?? null,
|
|
85
77
|
},
|
|
86
|
-
},
|
|
78
|
+
}, event.user, db, { streamTenantId });
|
|
87
79
|
if (!result.isSuccess)
|
|
88
80
|
return result;
|
|
89
81
|
return {
|
|
@@ -95,7 +87,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
95
87
|
profileKey: event.payload.profileKey,
|
|
96
88
|
override: event.payload.override ?? null,
|
|
97
89
|
tenantId,
|
|
98
|
-
},
|
|
90
|
+
}, event.user, db, { streamTenantId });
|
|
99
91
|
if (!result.isSuccess)
|
|
100
92
|
return result;
|
|
101
93
|
return {
|
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;
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
2
3
|
import { fieldDefinitionAggregateId } from "../aggregate-id.js";
|
|
3
4
|
import { defineOrResurrectFieldDefinition } from "../lib/define-or-resurrect.js";
|
|
4
5
|
import { buildFieldDefinitionColumns } from "../lib/field-definition-row.js";
|
|
5
6
|
import { defineFieldPayloadSchema } from "../schemas.js";
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// **Use-Case:** Vendor (cdgs) sagt "alle Hausverwaltungs-customers haben ab
|
|
12
|
-
// heute ein `internalNumber`-Field". Tenant kann den Wert pro customer
|
|
13
|
-
// setzen, aber die Definition nicht ändern oder löschen.
|
|
14
|
-
//
|
|
15
|
-
// **Same-scope-conflict** wie bei define-tenant-field via aggregate-version-
|
|
16
|
-
// conflict (deterministische ID mit tenantId=SYSTEM_TENANT_ID).
|
|
7
|
+
// A system-wide field definition applies to every tenant. tenantId is forced to
|
|
8
|
+
// SYSTEM_TENANT_ID (never taken from the caller) and addressed via streamTenantId,
|
|
9
|
+
// so the SystemAdmin stays the recorded actor. Tenants can set values for the field
|
|
10
|
+
// but cannot change or delete the definition. A same-scope duplicate surfaces as an
|
|
11
|
+
// aggregate version conflict, because the ID is derived from SYSTEM_TENANT_ID.
|
|
17
12
|
export const defineSystemFieldHandler = {
|
|
18
13
|
name: "define-system-field",
|
|
19
14
|
schema: defineFieldPayloadSchema,
|
|
@@ -22,10 +17,7 @@ export const defineSystemFieldHandler = {
|
|
|
22
17
|
handler: async (event, ctx) => {
|
|
23
18
|
const payload = event.payload; // @cast-boundary engine-payload
|
|
24
19
|
const aggregateId = fieldDefinitionAggregateId(SYSTEM_TENANT_ID, payload.entityName, payload.fieldKey);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// — the row lives in the system-scope-stream.
|
|
28
|
-
const systemUser = { ...event.user, tenantId: SYSTEM_TENANT_ID };
|
|
29
|
-
return defineOrResurrectFieldDefinition(aggregateId, buildFieldDefinitionColumns(payload), systemUser, ctx.db);
|
|
20
|
+
const { db, streamTenantId } = requireForTenant(ctx, SYSTEM_TENANT_ID);
|
|
21
|
+
return defineOrResurrectFieldDefinition(aggregateId, buildFieldDefinitionColumns(payload), event.user, db, streamTenantId);
|
|
30
22
|
},
|
|
31
23
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
2
3
|
import { fieldDefinitionAggregateId } from "../aggregate-id.js";
|
|
3
4
|
import { FIELD_DEFINITION_AGGREGATE_TYPE } from "../constants.js";
|
|
4
5
|
import { fieldDefinitionExecutor } from "../executor.js";
|
|
@@ -18,8 +19,10 @@ export const deleteSystemFieldHandler = {
|
|
|
18
19
|
handler: async (event, ctx) => {
|
|
19
20
|
const payload = event.payload; // @cast-boundary engine-payload
|
|
20
21
|
const aggregateId = fieldDefinitionAggregateId(SYSTEM_TENANT_ID, payload.entityName, payload.fieldKey);
|
|
21
|
-
const
|
|
22
|
-
const result = await fieldDefinitionExecutor.delete({ id: aggregateId },
|
|
22
|
+
const { db, streamTenantId } = requireForTenant(ctx, SYSTEM_TENANT_ID);
|
|
23
|
+
const result = await fieldDefinitionExecutor.delete({ id: aggregateId }, event.user, db, {
|
|
24
|
+
streamTenantId,
|
|
25
|
+
});
|
|
23
26
|
// Cascade-cleanup-Event — host-entity-MSPs entfernen orphan values aus
|
|
24
27
|
// ihrer customFields jsonb. Im selben TX = atomic.
|
|
25
28
|
if (result.isSuccess) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
2
|
import { fieldDefinitionExecutor } from "../executor.js";
|
|
2
3
|
import type { FieldDefinitionColumns } from "./field-definition-row.js";
|
|
3
4
|
type DefineUser = Parameters<typeof fieldDefinitionExecutor.create>[1];
|
|
4
5
|
type DefineDb = Parameters<typeof fieldDefinitionExecutor.create>[2];
|
|
5
6
|
type WriteResult = Awaited<ReturnType<typeof fieldDefinitionExecutor.create>>;
|
|
6
|
-
export declare function defineOrResurrectFieldDefinition(aggregateId: string, columns: FieldDefinitionColumns, user: DefineUser, db: DefineDb): Promise<WriteResult>;
|
|
7
|
+
export declare function defineOrResurrectFieldDefinition(aggregateId: string, columns: FieldDefinitionColumns, user: DefineUser, db: DefineDb, streamTenantId?: TenantId): Promise<WriteResult>;
|
|
7
8
|
export {};
|
|
@@ -16,12 +16,16 @@ import { fieldDefinitionExecutor } from "../executor.js";
|
|
|
16
16
|
// fail with "current transaction is aborted". detail() (a read) + restore()
|
|
17
17
|
// (which sees soft-deleted rows via a tenant-scoped fetch and only writes on
|
|
18
18
|
// success) keep the tx clean until the single terminal write.
|
|
19
|
-
export async function defineOrResurrectFieldDefinition(aggregateId, columns, user, db) {
|
|
19
|
+
export async function defineOrResurrectFieldDefinition(aggregateId, columns, user, db, streamTenantId) {
|
|
20
20
|
const active = await fieldDefinitionExecutor.detail({ id: aggregateId }, user, db);
|
|
21
21
|
if (active) {
|
|
22
|
-
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db
|
|
22
|
+
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db, {
|
|
23
|
+
streamTenantId,
|
|
24
|
+
});
|
|
23
25
|
}
|
|
24
|
-
const restored = await fieldDefinitionExecutor.restore({ id: aggregateId }, user, db
|
|
26
|
+
const restored = await fieldDefinitionExecutor.restore({ id: aggregateId }, user, db, {
|
|
27
|
+
streamTenantId,
|
|
28
|
+
});
|
|
25
29
|
if (restored.isSuccess) {
|
|
26
30
|
// restore() just un-deleted the row in this same tx; no concurrent writer
|
|
27
31
|
// exists, so skip the optimistic-lock version match (we'd otherwise have to
|
|
@@ -31,10 +35,13 @@ export async function defineOrResurrectFieldDefinition(aggregateId, columns, use
|
|
|
31
35
|
// readonly FieldDefinitionColumns isn't assignable to it.
|
|
32
36
|
return fieldDefinitionExecutor.update({ id: aggregateId, changes: { ...columns } }, user, db, {
|
|
33
37
|
skipOptimisticLock: true,
|
|
38
|
+
streamTenantId,
|
|
34
39
|
});
|
|
35
40
|
}
|
|
36
41
|
if (restored.error.code === "not_found") {
|
|
37
|
-
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db
|
|
42
|
+
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db, {
|
|
43
|
+
streamTenantId,
|
|
44
|
+
});
|
|
38
45
|
}
|
|
39
46
|
return restored;
|
|
40
47
|
}
|
|
@@ -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.
|