@cosmicdrift/kumiko-bundled-features 0.165.2 → 0.165.3

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 (109) hide show
  1. package/package.json +7 -7
  2. package/src/__tests__/access-invalidation-consumer.integration.test.ts +1 -1
  3. package/src/__tests__/access-invalidation-stream.integration.test.ts +80 -66
  4. package/src/admin-shell/changes.json +8 -0
  5. package/src/agent-tools/changes.json +1 -0
  6. package/src/audit/__tests__/audit.integration.test.ts +5 -2
  7. package/src/audit/changes.json +8 -0
  8. package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +3 -0
  9. package/src/auth-email-password/changes.json +26 -0
  10. package/src/auth-email-password/web/__tests__/confirm-account-unlock-screen.test.tsx +3 -11
  11. package/src/auth-email-password/web/__tests__/fetch-mock.ts +14 -0
  12. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +3 -11
  13. package/src/auth-email-password/web/__tests__/request-account-unlock-screen.test.tsx +3 -11
  14. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +3 -11
  15. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +3 -11
  16. package/src/auth-foundation/changes.json +8 -0
  17. package/src/auth-mfa/__tests__/reencrypt-job.integration.test.ts +6 -6
  18. package/src/auth-mfa/changes.json +38 -0
  19. package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +3 -11
  20. package/src/auth-mfa-user-data/changes.json +1 -0
  21. package/src/billing-foundation/changes.json +1 -0
  22. package/src/cap-counter/changes.json +1 -0
  23. package/src/channel-email/changes.json +1 -0
  24. package/src/channel-in-app/changes.json +1 -0
  25. package/src/channel-push/changes.json +1 -0
  26. package/src/compliance-profiles/changes.json +1 -0
  27. package/src/config/changes.json +9 -0
  28. package/src/crypto-shredding/changes.json +8 -0
  29. package/src/crypto-shredding/handlers/forget-subject.write.ts +13 -0
  30. package/src/custom-fields/changes.json +8 -0
  31. package/src/data-retention/changes.json +8 -0
  32. package/src/delivery/changes.json +8 -0
  33. package/src/document-ingest-foundation/changes.json +1 -0
  34. package/src/feature-toggles/changes.json +26 -0
  35. package/src/file-foundation/changes.json +1 -0
  36. package/src/file-provider-inmemory/changes.json +1 -0
  37. package/src/file-provider-s3/changes.json +1 -0
  38. package/src/file-provider-s3-env/changes.json +1 -0
  39. package/src/files/changes.json +1 -0
  40. package/src/files-provider-s3/changes.json +1 -0
  41. package/src/folders/changes.json +1 -0
  42. package/src/folders-user-data/changes.json +1 -0
  43. package/src/foundation-shared/changes.json +1 -0
  44. package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +3 -1
  45. package/src/inbound-mail-foundation/changes.json +8 -0
  46. package/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
  47. package/src/inbound-provider-imap/changes.json +1 -0
  48. package/src/inbound-provider-inmemory/changes.json +1 -0
  49. package/src/jobs/__tests__/jobs-catalog.integration.test.ts +159 -0
  50. package/src/jobs/__tests__/jobs-screens.boot.test.ts +1 -0
  51. package/src/jobs/__tests__/jobs-security.integration.test.ts +4 -2
  52. package/src/jobs/changes.json +8 -0
  53. package/src/jobs/constants.ts +2 -0
  54. package/src/jobs/feature.ts +3 -1
  55. package/src/jobs/handlers/catalog.query.ts +43 -0
  56. package/src/jobs/handlers/trigger.write.ts +26 -2
  57. package/src/jobs/i18n.ts +33 -0
  58. package/src/jobs/web/__tests__/job-runs-screen.test.tsx +223 -0
  59. package/src/jobs/web/job-runs-screen.tsx +218 -84
  60. package/src/ledger/changes.json +1 -0
  61. package/src/legal-pages/changes.json +1 -0
  62. package/src/mail-foundation/changes.json +1 -0
  63. package/src/mail-transport-inmemory/changes.json +1 -0
  64. package/src/mail-transport-smtp/changes.json +1 -0
  65. package/src/managed-pages/changes.json +20 -0
  66. package/src/page-render/changes.json +14 -0
  67. package/src/personal-access-tokens/changes.json +8 -0
  68. package/src/presets/changes.json +1 -0
  69. package/src/presets/dsgvo-self-service.ts +1 -1
  70. package/src/rate-limiting/changes.json +1 -0
  71. package/src/readiness/changes.json +1 -0
  72. package/src/renderer-foundation/changes.json +1 -0
  73. package/src/renderer-simple/changes.json +1 -0
  74. package/src/secrets/changes.json +1 -0
  75. package/src/seo/changes.json +20 -0
  76. package/src/sessions/changes.json +8 -0
  77. package/src/sessions/handlers/revoke-all-for-user.write.ts +9 -1
  78. package/src/shared/changes.json +1 -0
  79. package/src/step-dispatcher/changes.json +1 -0
  80. package/src/subscription-mollie/changes.json +1 -0
  81. package/src/subscription-stripe/changes.json +1 -0
  82. package/src/tags/changes.json +1 -0
  83. package/src/template-resolver/changes.json +1 -0
  84. package/src/tenant/changes.json +27 -0
  85. package/src/tenant-lifecycle/changes.json +1 -0
  86. package/src/tenant-settings/changes.json +1 -0
  87. package/src/text-content/changes.json +14 -0
  88. package/src/tier-engine/changes.json +1 -0
  89. package/src/user/changes.json +1 -0
  90. package/src/user/schema/user.ts +8 -5
  91. package/src/user-data-rights/__tests__/audit-log.integration.test.ts +7 -3
  92. package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +0 -1
  93. package/src/user-data-rights/__tests__/download.integration.test.ts +0 -1
  94. package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +0 -1
  95. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +0 -1
  96. package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +0 -1
  97. package/src/user-data-rights/__tests__/request-export.integration.test.ts +0 -1
  98. package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +0 -2
  99. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +0 -1
  100. package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +0 -1
  101. package/src/user-data-rights/changes.json +21 -0
  102. package/src/user-data-rights/feature.ts +7 -6
  103. package/src/user-data-rights/handlers/lift-restriction.write.ts +4 -20
  104. package/src/user-data-rights/handlers/restrict-account.write.ts +4 -20
  105. package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +1 -0
  106. package/src/user-data-rights/lib/deny-if-target-outside-admin-tenant.ts +32 -0
  107. package/src/user-data-rights/run-forget-cleanup.ts +23 -0
  108. package/src/user-data-rights-defaults/changes.json +8 -0
  109. package/src/user-profile/changes.json +8 -0
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "version": "0.147.3",
4
+ "type": "fix",
5
+ "title": "`managed-pages`' `GET {basePath}/:slug` render route and `seo`'s `sitemap.xml`/`llms.txt`…",
6
+ "detail": "`managed-pages`' `GET {basePath}/:slug` render route and `seo`'s `sitemap.xml`/`llms.txt` routes now use the `systemQuery` in-process dispatch (introduced for `legal-pages` in the `resolverTrust` fix) instead of an internal `app.fetch` self-request with a forged `X-Tenant` header. That self-fetch carried no `Host` header (Host is only implied by the request URL, never stored in `Headers`), so a host-based `anonymousAccess.tenantResolver` reading only the header resolved `null` for the inner request. Under `resolverTrust: \"authoritative\"` there is no client-tenant fallback for a `null` resolution, so the route failed with `tenant_required` (503 \"page unavailable\" / empty sitemap entries) even for a legitimate, correctly-routed visitor. `systemQuery` forces the already host-resolved tenant in-process — no header round-trip, nothing that can go missing. Consumers running `managed-pages` or `seo` with `resolverTrust: \"authoritative\"` should upgrade — the render/discovery routes were not reliably servable under that mode before this fix. - @cosmicdrift/kumiko-framework@0.147.3 - @cosmicdrift/kumiko-dispatcher-live@0.147.3 - @cosmicdrift/kumiko-headless@0.147.3 - @cosmicdrift/kumiko-renderer@0.147.3 - @cosmicdrift/kumiko-renderer-web@0.147.3"
7
+ },
8
+ {
9
+ "version": "0.146.2",
10
+ "type": "fix",
11
+ "title": "seo: fix legal-pages/managed-pages URLs downgrading to http:// in sitemap.xml/llms.txt be…",
12
+ "detail": "seo: fix legal-pages/managed-pages URLs downgrading to http:// in sitemap.xml/llms.txt behind a TLS-terminating reverse proxy (#979 follow-up) — `requestHost()` trusted the raw request URL's scheme, which reflects the proxy's internal (plain HTTP) hop, not what the client actually used. Now prefers `x-forwarded-proto` when present, falling back to the raw URL scheme only for plain local dev without a proxy in front. Found by checking the sitemap.xml of the two production apps (cashcolt.kumiko.rocks, publicstatus.eu) that mount `seo` — both showed `http://` legal-pages entries next to correctly `https://` app-supplied entries (those app callbacks hardcode `https://` themselves, masking the bug). - @cosmicdrift/kumiko-framework@0.146.2 - @cosmicdrift/kumiko-dispatcher-live@0.146.2 - @cosmicdrift/kumiko-headless@0.146.2 - @cosmicdrift/kumiko-renderer@0.146.2 - @cosmicdrift/kumiko-renderer-web@0.146.2"
13
+ },
14
+ {
15
+ "version": "0.146.0",
16
+ "type": "improvement",
17
+ "title": "seo: neues bundled-feature für SEO/AEO/GEO-Site-Discovery (#979) — `createSeoFeature` mou…",
18
+ "detail": "seo: neues bundled-feature für SEO/AEO/GEO-Site-Discovery (#979) — `createSeoFeature` mountet GET /sitemap.xml, /llms.txt und optional /robots.txt (merged aus app-eigenem Callback + legal-pages + managed-pages), plus die additive OG/JSON-LD-Erweiterung für `wrapInLayout` und die pure `organizationSchema`/`webPageSchema`/`faqPageSchema`-Builder für `ApexHead.schemaJson`. `managed-pages` bekommt dafür die neue anonyme `by-tenant-published`-Query. War Teil von #979, hatte aber kein Changeset — daher hier nachgereicht, damit die Version tatsächlich published wird."
19
+ }
20
+ ]
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "version": "0.165.0",
4
+ "type": "improvement",
5
+ "title": "Revoke handlers emit session-revoked event",
6
+ "detail": "Session revocation now appends a session-revoked event to the aggregate stream — other features can react to it (e.g. cache invalidation)."
7
+ }
8
+ ]
@@ -1,3 +1,4 @@
1
+ import { requestContext } from "@cosmicdrift/kumiko-framework/api";
1
2
  import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
