@cosmicdrift/kumiko-bundled-features 0.240.0 → 0.242.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 (72) hide show
  1. package/package.json +9 -9
  2. package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +16 -7
  3. package/src/admin-shell/__tests__/overview-allowlist.test.ts +30 -0
  4. package/src/admin-shell/__tests__/overview-screens.boot.test.ts +31 -23
  5. package/src/admin-shell/changes.json +6 -0
  6. package/src/admin-shell/feature.ts +60 -6
  7. package/src/admin-shell/i18n.ts +0 -7
  8. package/src/admin-shell/web/client-plugin.tsx +1 -11
  9. package/src/agent-tools/__tests__/agent-doc-lint.test.ts +40 -0
  10. package/src/agent-tools/__tests__/agent-manifest.test.ts +36 -0
  11. package/src/agent-tools/__tests__/tool-catalog.test.ts +51 -0
  12. package/src/agent-tools/agent-doc-lint.ts +14 -3
  13. package/src/agent-tools/agent-manifest.ts +16 -3
  14. package/src/agent-tools/feature.ts +1 -1
  15. package/src/audit/__tests__/audit-screens.boot.test.ts +26 -6
  16. package/src/audit/changes.json +7 -0
  17. package/src/audit/feature.ts +57 -7
  18. package/src/audit/handlers/list.query.ts +50 -14
  19. package/src/audit/i18n.ts +3 -18
  20. package/src/audit/web/client-plugin.tsx +1 -7
  21. package/src/audit/web/index.ts +0 -2
  22. package/src/config/__tests__/config.integration.test.ts +31 -0
  23. package/src/config/handlers/readiness.query.ts +8 -1
  24. package/src/jobs/__tests__/jobs-screens.boot.test.ts +7 -2
  25. package/src/jobs/__tests__/jobs-security.integration.test.ts +41 -2
  26. package/src/jobs/changes.json +7 -0
  27. package/src/jobs/feature.ts +101 -9
  28. package/src/jobs/handlers/list.query.ts +21 -4
  29. package/src/jobs/handlers/trigger.write.ts +16 -3
  30. package/src/jobs/i18n.ts +3 -25
  31. package/src/jobs/index.ts +1 -0
  32. package/src/jobs/web/client-plugin.tsx +1 -7
  33. package/src/jobs/web/index.ts +0 -2
  34. package/src/tenant/__tests__/tenant.integration.test.ts +32 -0
  35. package/src/tenant/handlers/list.query.ts +1 -0
  36. package/src/tier-engine/__tests__/drift.test.ts +1 -1
  37. package/src/tier-engine/__tests__/tier-engine-screens.boot.test.ts +82 -0
  38. package/src/tier-engine/changes.json +9 -1
  39. package/src/tier-engine/constants.ts +5 -5
  40. package/src/tier-engine/feature.ts +36 -10
  41. package/src/tier-engine/i18n.ts +19 -22
  42. package/src/tier-engine/web/client-plugin.tsx +5 -11
  43. package/src/tier-engine/web/index.ts +0 -1
  44. package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +99 -0
  45. package/src/user-data-rights/changes.json +7 -0
  46. package/src/user-data-rights/constants.ts +20 -6
  47. package/src/user-data-rights/feature.ts +113 -14
  48. package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +29 -71
  49. package/src/user-data-rights/web/client-plugin.tsx +19 -21
  50. package/src/user-data-rights/web/i18n.ts +6 -11
  51. package/src/user-data-rights/web/index.ts +1 -1
  52. package/src/user-data-rights/web/privacy-center-screen.tsx +32 -245
  53. package/src/user-profile/__tests__/profile-screen.boot.test.ts +97 -0
  54. package/src/user-profile/__tests__/profile-screen.test.tsx +140 -104
  55. package/src/user-profile/changes.json +7 -0
  56. package/src/user-profile/constants.ts +15 -7
  57. package/src/user-profile/feature.ts +96 -9
  58. package/src/user-profile/i18n.ts +8 -12
  59. package/src/user-profile/web/client-plugin.ts +22 -5
  60. package/src/user-profile/web/index.ts +1 -1
  61. package/src/user-profile/web/profile-screen.tsx +122 -265
  62. package/src/admin-shell/web/overview-layout.tsx +0 -69
  63. package/src/admin-shell/web/overview-query.ts +0 -15
  64. package/src/admin-shell/web/platform-overview-screen.tsx +0 -91
  65. package/src/admin-shell/web/tenant-overview-screen.tsx +0 -91
  66. package/src/audit/__tests__/audit-log-screen.test.tsx +0 -302
  67. package/src/audit/web/audit-log-detail-screen.tsx +0 -169
  68. package/src/audit/web/audit-log-screen.tsx +0 -290
  69. package/src/jobs/web/__tests__/job-runs-screen.test.tsx +0 -225
  70. package/src/jobs/web/job-run-detail-screen.tsx +0 -181
  71. package/src/jobs/web/job-runs-screen.tsx +0 -290
  72. package/src/tier-engine/web/tier-admin-screen.tsx +0 -168
