@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.
Files changed (30) hide show
  1. package/dist/audit/handlers/details.query.d.ts +1 -1
  2. package/dist/audit/handlers/details.query.js +2 -1
  3. package/dist/audit/handlers/list.query.d.ts +1 -1
  4. package/dist/audit/handlers/list.query.js +4 -5
  5. package/dist/auth-email-password/web/auth-client.d.ts +2 -0
  6. package/dist/auth-email-password/web/auth-client.js +3 -0
  7. package/dist/auth-email-password/web/session.js +19 -3
  8. package/dist/cap-counter/index.d.ts +1 -0
  9. package/dist/cap-counter/index.js +1 -0
  10. package/dist/data-retention/run-retention-cleanup.js +9 -6
  11. package/dist/folders-user-data/hooks.js +4 -1
  12. package/dist/form-draft-user-data/hooks.js +4 -1
  13. package/dist/shared/encrypt-for-direct-write.js +2 -2
  14. package/dist/shared/index.d.ts +1 -0
  15. package/dist/shared/index.js +1 -0
  16. package/dist/shared/redact-pii-ciphertext.d.ts +2 -0
  17. package/dist/shared/redact-pii-ciphertext.js +14 -0
  18. package/dist/tenant-lifecycle/run-tenant-destroy.js +3 -3
  19. package/dist/tenant-lifecycle/stages.js +2 -2
  20. package/dist/user/handlers/me.query.d.ts +3 -1
  21. package/dist/user/handlers/me.query.js +5 -1
  22. package/dist/user-data-rights/handlers/my-audit-log.query.d.ts +1 -1
  23. package/dist/user-data-rights/handlers/my-audit-log.query.js +2 -1
  24. package/dist/user-data-rights-defaults/hooks/file-ref.userdata-hook.js +2 -1
  25. package/dist/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.js +3 -2
  26. package/dist/user-data-rights-defaults/hooks/user.userdata-hook.js +3 -3
  27. package/package.json +9 -9
  28. package/src/audit/changes.json +6 -0
  29. package/src/cap-counter/changes.json +6 -0
  30. 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: Record<string, unknown>;
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: Record<string, unknown>;
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
- // Sensitive field-values are ciphertext inside the event payload (the log
13
- // carries them encrypted); stripSensitive only strips the event echo. This
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
- return _jsx(SessionContext.Provider, { value: api, children: children });
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, no column, resolved via file_refs. Same set
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 have no column and
173
- // resolve via file_refs (tenantId, entityType, entityId, fieldName).
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, { skipOptimisticLock: true }));
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 { collectPiiSubjectFields, configuredPiiSubjectKms, encryptPiiFieldValues, KeyErasedError, } from "@cosmicdrift/kumiko-framework/crypto";
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, collectPiiSubjectFields(entity), kms, { requestId: requestContext.get()?.requestId ?? fallbackRequestId }, { entityName });
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)
@@ -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";
@@ -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,2 @@
1
+ export declare const REDACTED_PII_PLACEHOLDER: "[[personal data]]";
2
+ export declare function redactPiiCiphertext(value: unknown): unknown;
@@ -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>, Record<string, unknown> | null, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
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
- return crud.detail({ id: query.user.id }, query.user, db); // @wrapper-known semantic-alias
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: Record<string, unknown>;
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.360.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.360.0",
524
- "@cosmicdrift/kumiko-framework": "0.360.0",
525
- "@cosmicdrift/kumiko-headless": "0.360.0",
526
- "@cosmicdrift/kumiko-renderer": "0.360.0",
527
- "@cosmicdrift/kumiko-renderer-web": "0.360.0",
528
- "@cosmicdrift/kumiko-types": "0.360.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.360.0",
1063
- "@cosmicdrift/kumiko-locale-es": "0.360.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
  }
@@ -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",
@@ -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",