3
  import { access, defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import { append } from "@cosmicdrift/kumiko-framework/event-store";
@@ -58,14 +59,21 @@ export const revokeAllForUserWrite = defineWriteHandler({
58
59
  userId: event.payload.userId,
59
60
  sessionIds: updated.map((row) => row.id),
60
61
  });
62
+ const reqCtx = requestContext.get();
61
63
  await append(ctx.db.raw, {
62
64
  aggregateId: generateId(),
63
65
  aggregateType: SESSION_REVOKED_AGGREGATE_TYPE,
64
66
  tenantId: SYSTEM_TENANT_ID,
65
67
  expectedVersion: 0,
66
68
  type: SESSION_REVOKED_EVENT_QN,
69
+ eventVersion: ctx.registry.getEvent(SESSION_REVOKED_EVENT_QN)?.version ?? 1,
67
70
  payload,
68
- metadata: { userId: event.user.id },
71
+ metadata: {
72
+ userId: event.user.id,
73
+ ...(reqCtx?.requestId ? { requestId: reqCtx.requestId } : {}),
74
+ ...(reqCtx?.correlationId ? { correlationId: reqCtx.correlationId } : {}),
75
+ ...(reqCtx?.causationId ? { causationId: reqCtx.causationId } : {}),
76
+ },
69
77
  });
70
78
  }