@@ -7,7 +7,13 @@ import {
7
7
  SYSTEM_USER_ID,
8
8
  } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { validateGdprHookCompleteness, validateGdprPiiHookCoverage } from "./boot-checks";
10
- import { PRIVACY_CENTER_SCREEN_ID } from "./constants";
10
+ import {
11
+ EXPORT_SECTION_EXTENSION_NAME,
12
+ PRIVACY_CENTER_SCREEN_ID,
13
+ STATUS_OPTION_KEY_PREFIX,
14
+ USER_ME_QUERY,
15
+ UserDataRightsHandlers,
16
+ } from "./constants";
11
17
  import { cancelDeletionWrite } from "./handlers/cancel-deletion.write";
12
18
  import { createConfirmDeletionByTokenHandler } from "./handlers/confirm-deletion-by-token.write";
13
19
  import { downloadAttemptListQuery } from "./handlers/download-attempt-list.query";
@@ -132,6 +138,17 @@ export type UserDataRightsOptions = {
132
138
  * wenn der jeweilige send*Email-Opt NICHT gesetzt ist; Export-ready braucht
133
139
  * zusaetzlich appExportDownloadUrl. */
134
140
  readonly mailDefaults?: GdprMailDefaults;
141
+ /** Hides the Art. 17 deletion section + its request-deletion/cancel-deletion
142
+ * actions on the privacy-center screen — for apps that already offer
143
+ * account deletion elsewhere (e.g. a profile danger-zone), against
144
+ * duplication. Default `true` (deletion section shown).
145
+ * BREAKING (fw#2312): replaces the removed client-side
146
+ * `userDataRightsClient({ privacyCenter: { showDeletion } })` option —
147
+ * the screen is now declared once, server-side (`type: "projectionDetail"`),
148
+ * so per-app visibility can only be a boot-time option, not a client-side
149
+ * component swap. Consumers that passed `showDeletion: false` client-side
150
+ * must move it here. */
151
+ readonly privacyCenterShowDeletion?: boolean;
135
152
  };
136
153
 
