@cosmicdrift/kumiko-bundled-features 0.360.0 → 0.361.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/handlers/details.query.d.ts +1 -1
- package/dist/audit/handlers/details.query.js +2 -1
- package/dist/audit/handlers/list.query.d.ts +1 -1
- package/dist/audit/handlers/list.query.js +4 -5
- package/dist/auth-email-password/web/auth-client.d.ts +2 -0
- package/dist/auth-email-password/web/auth-client.js +3 -0
- package/dist/auth-email-password/web/session.js +19 -3
- package/dist/cap-counter/index.d.ts +1 -0
- package/dist/cap-counter/index.js +1 -0
- package/dist/data-retention/run-retention-cleanup.js +9 -6
- package/dist/folders-user-data/hooks.js +4 -1
- package/dist/form-draft-user-data/hooks.js +4 -1
- package/dist/shared/encrypt-for-direct-write.js +2 -2
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/redact-pii-ciphertext.d.ts +2 -0
- package/dist/shared/redact-pii-ciphertext.js +14 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +3 -3
- package/dist/tenant-lifecycle/stages.js +2 -2
- package/dist/user/handlers/me.query.d.ts +3 -1
- package/dist/user/handlers/me.query.js +5 -1
- package/dist/user-data-rights/handlers/my-audit-log.query.d.ts +1 -1
- package/dist/user-data-rights/handlers/my-audit-log.query.js +2 -1
- package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +2 -1
- package/dist/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.js +3 -2
- package/dist/user-data-rights-defaults/hooks/user.userdata-hook.js +3 -3
- package/package.json +9 -9
- package/src/audit/changes.json +6 -0
- package/src/cap-counter/changes.json +6 -0
- package/src/user/changes.json +6 -0
|
@@ -11,7 +11,7 @@ export declare const detailsQuery: import("@cosmicdrift/kumiko-types/define-hand
|
|
|
11
11
|
aggregateType: string;
|
|
12
12
|
version: number;
|
|
13
13
|
type: string;
|
|
14
|
-
payload:
|
|
14
|
+
payload: unknown;
|
|
15
15
|
metadata: Record<string, unknown>;
|
|
16
16
|
createdAt: unknown;
|
|
17
17
|
createdBy: string;
|
|
@@ -6,6 +6,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
6
6
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
8
8
|
import * as z from "zod";
|
|
9
|
+
import { redactPiiCiphertext } from "../../shared/index.js";
|
|
9
10
|
import { AUDIT_SCOPE_VALUES } from "../constants.js";
|
|
10
11
|
import { resolveAuditScopeFilter } from "./resolve-audit-tenant.js";
|
|
11
12
|
export const detailsQuery = defineQueryHandler({
|
|
@@ -34,7 +35,7 @@ export const detailsQuery = defineQueryHandler({
|
|
|
34
35
|
aggregateType: row.aggregateType,
|
|
35
36
|
version: row.version,
|
|
36
37
|
type: row.type,
|
|
37
|
-
payload: row.payload,
|
|
38
|
+
payload: redactPiiCiphertext(row.payload),
|
|
38
39
|
metadata: row.metadata,
|
|
39
40
|
createdAt: row.createdAt,
|
|
40
41
|
createdBy: row.createdBy,
|
|
@@ -29,7 +29,7 @@ export declare const listQuery: import("@cosmicdrift/kumiko-types/define-handler
|
|
|
29
29
|
aggregateType: string;
|
|
30
30
|
version: number;
|
|
31
31
|
type: string;
|
|
32
|
-
payload:
|
|
32
|
+
payload: unknown;
|
|
33
33
|
metadata: Record<string, unknown>;
|
|
34
34
|
createdAt: unknown;
|
|
35
35
|
createdBy: string;
|
|
@@ -9,15 +9,14 @@
|
|
|
9
9
|
// A SystemAdmin can pass scope "system" to read the app-instance system events
|
|
10
10
|
// (e.g. app.started); no other cross-tenant read exists.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// query returns payloads as stored, so a sensitive value surfaces as ciphertext,
|
|
15
|
-
// never as plaintext.
|
|
12
|
+
// Event payloads hold PII as subject-KMS ciphertext; it is replaced by
|
|
13
|
+
// REDACTED_PII_PLACEHOLDER before returning, since the API leak guard refuses ciphertext.
|
|
16
14
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
17
15
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
16
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
19
17
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
20
18
|
import * as z from "zod";
|
|
19
|
+
import { redactPiiCiphertext } from "../../shared/index.js";
|
|
21
20
|
import { AUDIT_SCOPE_VALUES } from "../constants.js";
|
|
22
21
|
import { resolveAuditScopeFilter } from "./resolve-audit-tenant.js";
|
|
23
22
|
const MAX_LIMIT = 100;
|
|
@@ -102,7 +101,7 @@ export const listQuery = defineQueryHandler({
|
|
|
102
101
|
aggregateType: r.aggregateType,
|
|
103
102
|
version: r.version,
|
|
104
103
|
type: r.type,
|
|
105
|
-
payload: r.payload,
|
|
104
|
+
payload: redactPiiCiphertext(r.payload),
|
|
106
105
|
metadata: r.metadata,
|
|
107
106
|
createdAt: r.createdAt,
|
|
108
107
|
createdBy: r.createdBy,
|
|
@@ -135,6 +135,8 @@ export type CurrentUserProfile = {
|
|
|
135
135
|
readonly email: string;
|
|
136
136
|
readonly displayName: string;
|
|
137
137
|
readonly locale?: string;
|
|
138
|
+
/** Effective IANA zone (user override, else tenant, else UTC). */
|
|
139
|
+
readonly effectiveTimeZone?: string;
|
|
138
140
|
readonly globalRoles: readonly string[];
|
|
139
141
|
};
|
|
140
142
|
export declare function fetchCurrentUser(): Promise<CurrentUserProfile | null>;
|
|
@@ -295,6 +295,9 @@ export async function fetchCurrentUser() {
|
|
|
295
295
|
email: body.data.email,
|
|
296
296
|
displayName: body.data.displayName,
|
|
297
297
|
...(body.data.locale !== undefined && { locale: body.data.locale }),
|
|
298
|
+
...(body.data.effectiveTimeZone !== undefined && {
|
|
299
|
+
effectiveTimeZone: body.data.effectiveTimeZone,
|
|
300
|
+
}),
|
|
298
301
|
globalRoles: parseRoles(body.data.roles),
|
|
299
302
|
};
|
|
300
303
|
}
|
|
@@ -10,8 +10,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
10
10
|
// unter `<SessionProvider>`; der `useSession()`-Hook liefert den State
|
|
11
11
|
// und die Transitions.
|
|
12
12
|
import { readCsrfToken } from "@cosmicdrift/kumiko-dispatcher-live";
|
|
13
|
-
import { useSessionEndedSignal } from "@cosmicdrift/kumiko-renderer";
|
|
14
|
-
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
13
|
+
import { CurrentUserProvider, useSessionEndedSignal, } from "@cosmicdrift/kumiko-renderer";
|
|
14
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
15
15
|
import { AuthRequestError, fetchCurrentUser, fetchTenants, login as loginApi, logout as logoutApi, switchTenant as switchTenantApi, } from "./auth-client.js";
|
|
16
16
|
// Exported so screens that render on BOTH authenticated and anonymous public
|
|
17
17
|
// routes (e.g. InviteAcceptScreen) can read `.status` without requiring a
|
|
@@ -154,7 +154,23 @@ export function SessionProvider({ children, postLogoutUrl, }) {
|
|
|
154
154
|
}
|
|
155
155
|
}, []);
|
|
156
156
|
const api = { ...state, login, logout, switchTenant, refresh: doRefresh };
|
|
157
|
-
|
|
157
|
+
// Keyed on the primitives, not the profile object: every refresh (tenant
|
|
158
|
+
// switch, session bootstrap retry) builds a new profile object, and a new
|
|
159
|
+
// CurrentUser reference would re-seed a create form and drop typed input.
|
|
160
|
+
const profile = state.user;
|
|
161
|
+
const userId = profile?.id;
|
|
162
|
+
const userEmail = profile?.email;
|
|
163
|
+
const userDisplayName = profile?.displayName;
|
|
164
|
+
const userTimeZone = profile?.effectiveTimeZone;
|
|
165
|
+
const currentUser = useMemo(() => userId === undefined || userEmail === undefined || userDisplayName === undefined
|
|
166
|
+
? undefined
|
|
167
|
+
: {
|
|
168
|
+
id: userId,
|
|
169
|
+
email: userEmail,
|
|
170
|
+
displayName: userDisplayName,
|
|
171
|
+
...(userTimeZone !== undefined && { timeZone: userTimeZone }),
|
|
172
|
+
}, [userId, userEmail, userDisplayName, userTimeZone]);
|
|
173
|
+
return (_jsx(SessionContext.Provider, { value: api, children: _jsx(CurrentUserProvider, { user: currentUser, children: children }) }));
|
|
158
174
|
}
|
|
159
175
|
export function useSession() {
|
|
160
176
|
const ctx = useContext(SessionContext);
|
|
@@ -7,5 +7,6 @@ export { capCounterEntity } from "./entity.js";
|
|
|
7
7
|
export { capCounterFeature } from "./feature.js";
|
|
8
8
|
export { readPayloadField } from "./payload-field.js";
|
|
9
9
|
export { type CapGuardContext, type CapLimitContext, createStockCapGuard, type StockCapGuard, type StockCapSpec, } from "./stock-cap-guard.js";
|
|
10
|
+
export { capReservationsTable, capReservationsTableMeta } from "./tables.js";
|
|
10
11
|
export { createValueCapGuard, type ValueCapGuard, type ValueCapSpec, } from "./value-cap-guard.js";
|
|
11
12
|
export { type CalendarCapDef, type CalendarCapResolver, type RollingCapDef, type RollingCapResolver, withCapEnforcement, withRollingCapEnforcement, } from "./with-cap-enforcement.js";
|
|
@@ -8,5 +8,6 @@ export { capCounterEntity } from "./entity.js";
|
|
|
8
8
|
export { capCounterFeature } from "./feature.js";
|
|
9
9
|
export { readPayloadField } from "./payload-field.js";
|
|
10
10
|
export { createStockCapGuard, } from "./stock-cap-guard.js";
|
|
11
|
+
export { capReservationsTable, capReservationsTableMeta } from "./tables.js";
|
|
11
12
|
export { createValueCapGuard, } from "./value-cap-guard.js";
|
|
12
13
|
export { withCapEnforcement, withRollingCapEnforcement, } from "./with-cap-enforcement.js";
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// einzelnen Page — sonst verstopfen erledigte Rows das batchLimit-Fenster.
|
|
40
40
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
41
41
|
import { collectPiiSubjectFields, configuredPiiSubjectKms, isSelfPiiField, resolveSubjectForField, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
42
|
-
import { createEventStoreExecutor, createTenantDb, nullBlindIndexesForSubject, } from "@cosmicdrift/kumiko-framework/db";
|
|
42
|
+
import { createEventStoreExecutor, createTenantDb, nullBlindIndexesForSubject, WritePurposes, } from "@cosmicdrift/kumiko-framework/db";
|
|
43
43
|
import { deleteStoredFileAndDerivatives } from "@cosmicdrift/kumiko-framework/derivatives";
|
|
44
44
|
import { createSystemUser, entityHasAnonymizableSubjectField, } from "@cosmicdrift/kumiko-framework/engine";
|
|
45
45
|
import { fileRefEntity, fileRefsTable, } from "@cosmicdrift/kumiko-framework/files";
|
|
@@ -149,7 +149,7 @@ async function forgetRowAndEraseKeys(args) {
|
|
|
149
149
|
}
|
|
150
150
|
const NO_FILE_FIELDS = { singleFields: [], multiFields: [] };
|
|
151
151
|
// file/image = single-column fields (fileRef-id lives directly in the entity
|
|
152
|
-
// row); files/images = plural,
|
|
152
|
+
// row); files/images = plural, resolved via file_refs. Same set
|
|
153
153
|
// as boot-validator/entity-handler.ts FILE_FIELD_TYPES and engine's
|
|
154
154
|
// isFileField — inlined here rather than importing across the package
|
|
155
155
|
// barrel since this module already needs the single/multi split, not just
|
|
@@ -169,8 +169,8 @@ function planFileFields(fields) {
|
|
|
169
169
|
: NO_FILE_FIELDS;
|
|
170
170
|
}
|
|
171
171
|
// Every fileRef-id a row's file/image/files/images-fields reference: single
|
|
172
|
-
// fields hold the id in their own column, plural fields
|
|
173
|
-
//
|
|
172
|
+
// fields hold the id in their own column, plural fields resolve
|
|
173
|
+
// via file_refs (tenantId, entityType, entityId, fieldName).
|
|
174
174
|
async function collectFileRefIds(db, tenantId, entityName, row, plan) {
|
|
175
175
|
const ids = new Set();
|
|
176
176
|
for (const field of plan.singleFields) {
|
|
@@ -536,7 +536,7 @@ export async function runRetentionCleanup(args) {
|
|
|
536
536
|
break;
|
|
537
537
|
}
|
|
538
538
|
const executor = createEventStoreExecutor(proj.table, entity, { entityName });
|
|
539
|
-
softDeleted += await purgeMatchingRows(db, proj.table, { ...where, isDeleted: false }, batchLimit, (id) => executor.delete({ id }, systemUser, tdb));
|
|
539
|
+
softDeleted += await purgeMatchingRows(db, proj.table, { ...where, isDeleted: false }, batchLimit, (id) => executor.delete({ id }, systemUser, tdb, { writePurpose: WritePurposes.retention }));
|
|
540
540
|
break;
|
|
541
541
|
}
|
|
542
542
|
// blockDelete = Legal-Hold: rows stay untouched during keepFor (the
|
|
@@ -560,7 +560,10 @@ export async function runRetentionCleanup(args) {
|
|
|
560
560
|
break;
|
|
561
561
|
}
|
|
562
562
|
const executor = createEventStoreExecutor(proj.table, entity, { entityName });
|
|
563
|
-
anonymized += await anonymizeMatchingRows(db, proj.table, where, batchLimit, targets, (id, changes) => executor.update({ id, changes }, systemUser, tdb, {
|
|
563
|
+
anonymized += await anonymizeMatchingRows(db, proj.table, where, batchLimit, targets, (id, changes) => executor.update({ id, changes }, systemUser, tdb, {
|
|
564
|
+
skipOptimisticLock: true,
|
|
565
|
+
writePurpose: WritePurposes.retention,
|
|
566
|
+
}));
|
|
564
567
|
break;
|
|
565
568
|
}
|
|
566
569
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// consumers without the user-data-rights pipeline don't pull a hard dependency.
|
|
4
4
|
// Mirrors credit-user-data — standard tenant-scoped pattern, no name-stripping
|
|
5
5
|
// (a folder name is tenant data, not per-user PII).
|
|
6
|
+
import { WritePurposes } from "@cosmicdrift/kumiko-framework/db";
|
|
6
7
|
import { createEntityExecutor, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
8
|
import { folderAssignmentEntity, folderEntity } from "../folders/index.js";
|
|
8
9
|
const { table: folderTable, executor: folderExecutor } = createEntityExecutor("folder", folderEntity);
|
|
@@ -48,7 +49,9 @@ function tenantScopedDelete(table, executor) {
|
|
|
48
49
|
const systemUser = createSystemUser(ctx.tenantId);
|
|
49
50
|
const rows = await ctx.db.selectMany(table, { tenantId: ctx.tenantId });
|
|
50
51
|
for (const row of rows) {
|
|
51
|
-
await executor.delete({ id: row.id }, systemUser, ctx.db
|
|
52
|
+
await executor.delete({ id: row.id }, systemUser, ctx.db, {
|
|
53
|
+
writePurpose: WritePurposes.erasure,
|
|
54
|
+
});
|
|
52
55
|
}
|
|
53
56
|
};
|
|
54
57
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (GDPR Art. 20 export / Art. 17 erasure). Lives apart from form-draft so
|
|
3
3
|
// consumers without the user-data-rights pipeline don't pull a hard
|
|
4
4
|
// dependency.
|
|
5
|
+
import { WritePurposes } from "@cosmicdrift/kumiko-framework/db";
|
|
5
6
|
import { createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
7
|
import { formDraftExecutor, formDraftTable } from "../form-draft/index.js";
|
|
7
8
|
export const formDraftExportHook = async (ctx) => {
|
|
@@ -36,7 +37,9 @@ export const formDraftDeleteHook = async (ctx) => {
|
|
|
36
37
|
const systemUser = createSystemUser(ctx.tenantId);
|
|
37
38
|
const failures = [];
|
|
38
39
|
for (const row of rows) {
|
|
39
|
-
const result = await formDraftExecutor.delete({ id: row.id }, systemUser, ctx.db
|
|
40
|
+
const result = await formDraftExecutor.delete({ id: row.id }, systemUser, ctx.db, {
|
|
41
|
+
writePurpose: WritePurposes.erasure,
|
|
42
|
+
});
|
|
40
43
|
if (!result.isSuccess) {
|
|
41
44
|
failures.push(`${row.id}: ${result.error.message}`);
|
|
42
45
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
-
import {
|
|
2
|
+
import { collectPiiEncryptedFields, configuredPiiSubjectKms, encryptPiiFieldValues, KeyErasedError, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
3
|
import { subjectErasedConflict } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
// Unmanaged direct-write stores (r.unmanagedTable) skip the executor, so its
|
|
5
5
|
// PII encryption never runs — every insert of subject-annotated fields must
|
|
@@ -10,7 +10,7 @@ export async function encryptForDirectWrite(entity, entityName, row, fallbackReq
|
|
|
10
10
|
if (!kms)
|
|
11
11
|
return row;
|
|
12
12
|
try {
|
|
13
|
-
return await encryptPiiFieldValues(row, entity,
|
|
13
|
+
return await encryptPiiFieldValues(row, entity, collectPiiEncryptedFields(entity), kms, { requestId: requestContext.get()?.requestId ?? fallbackRequestId }, { entityName });
|
|
14
14
|
}
|
|
15
15
|
catch (e) {
|
|
16
16
|
if (e instanceof KeyErasedError)
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible,
|
|
|
14
14
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
15
15
|
export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
16
16
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
17
|
+
export { REDACTED_PII_PLACEHOLDER, redactPiiCiphertext } from "./redact-pii-ciphertext.js";
|
|
17
18
|
export { requireForTenant } from "./require-for-tenant.js";
|
|
18
19
|
export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
19
20
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
package/dist/shared/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible,
|
|
|
14
14
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
15
15
|
export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
16
16
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
17
|
+
export { REDACTED_PII_PLACEHOLDER, redactPiiCiphertext } from "./redact-pii-ciphertext.js";
|
|
17
18
|
export { requireForTenant } from "./require-for-tenant.js";
|
|
18
19
|
export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
19
20
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isPiiCiphertext } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
|
+
export const REDACTED_PII_PLACEHOLDER = "[[personal data]]";
|
|
3
|
+
// Event payloads store PII fields as subject-KMS ciphertext; the API leak guard
|
|
4
|
+
// rejects any response carrying it, so payload reads swap it for a placeholder.
|
|
5
|
+
export function redactPiiCiphertext(value) {
|
|
6
|
+
if (isPiiCiphertext(value))
|
|
7
|
+
return REDACTED_PII_PLACEHOLDER;
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return value.map(redactPiiCiphertext);
|
|
10
|
+
if (typeof value === "object" && value !== null) {
|
|
11
|
+
return Object.fromEntries(Object.entries(value).map(([key, inner]) => [key, redactPiiCiphertext(inner)]));
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
2
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
|
-
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { createEventStoreExecutor, createTenantDb, WritePurposes, } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import { createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
5
|
import { append, loadAggregate, VersionConflictError, } from "@cosmicdrift/kumiko-framework/event-store";
|
|
6
6
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
@@ -95,7 +95,7 @@ async function markTenantDestroyFailed(db, tenantId) {
|
|
|
95
95
|
await tenantCrud.update({
|
|
96
96
|
id: tenantId,
|
|
97
97
|
changes: { status: "destroyFailed" },
|
|
98
|
-
}, user, scopedDb, { skipOptimisticLock: true });
|
|
98
|
+
}, user, scopedDb, { skipOptimisticLock: true, writePurpose: WritePurposes.erasure });
|
|
99
99
|
invalidateTenantLifecycleGate(tenantId);
|
|
100
100
|
}
|
|
101
101
|
async function haltPipelineOnAbandon(args) {
|
|
@@ -240,7 +240,7 @@ export async function runTenantDestructionSweep(args) {
|
|
|
240
240
|
// tenant stream, so an unguarded overwrite here would silently revert the
|
|
241
241
|
// cancellation. A version conflict means we lost that race — skip, the
|
|
242
242
|
// tenant is no longer due and the next tick re-evaluates it.
|
|
243
|
-
const result = await tenantCrud.update({ id: tenantId, version, changes: { status: "destroying", destroyStartedAt: now } }, user, scopedDb);
|
|
243
|
+
const result = await tenantCrud.update({ id: tenantId, version, changes: { status: "destroying", destroyStartedAt: now } }, user, scopedDb, { writePurpose: WritePurposes.erasure });
|
|
244
244
|
if (!result.isSuccess)
|
|
245
245
|
continue;
|
|
246
246
|
invalidateTenantLifecycleGate(tenantId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { configuredPiiSubjectKms, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
|
-
import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { createEventStoreExecutor, createTenantDb, WritePurposes, } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import { createSystemUser, EXT_EXTERNAL_RESOURCE, EXT_INFRA_RESOURCE, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, extensionUsageEscapeHatchReason, isTenantDataExtensionHooks, isTenantResourceExtensionHooks, } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
5
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
6
6
|
import { createEscapeHatchReporter, UNATTRIBUTED_ACTOR, } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
@@ -132,7 +132,7 @@ async function tombstoneTenantRow(ctx) {
|
|
|
132
132
|
destroyedAt: now,
|
|
133
133
|
isEnabled: false,
|
|
134
134
|
},
|
|
135
|
-
}, user, db, { skipOptimisticLock: true });
|
|
135
|
+
}, user, db, { skipOptimisticLock: true, writePurpose: WritePurposes.erasure });
|
|
136
136
|
invalidateTenantLifecycleGate(ctx.tenantId);
|
|
137
137
|
return STAGE_DONE;
|
|
138
138
|
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
export declare const meQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"user:me", z.ZodObject<{}, z.core.$strip>,
|
|
2
|
+
export declare const meQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"user:me", z.ZodObject<{}, z.core.$strip>, {
|
|
3
|
+
effectiveTimeZone: string;
|
|
4
|
+
} | null, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
@@ -6,6 +6,7 @@ import { userEntity, userTable } from "../schema/user.js";
|
|
|
6
6
|
const crud = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
|
|
7
7
|
// Returns the currently signed-in user's profile. Field-level read access
|
|
8
8
|
// strips out the passwordHash automatically (configured on the entity).
|
|
9
|
+
// `effectiveTimeZone` is the effective IANA zone of the caller.
|
|
9
10
|
export const meQuery = defineQueryHandler({
|
|
10
11
|
name: "user:me",
|
|
11
12
|
schema: z.object({}),
|
|
@@ -26,6 +27,9 @@ export const meQuery = defineQueryHandler({
|
|
|
26
27
|
throw new InternalError({ message: "user:me requires r.systemScope()" });
|
|
27
28
|
}
|
|
28
29
|
const db = ctx.systemDb.acknowledgeCrossTenant("user rows are tenant-agnostic identity records; self-lookup by id needs no tenant filter");
|
|
29
|
-
|
|
30
|
+
const row = await crud.detail({ id: query.user.id }, query.user, db);
|
|
31
|
+
// `timezone` on the row is only the stored override (often unset); clients
|
|
32
|
+
// need the effective zone (override, else tenant, else UTC) to compute "today".
|
|
33
|
+
return row === null ? row : { ...row, effectiveTimeZone: ctx.tz.user };
|
|
30
34
|
},
|
|
31
35
|
});
|
|
@@ -13,7 +13,7 @@ export declare const myAuditLogQuery: import("@cosmicdrift/kumiko-types/define-h
|
|
|
13
13
|
aggregateType: string;
|
|
14
14
|
version: number;
|
|
15
15
|
type: string;
|
|
16
|
-
payload:
|
|
16
|
+
payload: unknown;
|
|
17
17
|
createdAt: unknown;
|
|
18
18
|
}[];
|
|
19
19
|
nextBefore: string | null;
|
|
@@ -3,6 +3,7 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
4
4
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
import * as z from "zod";
|
|
6
|
+
import { redactPiiCiphertext } from "../../shared/index.js";
|
|
6
7
|
// DSGVO Art. 15 Selbstauskunft — User reads HIS OWN audit-log.
|
|
7
8
|
// WHERE createdBy = ctx.user.id ist hard-coded (kein userId-Param,
|
|
8
9
|
// anti-cross-user-Snooping). KEIN tenantId-Filter: User hat Anspruch
|
|
@@ -63,7 +64,7 @@ export const myAuditLogQuery = defineQueryHandler({
|
|
|
63
64
|
aggregateType: r["aggregate_type"],
|
|
64
65
|
version: r["version"],
|
|
65
66
|
type: r["type"],
|
|
66
|
-
payload: r["payload"],
|
|
67
|
+
payload: redactPiiCiphertext(r["payload"]),
|
|
67
68
|
createdAt: r["created_at"],
|
|
68
69
|
}));
|
|
69
70
|
const last = serialised[serialised.length - 1];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { createEventStoreExecutor, WritePurposes, } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { deleteStoredFileAndDerivatives } from "@cosmicdrift/kumiko-framework/derivatives";
|
|
3
3
|
import { createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
@@ -184,6 +184,7 @@ async function severPersonLink(tdb, systemUser, rows) {
|
|
|
184
184
|
continue;
|
|
185
185
|
await crud.update({ id, changes: { insertedById: null } }, systemUser, tdb, {
|
|
186
186
|
skipOptimisticLock: true,
|
|
187
|
+
writePurpose: WritePurposes.erasure,
|
|
187
188
|
});
|
|
188
189
|
}
|
|
189
190
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { createEventStoreExecutor, WritePurposes } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { collectErasureFailure, decryptStoredPii, mapWithConcurrency, throwIfErasureFailed, } from "../../shared/index.js";
|
|
4
4
|
import { tenantInvitationEntity, tenantInvitationsTable } from "../../tenant/index.js";
|
|
@@ -95,7 +95,7 @@ export const tenantInvitationDeleteHook = async (ctx, strategy) => {
|
|
|
95
95
|
else {
|
|
96
96
|
// Row-id in the pseudonym keeps the (tenantId, email) unique index
|
|
97
97
|
// collision-free when a user has invitations in several states.
|
|
98
|
-
await crud.update({ id, changes: { email: `forgotten-${id}@anonymized.invalid` } }, systemUser, ctx.db, { skipOptimisticLock: true });
|
|
98
|
+
await crud.update({ id, changes: { email: `forgotten-${id}@anonymized.invalid` } }, systemUser, ctx.db, { skipOptimisticLock: true, writePurpose: WritePurposes.erasure });
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -115,6 +115,7 @@ export const tenantInvitationDeleteHook = async (ctx, strategy) => {
|
|
|
115
115
|
continue;
|
|
116
116
|
await crud.update({ id, changes: { invitedBy: INVITED_BY_ANONYMIZED } }, systemUser, ctx.db, {
|
|
117
117
|
skipOptimisticLock: true,
|
|
118
|
+
writePurpose: WritePurposes.erasure,
|
|
118
119
|
});
|
|
119
120
|
}
|
|
120
121
|
throwIfErasureFailed(failures);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { createEventStoreExecutor, WritePurposes } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { USER_ANONYMIZED_DISPLAY_NAME, USER_ANONYMIZED_EMAIL_DOMAIN, USER_ANONYMIZED_EMAIL_PREFIX, USER_DELETED_DISPLAY_NAME, USER_DELETED_EMAIL_PREFIX, USER_STATUS, userEntity, userTable, } from "../../user/index.js";
|
|
4
4
|
// Forget writes go through the executor (events), not a raw UPDATE: a
|
|
@@ -72,7 +72,7 @@ export const userDeleteHook = async (ctx, strategy) => {
|
|
|
72
72
|
// Frozen into the event payload, so a rebuild replays the same value.
|
|
73
73
|
deletedAt: Temporal.Now.instant(),
|
|
74
74
|
},
|
|
75
|
-
}, systemUser, ctx.db, { skipOptimisticLock: true });
|
|
75
|
+
}, systemUser, ctx.db, { skipOptimisticLock: true, writePurpose: WritePurposes.erasure });
|
|
76
76
|
}
|
|
77
77
|
else {
|
|
78
78
|
// anonymize: PII raus, aber Row bleibt active (damit FK-References
|
|
@@ -86,6 +86,6 @@ export const userDeleteHook = async (ctx, strategy) => {
|
|
|
86
86
|
email: `${USER_ANONYMIZED_EMAIL_PREFIX}-${ctx.userId}@${USER_ANONYMIZED_EMAIL_DOMAIN}`,
|
|
87
87
|
displayName: USER_ANONYMIZED_DISPLAY_NAME,
|
|
88
88
|
},
|
|
89
|
-
}, systemUser, ctx.db, { skipOptimisticLock: true });
|
|
89
|
+
}, systemUser, ctx.db, { skipOptimisticLock: true, writePurpose: WritePurposes.erasure });
|
|
90
90
|
}
|
|
91
91
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.361.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -520,12 +520,12 @@
|
|
|
520
520
|
}
|
|
521
521
|
},
|
|
522
522
|
"dependencies": {
|
|
523
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
524
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
525
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
526
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
527
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
528
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
523
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.361.0",
|
|
524
|
+
"@cosmicdrift/kumiko-framework": "0.361.0",
|
|
525
|
+
"@cosmicdrift/kumiko-headless": "0.361.0",
|
|
526
|
+
"@cosmicdrift/kumiko-renderer": "0.361.0",
|
|
527
|
+
"@cosmicdrift/kumiko-renderer-web": "0.361.0",
|
|
528
|
+
"@cosmicdrift/kumiko-types": "0.361.0",
|
|
529
529
|
"@mollie/api-client": "^4.5.0",
|
|
530
530
|
"@node-rs/argon2": "^2.0.2",
|
|
531
531
|
"@types/mailparser": "^3.4.6",
|
|
@@ -1059,8 +1059,8 @@
|
|
|
1059
1059
|
],
|
|
1060
1060
|
"devDependencies": {
|
|
1061
1061
|
"@testing-library/user-event": "^14.6.1",
|
|
1062
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
1063
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
1062
|
+
"@cosmicdrift/kumiko-locale-de": "0.361.0",
|
|
1063
|
+
"@cosmicdrift/kumiko-locale-es": "0.361.0",
|
|
1064
1064
|
"jsqr": "^1.4.0",
|
|
1065
1065
|
"unpdf": "^1.8.1"
|
|
1066
1066
|
}
|
package/src/audit/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.361.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Audit log works under an active subject KMS",
|
|
6
|
+
"detail": "audit list, audit details and user-data-rights my-audit-log returned event payloads as stored. With a subject KMS configured the payload carried kumiko-pii ciphertext and the API leak guard answered 500 pii_ciphertext_leak. Every PII ciphertext string in the payload is now replaced by REDACTED_PII_PLACEHOLDER (\"[[personal data]]\"); the audit log still never shows plaintext PII."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.353.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.361.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "cap-counter exports capReservationsTable and capReservationsTableMeta",
|
|
6
|
+
"detail": "The reservation table store_cap_reservations was only reachable through internal imports, so kumiko-enterprise redeclared it in a test. The cap-counter entry now exports capReservationsTable and capReservationsTableMeta, following the feature-toggles pattern."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.358.0",
|
|
4
10
|
"type": "improvement",
|
package/src/user/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.361.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "user:me returns effectiveTimeZone, SessionProvider provides the current user",
|
|
6
|
+
"detail": "The me query adds effectiveTimeZone (ctx.tz.user), CurrentUserProfile.effectiveTimeZone is parsed by fetchCurrentUser and SessionProvider wraps its children in CurrentUserProvider."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.354.0",
|
|
4
10
|
"type": "fix",
|