71
79
 
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1,27 @@
1
+ [
2
+ {
3
+ "version": "0.165.0",
4
+ "type": "fix",
5
+ "title": "resolveRawDb: removed redundant null check",
6
+ "detail": "CodeQL flagged db !== null after if (!db) — truthiness already excludes null. Internal cleanup, no API break."
7
+ },
8
+ {
9
+ "version": "0.165.0",
10
+ "type": "fix",
11
+ "title": "Seed hook: explicit SessionUser typing",
12
+ "detail": "Fixed tenant seeding type annotation — prevents implicit any propagation in seed handlers."
13
+ },
14
+ {
15
+ "version": "0.165.0",
16
+ "type": "fix",
17
+ "title": "Seed afterCommit + real tenant rescope",
18
+ "detail": "ScheduleAfterCommit now available on HandlerContext — fireEntityPostSave queues after TX commit instead of mid-transaction. DB/notify/hasFeature/_tenantId are correctly rescoped when targeting an entity's tenant.",
19
+ "migration": "Only affects custom seed handlers using fireEntityPostSave — now afterCommit-compatible."
20
+ },
21
+ {
22
+ "version": "0.163.0",
23
+ "type": "improvement",
24
+ "title": "tenant: seedTenant fires entity postSave hooks (#1463).",
25
+ "detail": "tenant: seedTenant fires entity postSave hooks (#1463). `seedTenant` writes through the raw event-store executor, which never fired postSave hooks — self-signup (`provisionSignupAccount` → `signup-confirm`) silently skipped feature-registered entity hooks like tier-engine's auto-default-tier and app-level auto-default-compliance, leaving new tenants without a tier/compliance row. `seedTenant` and `provisionSignupAccount` take a new optional `hooks: { registry, context }` param; passing it fires the same entity-scoped postSave hooks the dispatcher would, without the full dispatch roundtrip. Existing call-sites are unaffected — the param is optional and append-only."
26
+ }
27
+ ]
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "version": "0.165.0",
4
+ "type": "fix",
5
+ "title": "Legal pages seed: SYSTEM_USER instead of tenant user",
6
+ "detail": "Seed now runs as SYSTEM_USER and skips identical re-seeds — prevents unnecessary DB writes."
7
+ },
8
+ {
9
+ "version": "0.130.2",
10
+ "type": "fix",
11
+ "title": "Content-Tree + Config-Nav Sysadmin-Shell polish: - text-content: Leaf-Knoten tragen jetzt…",
12
+ "detail": "Content-Tree + Config-Nav Sysadmin-Shell polish: - text-content: Leaf-Knoten tragen jetzt ein `file`-Icon statt eines Dots; der Editor läuft auf der Page-Shell (`Form`-Primitive mit Card statt des entfernten `FormPanelShell`). - Sidebar-Nav bekommt ein Suchfeld, das den Baum live filtert (Treffer + ihre Ancestors bleiben, zugeklappte Ordner öffnen für die Suche). - Ordner-Knoten zeigen `folder-open` wenn ausgeklappt. - NAV_ICONS um `server`, `mail`, `lock`, `hash`, `download`, `folder-open` ergänzt — SMTP-/Config-Nav-Kinder (z.B. „Email-Versand\") rendern damit ein Icon statt blank. - Verschachtelte Provider-Ordner (Content-Tree) rendern ihre Kinder in einem `<ul>` (valides HTML + Einrück-Stufe pro Tiefe) statt `<li>`-in-`<li>`. - Platform-Overview: `user:query:user:list` in der Allowlist (behebt den Overview-Crash). - @cosmicdrift/kumiko-renderer-web@0.130.2 - @cosmicdrift/kumiko-renderer@0.130.2 - @cosmicdrift/kumiko-framework@0.130.2 - @cosmicdrift/kumiko-dispatcher-live@0.130.2 - @cosmicdrift/kumiko-headless@0.130.2"
13
+ }
14
+ ]
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1 @@
1
+ []
@@ -79,11 +79,14 @@ export const userEntity = createEntity({
79
79
  access: { read: access.privileged, write: access.privileged },
80
80
  }),