137
154
  export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): FeatureDefinition {
@@ -139,6 +156,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
139
156
  // process, not every run). Lives in the factory scope so the cron closure
140
157
  // shares it across runs.
141
158
  let warnedMissingExportUrl = false;
159
+ const showDeletion = opts.privacyCenterShowDeletion ?? true;
142
160
  return defineFeature("user-data-rights", (r) => {
143
161
  r.describe(
144
162
  'Implements GDPR Art. 15 (access / `my-audit-log` query), Art. 17 (erasure / `request-deletion` + `cancel-deletion`, plus the anonymous email-verified `request-deletion-by-email` + `confirm-deletion-by-token` flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / `restrict-account` + `lift-restriction`), and Art. 20 (portability / async `request-export` \u2192 ZIP via `file-foundation`, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })` \u2014 the feature then orchestrates the export and forget pipelines across all registered hooks automatically. When `mail-foundation` and a `mail-transport-*` are mounted, it also sends the four GDPR notifications (export ready/failed, deletion requested/executed) itself with no app callback code, rendered in each recipient’s locale. Requires `user`, `data-retention`, `compliance-profiles`, and `sessions`.',
@@ -283,24 +301,105 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
283
301
 
284
302
  r.translations({ keys: USER_DATA_RIGHTS_I18N });
285
303
 
286
- // Dormant self-service screen (Art. 15/17/18/20): export, activity log,
287
- // restriction, deletion in one screen. No r.nav — the app places it in
288
- // its logged-in area. The React component comes from userDataRightsClient()
289
- // (web/) client-side. access is openToAll because no app role name is
290
- // portable; the per-user handlers enforce auth server-side, and the
291
- // screen is invisible without r.nav until the app actively links it in
292
- // its authed area. dormant: true so createKumikoApp's boot diagnostic
293
- // (#2025) doesn't flag apps that haven't (yet) navved the screen as
294
- // having a missing client plugin (#2034).
295
- // kumiko-lint-ignore app-feature-structure Phase-3 conversion tracked in #2312
304
+ // Self-service screen (Art. 15/17/18/20): export, restriction, deletion
305
+ // in one projectionDetail screen bound to the user's own `me` row. No
306
+ // r.nav — the app places it in its logged-in area (createKumikoApp's
307
+ // missing-client-plugin boot diagnostic (#2025/#2034) only scans
308
+ // type: "custom" screens, so a non-navved projectionDetail never
309
+ // triggers it — no `dormant` flag needed, that field doesn't exist on
310
+ // this screen type). access is openToAll because no app role name is
311
+ // portable; the per-user handlers enforce auth server-side.
312
+ //
313
+ // Restriction/Deletion are declarative fields + actions; Export stays a
314
+ // custom extension section (web/client-plugin.tsx registers the
315
+ // component under EXPORT_SECTION_EXTENSION_NAME) since it needs
316
+ // async-job polling + a signed-URL download that a declarative action
317
+ // can't express. entityName: "export-job" names the domain entity the
318
+ // extension persists against (projectionDetail has no real entity of
319
+ // its own for the extension to default to).
296
320
  r.screen({
297
321
  id: PRIVACY_CENTER_SCREEN_ID,
298
- type: "custom",
299
- renderer: { react: { __component: "PrivacyCenterScreen" } },
322
+ type: "projectionDetail",
323
+ query: USER_ME_QUERY,
324
+ singleton: true,
300
325
  access: { openToAll: true },
301
- dormant: true,
302
326
  description:
303
327
  "Logged-in GDPR self-service page where a user requests and downloads a data export (Art. 20), restricts processing of their account (Art. 18) and requests its deletion (Art. 17).",
328
+ fieldLabels: {
329
+ status: "userDataRights.privacyCenter.field.status",
330
+ gracePeriodEnd: "userDataRights.privacyCenter.field.gracePeriodEnd",
331
+ },
332
+ layout: {
333
+ sections: [
334
+ {
335
+ kind: "extension",
336
+ title: "userDataRights.privacyCenter.export.title",
337
+ component: { react: { __component: EXPORT_SECTION_EXTENSION_NAME } },
338
+ entityName: "export-job",
339
+ },
340
+ {
341
+ // `status` lives here (not in Deletion) because this is the one
342
+ // section always on the screen — Deletion is dropped entirely
343
+ // when `showDeletion` is false, and status covers restriction
344
+ // AND deletion state, so it can't hang off the optional section.
345
+ title: "userDataRights.privacyCenter.restriction.title",
346
+ description: "userDataRights.privacyCenter.restriction.explainer",
347
+ fields: [
348
+ {
349
+ field: "status",
350
+ renderer: { format: "enumOption", keyPrefix: STATUS_OPTION_KEY_PREFIX },
351
+ },
352
+ ],
353
+ },
354
+ ...(showDeletion
355
+ ? [
356
+ {
357
+ title: "userDataRights.privacyCenter.deletion.title",
358
+ description: "userDataRights.privacyCenter.deletion.explainer",
359
+ // gracePeriodEnd is only meaningful once a deletion is
360
+ // actually pending — hide it instead of showing an empty
361
+ // date when status isn't deletionRequested.
362
+ fields: [
363
+ {
364
+ field: "gracePeriodEnd",
365
+ renderer: { format: "date" as const },
366
+ visible: { field: "status", eq: "deletionRequested" },
367
+ },
368
+ ],
369
+ },
370
+ ]
371
+ : []),
372
+ ],
373
+ },
374
+ actions: [
375
+ {
376
+ id: "restrict",
377
+ label: "userDataRights.privacyCenter.restriction.restrict",
378
+ handler: UserDataRightsHandlers.restrictAccount,
379
+ confirm: "userDataRights.privacyCenter.restriction.dialogDescription",
380
+ visible: { field: "status", ne: "restricted" },
381
+ style: "danger",
382
+ },
383
+ ...(showDeletion
384
+ ? [
385
+ {
386
+ id: "request-deletion",
387
+ label: "userDataRights.privacyCenter.deletion.delete",
388
+ handler: UserDataRightsHandlers.requestDeletion,
389
+ confirm: "userDataRights.privacyCenter.deletion.dialogDescription",
390
+ visible: { field: "status", ne: "deletionRequested" },
391
+ style: "danger" as const,
392
+ },
393
+ {
394
+ id: "cancel-deletion",
395
+ label: "userDataRights.privacyCenter.deletion.cancel",
396
+ handler: UserDataRightsHandlers.cancelDeletion,
397
+ visible: { field: "status", eq: "deletionRequested" },
398
+ style: "secondary" as const,
399
+ },
400
+ ]
401
+ : []),
402
+ ],
304
403
  });
305
404
 
306
405
  // Magic-link path (anonymous): the email link carries the token as a
@@ -1,8 +1,10 @@
1
- // Render-Test gegen echte i18n-Bundles (fängt fehlende Keys — der Screen
2
- // darf nie rohe "userDataRights.privacyCenter.*"-Keys zeigen) plus QN-Wiring
3
- // (die dispatchten Query-/Handler-Namen) und die status-getriebenen Branches.
4
- // Provider-Wrapper lokal (Dependency-Richtung renderer-web → bundled-features
5
- // verbietet test-utils-Import).
1
+ // Render test against real i18n bundles (catches missing keys — the Section
2
+ // must never show raw "userDataRights.privacyCenter.*" keys) plus QN wiring
3
+ // (the dispatched query/handler names). Restriction/deletion (status-driven
4
+ // branches) moved to declarative fields/actions with fw#2312 (feature.ts,
5
+ // booted in inspector-screens.boot.test.ts) — only Export (ExportSection) is
6
+ // still a React component. Provider wrapper is local (the renderer-web →
7
+ // bundled-features dependency direction forbids importing test-utils).
6
8
 
7
9
  import { describe, expect, spyOn, test } from "bun:test";
8
10
  import { createStore, type Dispatcher, type DispatcherStatus } from "@cosmicdrift/kumiko-headless";
@@ -28,7 +30,7 @@ import {
28
30
  } from "../../constants";
29
31
  import { EXPORT_JOB_STATUS as SCHEMA_EXPORT_JOB_STATUS } from "../../schema/export-job";
30
32
  import { defaultTranslations } from "../i18n";
31
- import { formatDate, PrivacyCenterScreen } from "../privacy-center-screen";
33
+ import { ExportSection, formatDate } from "../privacy-center-screen";
32
34
 
33
35
  const stubLiveEvents: LiveEventSubscriber = () => () => {};
34
36
  const stubTokens = {
@@ -40,7 +42,6 @@ const stubTokens = {
40
42
  const stubResolver = createStaticLocaleResolver();
41
43
 
42
44
  type QueryResponses = {
43
- readonly me: Record<string, unknown>;
44
45
  readonly exportStatus?: unknown;
45
46
  readonly auditLog?: unknown;
46
47
  /** Signed URL returned by downloadByJob — drives postWithDownload navigation. */
@@ -55,7 +56,6 @@ function makeDispatcher(
55
56
  const statusStore = createStore<DispatcherStatus>("online");
56
57
  const query = (async (type: string, payload: unknown) => {
57
58
  queries.push({ type, payload });
58
- if (type === USER_ME_QUERY) return { isSuccess: true, data: responses.me };
59
59
  if (type === UserDataRightsQueries.exportStatus) {
60
60
  return { isSuccess: true, data: responses.exportStatus ?? { hasJob: false } };
61
61
  }
@@ -85,10 +85,7 @@ function makeDispatcher(
85
85
  } as unknown as Dispatcher; // @cast-boundary test-stub
86
86
  }
87
87
 
88
- function renderCenter(
89
- responses: QueryResponses,
90
- opts: { readonly showDeletion?: boolean } = {},
91
- ): {
88
+ function renderExportSection(responses: QueryResponses): {
92
89
  view: ReturnType<typeof render>;
93
90
  writes: Array<{ type: string; payload: unknown }>;
94
91
  queries: Array<{ type: string; payload: unknown }>;
@@ -111,22 +108,21 @@ function renderCenter(
111
108
  </LocaleProvider>
112
109
  </TokensProvider>
113
110
  );
114
- const view = render(<PrivacyCenterScreen showDeletion={opts.showDeletion ?? true} />, {
115
- wrapper,
116
- });
111
+ // ExportSection renders no Section/title of its own in production (the
112
+ // renderer's ExtensionSectionMount supplies that) — a plain testId wrapper
113
+ // here is test-only scaffolding to detect "mounted".
114
+ const view = render(
115
+ <div data-testid="export-section-root">
116
+ <ExportSection />
117
+ </div>,
118
+ { wrapper },
119
+ );
117
120
  return { view, writes, queries };
118
121
  }
119
122
 
120
- const activeMe = {
121
- id: "00000000-0000-4000-8000-000000000042",
122
- email: "marc@example.com",
123
- status: "active",
124
- gracePeriodEnd: null,
125
- };
126
-
127
123
  async function waitForMount(view: ReturnType<typeof render>): Promise<void> {
128
124
  await waitFor(() => {
129
- if (view.queryByTestId("privacy-center-screen") === null) {
125
+ if (view.queryByTestId("export-section-root") === null) {
130
126
  throw new Error("not mounted yet");
131
127
  }
132
128
  });
@@ -151,22 +147,16 @@ async function waitForDownloadReady(view: ReturnType<typeof render>): Promise<vo
151
147
  // plus accumulated global DOM/event state across ~30 prior DOM test files corrupts
152
148
  // these in-flight renders (#457-class). A fresh process has no such accumulation.
153
149
  // The QN-Drift-Pins + formatDate describes below are pure-logic and CI-stable.
154
- describe("PrivacyCenterScreen", () => {
155
- test("aktiver User: Export/Einschränken/Löschen-Sektionen, Texte übersetzt (keine rohen Keys)", async () => {
156
- const { view } = renderCenter({ me: activeMe });
150
+ describe("ExportSection", () => {
151
+ test("idle: Intro + Request-Button, Texte übersetzt (keine rohen Keys)", async () => {
152
+ const { view } = renderExportSection({});
157
153
  await waitForMount(view);
158
- expect(view.getByTestId("privacy-export")).toBeTruthy();
159
- expect(view.getByTestId("privacy-restriction")).toBeTruthy();
160
- expect(view.getByTestId("privacy-deletion")).toBeTruthy();
161
154
  expect(view.getByTestId("privacy-export-request")).toBeTruthy();
162
- expect(view.getByTestId("privacy-restriction-restrict")).toBeTruthy();
163
- expect(view.getByTestId("privacy-deletion-delete")).toBeTruthy();
164
155
  expect(view.container.textContent).not.toContain("userDataRights.privacyCenter");
165
156
  });
166
157
 
167
158
  test("export done: Download-Button + Verfügbar-bis-Datum", async () => {
168
- const { view } = renderCenter({
169
- me: activeMe,
159
+ const { view } = renderExportSection({
170
160
  exportStatus: {
171
161
  hasJob: true,
172
162
  job: { id: "job-123", status: EXPORT_JOB_STATUS.Done, expiresAt: "2026-07-11T00:00:00Z" },
@@ -180,8 +170,7 @@ describe("PrivacyCenterScreen", () => {
180
170
  });
181
171
 
182
172
  test("export failed: Fehler-Banner + Re-Request möglich", async () => {
183
- const { view } = renderCenter({
184
- me: activeMe,
173
+ const { view } = renderExportSection({
185
174
  exportStatus: { hasJob: true, job: { id: "job-9", status: EXPORT_JOB_STATUS.Failed } },
186
175
  });
187
176
  await waitForTestId(view, "privacy-export-failed");
@@ -190,8 +179,7 @@ describe("PrivacyCenterScreen", () => {
190
179
  });
191
180
 
192
181
  test("export pending: in-progress Banner, kein Request-Button", async () => {
193
- const { view } = renderCenter({
194
- me: activeMe,
182
+ const { view } = renderExportSection({
195
183
  exportStatus: { hasJob: true, job: { id: "job-1", status: EXPORT_JOB_STATUS.Pending } },
196
184
  });
197
185
  await waitForTestId(view, "privacy-export-pending");
@@ -199,28 +187,8 @@ describe("PrivacyCenterScreen", () => {
199
187
  expect(view.queryByTestId("privacy-export-request")).toBeNull();
200
188
  });
201
189
 
202
- test("deletionRequested: Frist-Banner + Abbrechen statt Lösch-Button", async () => {
203
- const { view } = renderCenter({
204
- me: { ...activeMe, status: "deletionRequested", gracePeriodEnd: "2026-07-11T00:00:00Z" },
205
- });
206
- await waitForMount(view);
207
- const banner = view.getByTestId("privacy-deletion-requested");
208
- expect(banner.textContent).toContain("2026-07-11");
209
- expect(banner.textContent).not.toContain("{date}");
210
- expect(banner.textContent).not.toContain("T00:00");
211
- expect(view.queryByTestId("privacy-deletion-delete")).toBeNull();
212
- expect(view.getByTestId("privacy-deletion-cancel")).toBeTruthy();
213
- });
214
-
215
- test("restricted: Info-Banner statt Einschränken-Button", async () => {
216
- const { view } = renderCenter({ me: { ...activeMe, status: "restricted" } });
217
- await waitForMount(view);
218
- expect(view.getByTestId("privacy-restriction-active")).toBeTruthy();
219
- expect(view.queryByTestId("privacy-restriction-restrict")).toBeNull();
220
- });
221
-
222
190
  test("Export-Request dispatcht den korrekten Handler-QN", async () => {
223
- const { view, writes } = renderCenter({ me: activeMe });
191
+ const { view, writes } = renderExportSection({});
224
192
  await waitForMount(view);
225
193
  fireEvent.click(view.getByTestId("privacy-export-request"));
226
194
  await waitFor(() => {
@@ -229,16 +197,8 @@ describe("PrivacyCenterScreen", () => {
229
197
  expect(writes[0]?.type).toBe(UserDataRightsHandlers.requestExport);
230
198
  });
231
199
 
232
- test("showDeletion=false: keine Lösch-Sektion", async () => {
233
- const { view } = renderCenter({ me: activeMe }, { showDeletion: false });
234
- await waitForMount(view);
235
- expect(view.queryByTestId("privacy-deletion")).toBeNull();
236
- expect(view.queryByTestId("privacy-deletion-delete")).toBeNull();
237
- });
238
-
239
200
  test("Download-Button dispatcht downloadByJob mit der korrekten jobId", async () => {
240
- const { view, queries } = renderCenter({
241
- me: activeMe,
201
+ const { view, queries } = renderExportSection({
242
202
  exportStatus: {
243
203
  hasJob: true,
244
204
  job: { id: "job-123", status: EXPORT_JOB_STATUS.Done, expiresAt: "2026-07-11T00:00:00Z" },
@@ -259,8 +219,7 @@ describe("PrivacyCenterScreen", () => {
259
219
  const signedUrl = "https://cdn.test/exports/job-123.zip?sig=abc";
260
220
  const assign = spyOn(window.location, "assign").mockImplementation(() => {});
261
221
  try {
262
- const { view } = renderCenter({
263
- me: activeMe,
222
+ const { view } = renderExportSection({
264
223
  downloadUrl: signedUrl,
265
224
  exportStatus: {
266
225
  hasJob: true,
@@ -281,8 +240,7 @@ describe("PrivacyCenterScreen", () => {
281
240
  test("Download-Button does not navigate when downloadByJob returns no url", async () => {
282
241
  const assign = spyOn(window.location, "assign").mockImplementation(() => {});
283
242
  try {
284
- const { view } = renderCenter({
285
- me: activeMe,
243
+ const { view } = renderExportSection({
286
244
  downloadUrl: undefined,
287
245
  exportStatus: {
288
246
  hasJob: true,
@@ -1,16 +1,25 @@
1
1
  // @runtime client
2
- // Client-Feature-Factory für user-data-rights. Liefert den
3
- // PrivacyCenterScreen (gemappt auf die Screen-id "privacy-center") +
4
- // Default-Translations. Apps hängen es in
5
- // createKumikoApp({ clientFeatures: [userDataRightsClient()] }) ein; der
6
- // Screen wird server-seitig vom Feature dormant als custom-Screen
7
- // registriert (r.screen), die App platziert ihn via r.nav.
2
+ // Client-feature factory for user-data-rights. Provides the ExportSection
3
+ // extension component for the privacy-center screen (registered server-side
4
+ // declaratively as `type: "projectionDetail"`, see feature.ts — restriction
5
+ // and deletion are fields/actions on the screen itself and need no client
6
+ // component) + default translations. Apps wire it in via
7
+ // createKumikoApp({ clientFeatures: [userDataRightsClient()] }); the screen
8
+ // has no own r.nav, the app places it via r.nav.
9
+ //
10
+ // fw#2312: the former `privacyCenter: { showDeletion }` option is gone — a
11
+ // declarative screen is now registered server-side once for all apps, so a
12
+ // client prop can no longer toggle its sections/actions per app. Its
13
+ // replacement is the server-side
14
+ // `UserDataRightsOptions.privacyCenterShowDeletion` (createUserDataRightsFeature,
15
+ // ../feature.ts) — BREAKING, consumers with `showDeletion: false` (e.g.
16
+ // money-horse) must carry the option over at feature setup.
8
17
 
9
18
  import { mergeTranslations, type TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
10
19
  import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
11
- import { PRIVACY_CENTER_SCREEN_ID, USER_DATA_RIGHTS_FEATURE } from "../constants";
20
+ import { EXPORT_SECTION_EXTENSION_NAME, USER_DATA_RIGHTS_FEATURE } from "../constants";
12
21
  import { defaultTranslations } from "./i18n";
13
- import { PrivacyCenterScreen } from "./privacy-center-screen";
22
+ import { ExportSection } from "./privacy-center-screen";
14
23
  import { makePublicDeletionGate, type PublicDeletionRoutes } from "./public-deletion-gate";
15
24
 
16
25
  export type UserDataRightsClientOptions = {
@@ -21,27 +30,16 @@ export type UserDataRightsClientOptions = {
21
30
  * privacy-center-Screen. Den Client VOR dem Auth-Client listen, sonst
22
31
  * landet der anonyme Besucher auf der Login-Maske. */
23
32
  readonly publicDeletion?: PublicDeletionRoutes;
24
- /** Konfiguration des eingeloggten privacy-center-Screens. */
25
- readonly privacyCenter?: {
26
- /** `false` blendet die Konto-Lösch-Sektion aus — für Apps, die die Löschung
27
- * schon woanders anbieten (z.B. Profil-DangerZone), gegen Doppelung.
28
- * Default `true`. */
29
- readonly showDeletion?: boolean;
30
- };
31
33
  };
32
34
 
33
35
  export function userDataRightsClient(
34
36
  options?: UserDataRightsClientOptions,
35
37
  ): ClientFeatureDefinition {
36
- const showDeletion = options?.privacyCenter?.showDeletion ?? true;
37
- const privacyCenter = showDeletion
38
- ? PrivacyCenterScreen
39
- : () => <PrivacyCenterScreen showDeletion={false} />;
40
38
  const base: ClientFeatureDefinition = {
41
39
  name: USER_DATA_RIGHTS_FEATURE,
42
40
  translations: mergeTranslations(defaultTranslations, options?.translations ?? {}),
43
- components: {
44
- [PRIVACY_CENTER_SCREEN_ID]: privacyCenter,
41
+ extensionSectionComponents: {
42
+ [EXPORT_SECTION_EXTENSION_NAME]: ExportSection,
45
43
  },
46
44
  };
47
45
  if (options?.publicDeletion === undefined) return base;
@@ -37,12 +37,13 @@ const apexTranslations: TranslationsByLocale = {
37
37
  "No token found in the link. Please open the link from the email again.",
38
38
  "userDataRights.deletion.confirm.error": "Something went wrong. Please try again.",
39
39
 
40
- "userDataRights.privacyCenter.title": "Privacy",
41
- "userDataRights.privacyCenter.intro":
42
- "Manage your GDPR rights: access, export, restrict, and delete your account.",
43
- "userDataRights.privacyCenter.loading": "Loading …",
44
- "userDataRights.privacyCenter.loadError": "Your data could not be loaded.",
45
40
  "userDataRights.privacyCenter.errors.generic": "Something went wrong. Please try again.",
41
+ "userDataRights.privacyCenter.field.status": "Account status",
42
+ "userDataRights.privacyCenter.field.gracePeriodEnd": "Scheduled deletion date",
43
+ "userDataRights.privacyCenter.field.status.option.active": "Active",
44
+ "userDataRights.privacyCenter.field.status.option.restricted": "Restricted",
45
+ "userDataRights.privacyCenter.field.status.option.deletionRequested": "Deletion requested",
46
+ "userDataRights.privacyCenter.field.status.option.deleted": "Deleted",
46
47
 
47
48
  "userDataRights.privacyCenter.export.title": "Export your data (Art. 20)",
48
49
  "userDataRights.privacyCenter.export.intro":
@@ -62,20 +63,14 @@ const apexTranslations: TranslationsByLocale = {
62
63
  "userDataRights.privacyCenter.restriction.explainer":
63
64
  "Freeze your account: processing of your data is paused and you are signed out. Lifting the restriction afterwards is only possible via support.",
64
65
  "userDataRights.privacyCenter.restriction.restrict": "Restrict account",
65
- "userDataRights.privacyCenter.restriction.dialogTitle": "Restrict your account?",
66
66
  "userDataRights.privacyCenter.restriction.dialogDescription":
67
67
  "You will be signed out immediately and cannot sign in again until support lifts the restriction.",
68
- "userDataRights.privacyCenter.restriction.restricted":
69
- "Your account is restricted. Contact support to lift the restriction.",
70
68
 
71
69
  "userDataRights.privacyCenter.deletion.title": "Delete account (Art. 17)",
72
70
  "userDataRights.privacyCenter.deletion.explainer":
73
71
  "Request deletion of your account. Until the grace period ends you can cancel the deletion.",
74
72
  "userDataRights.privacyCenter.deletion.delete": "Delete account",
75
- "userDataRights.privacyCenter.deletion.requested": "Your account will be deleted on {date}.",
76
73
  "userDataRights.privacyCenter.deletion.cancel": "Cancel deletion",
77
- "userDataRights.privacyCenter.deletion.cancelSuccess": "The deletion was cancelled.",
78
- "userDataRights.privacyCenter.deletion.dialogTitle": "Delete your account?",
79
74
  "userDataRights.privacyCenter.deletion.dialogDescription":
80
75
  "Confirming starts the deletion grace period. You can cancel the deletion until it ends.",
81
76
 
@@ -9,7 +9,7 @@ export { type UserDataRightsClientOptions, userDataRightsClient } from "./client
9
9
  export type { ConfirmAccountDeletionScreenProps } from "./confirm-deletion-screen";
10
10
  export { ConfirmAccountDeletionScreen } from "./confirm-deletion-screen";
11
11
  export { defaultTranslations } from "./i18n";
12
- export { formatDate, PrivacyCenterScreen } from "./privacy-center-screen";
12
+ export { formatDate } from "./privacy-center-screen";
13
13
  export { makePublicDeletionGate, type PublicDeletionRoutes } from "./public-deletion-gate";
14
14
  export type { RequestAccountDeletionScreenProps } from "./request-deletion-screen";
15
15
  export { RequestAccountDeletionScreen } from "./request-deletion-screen";