81
81
 
82
- // Profile — user-editable. Display-name is real-name in most apps,
83
- // so treat as PII for DSGVO export/forget pipelines. NOT searchable:
84
- // substring search on an encrypted field would require plaintext copies
85
- // in the search index (boot-validator rejects the combination, #818).
86
- displayName: createTextField({ required: true, maxLength: 100, pii: true }),
82
+ // Profile — user-editable. Real name in most apps → PII. Searchable via
83
+ // the derived Meili index; events/projection stay ciphertext (#1610).
84
+ displayName: createTextField({
85
+ required: true,
86
+ maxLength: 100,
87
+ pii: true,
88
+ searchable: true,
89
+ }),
87
90
  locale: createTextField({ maxLength: 10, default: "de" }),
88
91
 
89
92
  // Which tenant should this user land in on next login. Set by the login
@@ -44,7 +44,6 @@ beforeAll(async () => {
44
44
  createComplianceProfilesFeature(),
45
45
  authFoundationFeature,
46
46
  createSessionsFeature(),
47
-
48
47
  createUserDataRightsFeature(),
49
48
  ],
50
49
  });
@@ -128,8 +127,13 @@ describe("my-audit-log", () => {
128
127
  expect(bobLog.rows.length).toBe(1);
129
128
  // Payload-pinning beweist die Cross-User-Filterung: alice sieht nur
130
129
  // ihre Payload, bob nur seine.
131
- expect((aliceLog.rows[0]?.payload as { foo: string }).foo).toBe("alice");
132
- expect((bobLog.rows[0]?.payload as { foo: string }).foo).toBe("bob");
130
+ const aliceRow = aliceLog.rows[0];
131
+ const bobRow = bobLog.rows[0];
132
+ expect(aliceRow).toBeDefined();
133
+ expect(bobRow).toBeDefined();
134
+ if (!aliceRow || !bobRow) throw new Error("expected audit rows");
135
+ expect((aliceRow.payload as { foo: string }).foo).toBe("alice");
136
+ expect((bobRow.payload as { foo: string }).foo).toBe("bob");
133
137
  });
134
138
 
135
139
  test("Account-weite Sicht: User sieht events aus anderen Tenants (DSGVO Art. 15)", async () => {
@@ -121,7 +121,6 @@ beforeAll(async () => {
121
121
  createComplianceProfilesFeature(),
122
122
  authFoundationFeature,
123
123
  createSessionsFeature(),
124
-
125
124
  createUserDataRightsFeature(),
126
125
  createUserDataRightsDefaultsFeature(),
127
126
  testNotesFeature,
@@ -118,7 +118,6 @@ beforeAll(async () => {
118
118
  noSignedUrlProviderFeature,
119
119
  authFoundationFeature,
120
120
  createSessionsFeature(),
121
-
122
121
  createUserDataRightsFeature(),
123
122
  ],
124
123
  extraContext: ({ registry }) => ({
@@ -46,7 +46,6 @@ beforeAll(async () => {
46
46
  createComplianceProfilesFeature(),
47
47
  authFoundationFeature,
48
48
  createSessionsFeature(),
49
-
50
49
  createUserDataRightsFeature(),
51
50
  ],
52
51
  });
@@ -60,7 +60,6 @@ const features = [
60
60
  createComplianceProfilesFeature(),
61
61
  authFoundationFeature,
62
62
  createSessionsFeature(),
63
-
64
63
  createUserDataRightsFeature(),
65
64
  ];
66
65
 
@@ -67,7 +67,6 @@ beforeAll(async () => {
67
67
  createComplianceProfilesFeature(),
68
68
  authFoundationFeature,
69
69
  createSessionsFeature(),
70
-
71
70
  createUserDataRightsFeature({ sendDeletionRequestedEmail }),
72
71
  ],
73
72
  });
@@ -56,7 +56,6 @@ beforeAll(async () => {
56
56
  createComplianceProfilesFeature(),
57
57
  authFoundationFeature,
58
58
  createSessionsFeature(),
59
-
60
59
  createUserDataRightsFeature(),
61
60
  ],
62
61
  });
@@ -62,7 +62,6 @@ beforeAll(async () => {
62
62
  createComplianceProfilesFeature(),
63
63
  authFoundationFeature,
64
64
  createSessionsFeature(),
65
-
66
65
  createUserDataRightsFeature(),
67
66
  ],
68
67
  });
@@ -969,7 +968,6 @@ describe("runExportJobs :: Atom 3c file-binaries", () => {
969
968
  createComplianceProfilesFeature(),
970
969
  authFoundationFeature,
971
970
  createSessionsFeature(),
972
-
973
971
  createUserDataRightsFeature(),
974
972
  testFileExporter,
975
973
  ],
@@ -70,7 +70,6 @@ beforeAll(async () => {
70
70
  createComplianceProfilesFeature(),
71
71
  authFoundationFeature,
72
72
  createSessionsFeature(),
73
-
74
73
  createUserDataRightsFeature(),
75
74
  createUserDataRightsDefaultsFeature(),
76
75
  ],
@@ -64,7 +64,6 @@ beforeAll(async () => {
64
64
  createComplianceProfilesFeature(),
65
65
  authFoundationFeature,
66
66
  createSessionsFeature(),
67
-
68
67
  createUserDataRightsFeature(),
69
68
  createUserDataRightsDefaultsFeature(),
70
69
  ],
@@ -0,0 +1,21 @@
1
+ [
2
+ {
3
+ "version": "0.165.0",
4
+ "type": "breaking",
5
+ "title": "Export download: removed ?token= query param",
6
+ "detail": "GET /user-export/by-token no longer accepts the token as a query parameter. Only POST body (read from URL fragment) is supported now. Old email links with ?token= in the URL will stop working.",
7
+ "migration": "Replace any existing export links that use ?token= in the URL with POST-based links. The token is now only read from the URL fragment (#token), which browsers never send to the server."
8
+ },
9
+ {
10
+ "version": "0.122.0",
11
+ "type": "fix",
12
+ "title": "user-data-rights: export-download-token re-runs rotate the token in place on the same agg…",
13
+ "detail": "user-data-rights: export-download-token re-runs rotate the token in place on the same aggregate (update) instead of creating a second aggregate for the same jobId. A second `created` event without a `deleted` in between made every projection rebuild collide on the `one_per_job` unique index (fw#832). Operator recovery after a lost plain token is now just \"flip the job back to pending\" — never delete the token row read-side. - @cosmicdrift/kumiko-framework@0.122.0 - @cosmicdrift/kumiko-headless@0.122.0 - @cosmicdrift/kumiko-renderer@0.122.0 - @cosmicdrift/kumiko-dispatcher-live@0.122.0 - @cosmicdrift/kumiko-renderer-web@0.122.0"
14
+ },
15
+ {
16
+ "version": "0.119.0",
17
+ "type": "improvement",
18
+ "title": "Crypto-shredding phase D: forget wire.",
19
+ "detail": "Crypto-shredding phase D: forget wire. New `crypto-shredding` bundled feature with the `forget-subject` operator command (DPO/SystemAdmin) — erases a user/tenant subject key and appends a `subject-forgotten` audit event. `user-data-rights` forget-cleanup now erases the user's subject key inside the per-user sub-tx (crash-safe, before the status flip). Fixes `list()` returning ciphertext for camelCase encrypted/pii fields and caching plaintext rows."
20
+ }
21
+ ]
@@ -309,13 +309,13 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
309
309
  // afterward (a redirect doesn't undo an already-logged request) — the
310
310
  // only leak-proof fix is to never accept the token that way.
311
311
  //
312
- // Path liegt AUSSERHALB /api/* weil r.httpRoute den /api-namespace nicht
313
- // claimen darf (reserved fuer write/query/batch/auth/sse-dispatcher).
312
+ // Path sits outside /api/* — r.httpRoute must not claim the /api namespace
313
+ // (reserved for write/query/batch/auth/sse-dispatcher).
314
314
  //
315
- // Der Session-Pfad (eingeloggter Download) braucht KEINEN httpRoute-
316
- // Wrapper: der Client ruft download-by-job direkt via Dispatcher (traegt
317
- // X-CSRF-Token mit) und navigiert auf die zurueckgegebene signed-URL —
318
- // siehe postWithDownload im privacy-center-screen.
315
+ // The session path (logged-in download) needs no httpRoute wrapper: the
316
+ // client calls download-by-job via the Dispatcher (with X-CSRF-Token) and
317
+ // navigates to the returned signed URL — see postWithDownload in
318
+ // privacy-center-screen.
319
319
  r.httpRoute({
320
320
  method: "GET",
321
321
  path: "/user-export/by-token",
@@ -524,6 +524,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
524
524
  registry: forgetRegistry,
525
525
  now: T.Now.instant(),
526
526
  tenantModel,
527
+ ...(ctx.searchAdapter !== undefined && { searchAdapter: ctx.searchAdapter }),
527
528
  // Same per-tenant provider resolution as the export cron — forget
528
529
  // deletes binaries from the store upload + export use.
529
530
  buildStorageProvider: makeTenantStorageProviderResolver({
@@ -1,14 +1,9 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
- import {
4
- AccessDeniedError,
5
- UnprocessableError,
6
- writeFailure,
7
- } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
8
4
  import { z } from "zod";
9
- import { tenantMembershipsTable } from "../../tenant";
10
5
  import { USER_STATUS, userTable } from "../../user";
11
- import { isSystemAdminActor } from "../lib/is-admin-actor";
6
+ import { denyIfTargetOutsideAdminTenant } from "../lib/deny-if-target-outside-admin-tenant";
12
7
  import { updateUserLifecycle } from "../lib/update-user-lifecycle";
13
8
 
14
9
  // POST /api/user/lift-restriction (S2.U6) — DSGVO Art. 18 Reverse.
@@ -41,19 +36,8 @@ export const liftRestrictionWrite = defineWriteHandler({
41
36
  handler: async (event, ctx) => {
42
37
  const targetUserId = event.payload.userId;
43
38
 
44
- if (!isSystemAdminActor(event.user)) {
45
- const membership = await fetchOne(ctx.db.raw, tenantMembershipsTable, {
46
- userId: targetUserId,
47
- tenantId: event.user.tenantId,
48
- });
49
- if (!membership) {
50
- return writeFailure(
51
- new AccessDeniedError({
52
- details: { reason: "target_user_not_in_admin_tenant" },
53
- }),
54
- );
55
- }
56
- }
39
+ const outside = await denyIfTargetOutsideAdminTenant(ctx.db.raw, event.user, targetUserId);
40
+ if (outside) return outside;
57
41
 
58
42
  const userRow = await fetchOne<{ status: string }>(ctx.db.raw, userTable, {
59
43
  id: targetUserId,
@@ -6,9 +6,9 @@ import {
6
6
  writeFailure,
7
7
  } from "@cosmicdrift/kumiko-framework/errors";
8
8
  import { z } from "zod";
9
- import { tenantMembershipsTable } from "../../tenant";
10
9
  import { USER_STATUS, userTable } from "../../user";
11
- import { isAdminActor, isSystemAdminActor } from "../lib/is-admin-actor";
10
+ import { denyIfTargetOutsideAdminTenant } from "../lib/deny-if-target-outside-admin-tenant";
11
+ import { isAdminActor } from "../lib/is-admin-actor";
12
12
  import { updateUserLifecycle } from "../lib/update-user-lifecycle";
13
13
 
14
14
  // POST /api/user/restrict (S2.U6) — DSGVO Art. 18 Account-Freeze.
@@ -56,24 +56,8 @@ export const restrictAccountWrite = defineWriteHandler({
56
56
  }),
57
57
  );
58
58
  }
59
- // Same cross-tenant guard as lift-restriction.write.ts: only
60
- // SystemAdmin (platform-wide) may target a user without an active
61
- // membership in the caller's own tenant. Without this, a TenantAdmin
62
- // from tenant A could restrict — and force-revoke the sessions of —
63
- // a user who has never been a member of tenant A.
64
- if (!isSystemAdminActor(event.user)) {
65
- const membership = await fetchOne(ctx.db.raw, tenantMembershipsTable, {
66
- userId: targetUserId,
67
- tenantId: event.user.tenantId,
68
- });
69
- if (!membership) {
70
- return writeFailure(
71
- new AccessDeniedError({
72
- details: { reason: "target_user_not_in_admin_tenant" },
73
- }),
74
- );
75
- }
76
- }
59
+ const outside = await denyIfTargetOutsideAdminTenant(ctx.db.raw, event.user, targetUserId);
60
+ if (outside) return outside;
77
61
  }
78
62
 
79
63
  // ctx.db.raw weil User-Entity tenant-agnostisch ist (analog
@@ -57,6 +57,7 @@ export function createRunForgetCleanupHandler(opts: RunForgetCleanupOptions = {}
57
57
  registry: ctx.registry,
58
58
  now: T.Now.instant(),
59
59
  tenantModel,
60
+ ...(ctx.searchAdapter !== undefined && { searchAdapter: ctx.searchAdapter }),
60
61
  buildStorageProvider: makeTenantStorageProviderResolver({
61
62
  registry: ctx.registry,
62
63
  configResolver: ctx.configResolver,
@@ -0,0 +1,32 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
3
+ import {
4
+ AccessDeniedError,
5
+ type WriteFailure,
6
+ writeFailure,
7
+ } from "@cosmicdrift/kumiko-framework/errors";
8
+ import { tenantMembershipsTable } from "../../tenant";
9
+ import { isSystemAdminActor } from "./is-admin-actor";
10
+
11
+ export const TARGET_USER_NOT_IN_ADMIN_TENANT = "target_user_not_in_admin_tenant" as const;
12
+
13
+ type DbRaw = Parameters<typeof fetchOne>[0];
14
+
15
+ /** SystemAdmin skips; otherwise the target must have a membership in the actor's tenant. */
16
+ export async function denyIfTargetOutsideAdminTenant(
17
+ db: DbRaw,
18
+ actor: SessionUser,
19
+ targetUserId: string,
20
+ ): Promise<WriteFailure | undefined> {
21
+ if (isSystemAdminActor(actor)) return undefined;
22
+ const membership = await fetchOne(db, tenantMembershipsTable, {
23
+ userId: targetUserId,
24
+ tenantId: actor.tenantId,
25
+ });
26
+ if (membership) return undefined;
27
+ return writeFailure(
28
+ new AccessDeniedError({
29
+ details: { reason: TARGET_USER_NOT_IN_ADMIN_TENANT },
30
+ }),
31
+ );
32
+ }
@@ -49,6 +49,10 @@ import {
49
49
  type UserDataDeleteStrategy,
50
50
  type UserDataStorageProvider,
51
51
  } from "@cosmicdrift/kumiko-framework/engine";
52
+ import {
53
+ purgeSearchDocumentsForSubject,
54
+ type SearchAdapter,
55
+ } from "@cosmicdrift/kumiko-framework/search";
52
56
  import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
53
57
  import { resolveRetentionPolicyForTenant } from "../data-retention";
54
58
  import { decryptStoredPii } from "../shared";
@@ -116,6 +120,13 @@ export interface RunForgetCleanupArgs {
116
120
  */
117
121
  readonly kms?: KmsAdapter;
118
122
 
123
+ /**
124
+ * Search adapter for purging derived Meili docs of searchable subject-PII
125
+ * (#1610). Same seam as forget-subject. Omitted → no search purge (apps
126
+ * without Meili).
127
+ */
128
+ readonly searchAdapter?: SearchAdapter;
129
+
119
130
  /**
120
131
  * App-level tenant-occupancy model (resolved from the `tenantModel` config by
121
132
  * the cron/handler). The pipeline refines it PER TENANT with a sole-member
@@ -235,6 +246,7 @@ export async function runForgetCleanup(
235
246
  buildStorageProvider,
236
247
  appTenantModel,
237
248
  kms,
249
+ searchAdapter: args.searchAdapter,
238
250
  });
239
251
  hookCallsAttempted += userResult.hookCallsAttempted;
240
252
  errors.push(...userResult.errors);
@@ -298,6 +310,7 @@ async function processUser(args: {
298
310
  buildStorageProvider?: (tenantId: TenantId) => Promise<UserDataStorageProvider | undefined>;
299
311
  appTenantModel: TenantUserModel;
300
312
  kms?: KmsAdapter;
313
+ searchAdapter?: SearchAdapter;
301
314
  }): Promise<ProcessUserResult> {
302
315
  const { db, registry, userId, hookEntries, buildStorageProvider, appTenantModel, kms } = args;
303
316
  const errors: ForgetCleanupError[] = [];
@@ -422,6 +435,16 @@ async function processUser(args: {
422
435
  registry.features,
423
436
  subjectIdToKey({ kind: "user", userId }),
424
437
  );
438
+ if (args.searchAdapter) {
439
+ const subject = { kind: "user" as const, userId };
440
+ await purgeSearchDocumentsForSubject(
441
+ tx,
442
+ registry.features,
443
+ args.searchAdapter,
444
+ subjectIdToKey(subject),
445
+ subject,
446
+ );
447
+ }
425
448
  }
426
449
 
427
450
  // Status-Flip in derselben Sub-Tx. Falls einer der Hooks oben
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "version": "0.116.0",
4
+ "type": "fix",
5
+ "title": "user-data-rights-defaults now registers EXT_USER_DATA export/delete hooks for six more bu…",
6
+ "detail": "user-data-rights-defaults now registers EXT_USER_DATA export/delete hooks for six more bundled entities: user-session (ip/userAgent), api-token, in-app-message, tenant-invitation, notification-preference and user-scoped config-value. Hooks no-op when the source feature isn't mounted. pii annotations added on the affected schema fields. - @cosmicdrift/kumiko-renderer-web@0.116.0 - @cosmicdrift/kumiko-framework@0.116.0 - @cosmicdrift/kumiko-dispatcher-live@0.116.0 - @cosmicdrift/kumiko-headless@0.116.0 - @cosmicdrift/kumiko-renderer@0.116.0"
7
+ }
8
+ ]
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "version": "0.123.2",
4
+ "type": "fix",
5
+ "title": "Consistency: user-profile's ProfileScreen adopts the shared `FormScreenShell` (centered `…",
6
+ "detail": "Consistency: user-profile's ProfileScreen adopts the shared `FormScreenShell` (centered `max-w-3xl` like all other settings screens, was left-aligned `max-w-5xl`). DataTable now sits on `bg-card` instead of a transparent surface — on themes with a colored page background (e.g. cream) lists previously didn't match the white cards; now they do. - @cosmicdrift/kumiko-renderer-web@0.123.2 - @cosmicdrift/kumiko-framework@0.123.2 - @cosmicdrift/kumiko-dispatcher-live@0.123.2 - @cosmicdrift/kumiko-headless@0.123.2 - @cosmicdrift/kumiko-renderer@0.123.2"
7
+ }
8
+ ]