@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
@@ -1,17 +1,14 @@
1
1
  // @runtime client
2
- // PrivacyCenterScreen — eingeloggte DSGVO-Self-Service-Seite: Datenexport
3
- // (Art. 20), Verarbeitung einschränken (Art. 18) und Konto löschen (Art. 17)
4
- // in einem Screen. Das Feature registriert ihn dormant als custom-Screen
5
- // (r.screen, kein r.nav); die App platziert ihn via r.nav im eingeloggten
6
- // Bereich.
2
+ // Export section (Art. 20) — the one part of user-data-rights' privacy-center
3
+ // screen that stays custom (fw#2312). Registered as an EditExtensionSection
4
+ // component (see web/client-plugin.tsx); the projectionDetail screen
5
+ // (feature.ts) supplies the surrounding Section chrome (title, testId), so
6
+ // this component renders only its own body content.
7
7
  //
8
- // `showDeletion=false` blendet die Lösch-Sektion aus — für Apps, die die
9
- // Konto-Löschung bereits an anderer Stelle anbieten (z.B. Profil-DangerZone),
10
- // damit sie nicht doppelt erscheint.
11
- //
12
- // Art. 18 Lift ist hier bewusst NICHT actionbar: ein eingeschränktes Konto ist
13
- // vom Login geblockt und erreicht diesen Screen gar nicht erst — das Aufheben
14
- // läuft über Support / Magic-Link, nicht über die Self-Service-UI.
8
+ // Export needs async-job polling (worker-Lane-Cron, ~1 Min) plus a
9
+ // signed-URL download — logic a declarative field/action can't express.
10
+ // Restriction (Art. 18) and Deletion (Art. 17) moved onto the screen's
11
+ // declarative fields/actions instead (feature.ts).
15
12
 
16
13
  import {
17
14
  useDispatcher,
@@ -19,30 +16,20 @@ import {
19
16
  useQuery,
20
17
  useTranslation,
21
18
  } from "@cosmicdrift/kumiko-renderer";
22
- import { FormScreenShell, postWithDownload } from "@cosmicdrift/kumiko-renderer-web";
19
+ import { postWithDownload } from "@cosmicdrift/kumiko-renderer-web";
23
20
  import { type ReactNode, useEffect, useState } from "react";
24
21
  import {
25
22
  EXPORT_JOB_STATUS,
26
23
  type ExportJobStatus,
27
- USER_ME_QUERY,
28
24
  UserDataRightsHandlers,
29
25
  UserDataRightsQueries,
30
26
  } from "../constants";
31
27
 
32
- const STATUS_DELETION_REQUESTED = "deletionRequested";
33
- const STATUS_RESTRICTED = "restricted";
34
28
  // Export-Job läuft async (worker-Lane-Cron, ~1 Min). Solange er pending/running
35
29
  // ist, pollt der Screen den Status, damit der Download ohne manuellen Reload
36
30
  // erscheint.
37
31
  const EXPORT_POLL_MS = 4000;
38
32
 
39
- type MeRow = {
40
- readonly id: string;
41
- readonly email: string;
42
- readonly status?: string;
43
- readonly gracePeriodEnd?: string | null;
44
- };
45
-
46
33
  type ExportJob = {
47
34
  readonly id: string;
48
35
  readonly status: ExportJobStatus;
@@ -86,9 +73,14 @@ function StatusBanner({ status }: { readonly status: SectionStatus }): ReactNode
86
73
  return null;
87
74
  }
88
75
 
89
- function ExportSection(): ReactNode {
76
+ // Mounted by the renderer's ExtensionSectionMount (already wrapped in
77
+ // <Section title={...} testId="section-extension-...">) — takes no props of
78
+ // its own, ExtensionSectionProps (entityName/entityId/...) are unused since
79
+ // export status/actions are scoped to the current user session, not to a
80
+ // host entity row.
81
+ export function ExportSection(): ReactNode {
90
82
  const t = useTranslation();
91
- const { Section, Button, Banner } = usePrimitives();
83
+ const { Button, Banner } = usePrimitives();
92
84
  const dispatcher = useDispatcher();
93
85
  const statusQuery = useQuery<ExportStatusResult | null>(UserDataRightsQueries.exportStatus, {});
94
86
  const [status, setStatus] = useState<SectionStatus>({ kind: "idle" });
@@ -131,26 +123,7 @@ function ExportSection(): ReactNode {
131
123
  }, [inProgress, refetch]);
132
124
 
133
125
  return (
134
- <Section
135
- title={t("userDataRights.privacyCenter.export.title")}
136
- testId="privacy-export"
137
- actions={
138
- !inProgress ? (
139
- <Button
140
- onClick={() => void request()}
141
- disabled={submitting}
142
- loading={submitting}
143
- testId="privacy-export-request"
144
- >
145
- {done
146
- ? t("userDataRights.privacyCenter.export.requestNew")
147
- : submitting
148
- ? t("userDataRights.privacyCenter.export.requesting")
149
- : t("userDataRights.privacyCenter.export.request")}
150
- </Button>
151
- ) : undefined
152
- }
153
- >
126
+ <>
154
127
  <p className="text-sm text-muted-foreground">
155
128
  {t("userDataRights.privacyCenter.export.intro")}
156
129
  </p>
@@ -190,209 +163,23 @@ function ExportSection(): ReactNode {
190
163
  </div>
191
164
  </Banner>
192
165
  )}
193
- <StatusBanner status={status} />
194
- </Section>
195
- );
196
- }
197
-
198
- function RestrictionSection({
199
- me,
200
- onChanged,
201
- }: {
202
- readonly me: MeRow;
203
- readonly onChanged: () => void;
204
- }): ReactNode {
205
- const t = useTranslation();
206
- const { Section, Button, Banner, Dialog } = usePrimitives();
207
- const dispatcher = useDispatcher();
208
- const [dialogOpen, setDialogOpen] = useState(false);
209
- const [status, setStatus] = useState<SectionStatus>({ kind: "idle" });
210
- const restricted = me.status === STATUS_RESTRICTED;
211
-
212
- // Erfolg ⇒ alle Sessions revoked, der User wird abgemeldet. onChanged ist
213
- // best-effort (das anschließende Refetch läuft typisch in 401 + Logout-
214
- // Redirect der App-Auth-Schicht).
215
- const restrict = async (): Promise<void> => {
216
- const res = await dispatcher.write(UserDataRightsHandlers.restrictAccount, {});
217
- if (!res.isSuccess) {
218
- setStatus({ kind: "error", messageKey: failureKey(res.error) });
219
- return;
220
- }
221
- onChanged();
222
- };
223
-
224
- return (
225
- <Section
226
- title={t("userDataRights.privacyCenter.restriction.title")}
227
- testId="privacy-restriction"
228
- variant="destructive"
229
- actions={
230
- restricted ? undefined : (
166
+ {!inProgress && (
167
+ <div className="mt-2">
231
168
  <Button
232
- variant="danger"
233
- onClick={() => setDialogOpen(true)}
234
- testId="privacy-restriction-restrict"
235
- >
236
- {t("userDataRights.privacyCenter.restriction.restrict")}
237
- </Button>
238
- )
239
- }
240
- >
241
- {restricted ? (
242
- <Banner variant="error" testId="privacy-restriction-active">
243
- {t("userDataRights.privacyCenter.restriction.restricted")}
244
- </Banner>
245
- ) : (
246
- <>
247
- <p className="text-sm text-muted-foreground">
248
- {t("userDataRights.privacyCenter.restriction.explainer")}
249
- </p>
250
- <StatusBanner status={status} />
251
- <Dialog
252
- open={dialogOpen}
253
- onOpenChange={setDialogOpen}
254
- title={t("userDataRights.privacyCenter.restriction.dialogTitle")}
255
- description={t("userDataRights.privacyCenter.restriction.dialogDescription")}
256
- variant="danger"
257
- confirmLabel={t("userDataRights.privacyCenter.restriction.restrict")}
258
- onConfirm={restrict}
259
- testId="privacy-restriction-dialog"
260
- />
261
- </>
262
- )}
263
- </Section>
264
- );
265
- }
266
-
267
- function DeletionSection({
268
- me,
269
- onChanged,
270
- }: {
271
- readonly me: MeRow;
272
- readonly onChanged: () => void;
273
- }): ReactNode {
274
- const t = useTranslation();
275
- const { Section, Button, Banner, Dialog } = usePrimitives();
276
- const dispatcher = useDispatcher();
277
- const [dialogOpen, setDialogOpen] = useState(false);
278
- const [status, setStatus] = useState<SectionStatus>({ kind: "idle" });
279
- const deletionRequested = me.status === STATUS_DELETION_REQUESTED;
280
-
281
- const requestDeletion = async (): Promise<void> => {
282
- const res = await dispatcher.write(UserDataRightsHandlers.requestDeletion, {});
283
- if (!res.isSuccess) {
284
- setStatus({ kind: "error", messageKey: failureKey(res.error) });
285
- return;
286
- }
287
- setStatus({ kind: "idle" });
288
- onChanged();
289
- };
290
-
291
- const cancelDeletion = async (): Promise<void> => {
292
- const res = await dispatcher.write(UserDataRightsHandlers.cancelDeletion, {});
293
- if (!res.isSuccess) {
294
- setStatus({ kind: "error", messageKey: failureKey(res.error) });
295
- return;
296
- }
297
- setStatus({
298
- kind: "success",
299
- messageKey: "userDataRights.privacyCenter.deletion.cancelSuccess",
300
- });
301
- onChanged();
302
- };
303
-
304
- return (
305
- <Section
306
- title={t("userDataRights.privacyCenter.deletion.title")}
307
- testId="privacy-deletion"
308
- variant="destructive"
309
- actions={
310
- deletionRequested ? (
311
- <Button
312
- variant="secondary"
313
- onClick={() => void cancelDeletion()}
314
- testId="privacy-deletion-cancel"
315
- >
316
- {t("userDataRights.privacyCenter.deletion.cancel")}
317
- </Button>
318
- ) : (
319
- <Button
320
- variant="danger"
321
- onClick={() => setDialogOpen(true)}
322
- testId="privacy-deletion-delete"
169
+ onClick={() => void request()}
170
+ disabled={submitting}
171
+ loading={submitting}
172
+ testId="privacy-export-request"
323
173
  >
324
- {t("userDataRights.privacyCenter.deletion.delete")}
174
+ {done
175
+ ? t("userDataRights.privacyCenter.export.requestNew")
176
+ : submitting
177
+ ? t("userDataRights.privacyCenter.export.requesting")
178
+ : t("userDataRights.privacyCenter.export.request")}
325
179
  </Button>
326
- )
327
- }
328
- >
329
- {deletionRequested ? (
330
- <>
331
- <Banner variant="error" testId="privacy-deletion-requested">
332
- {t("userDataRights.privacyCenter.deletion.requested", {
333
- date: formatDate(me.gracePeriodEnd),
334
- })}
335
- </Banner>
336
- <StatusBanner status={status} />
337
- </>
338
- ) : (
339
- <>
340
- <p className="text-sm text-muted-foreground">
341
- {t("userDataRights.privacyCenter.deletion.explainer")}
342
- </p>
343
- <StatusBanner status={status} />
344
- <Dialog
345
- open={dialogOpen}
346
- onOpenChange={setDialogOpen}
347
- title={t("userDataRights.privacyCenter.deletion.dialogTitle")}
348
- description={t("userDataRights.privacyCenter.deletion.dialogDescription")}
349
- variant="danger"
350
- confirmLabel={t("userDataRights.privacyCenter.deletion.delete")}
351
- onConfirm={requestDeletion}
352
- testId="privacy-deletion-dialog"
353
- />
354
- </>
180
+ </div>
355
181
  )}
356
- </Section>
357
- );
358
- }
359
-
360
- export function PrivacyCenterScreen({
361
- showDeletion = true,
362
- }: {
363
- readonly showDeletion?: boolean;
364
- } = {}): ReactNode {
365
- const t = useTranslation();
366
- const { Banner, Heading } = usePrimitives();
367
- const meQuery = useQuery<MeRow | null>(USER_ME_QUERY, {});
368
-
369
- if (meQuery.error) {
370
- return (
371
- <Banner padded variant="error" testId="privacy-error">
372
- {t("userDataRights.privacyCenter.loadError")}
373
- </Banner>
374
- );
375
- }
376
- const me = meQuery.data;
377
- if (me === null || me === undefined) {
378
- return (
379
- <Banner padded variant="loading" testId="privacy-loading">
380
- {t("userDataRights.privacyCenter.loading")}
381
- </Banner>
382
- );
383
- }
384
-
385
- const refetch = (): void => {
386
- void meQuery.refetch?.();
387
- };
388
-
389
- return (
390
- <FormScreenShell className="flex flex-col gap-6" testId="privacy-center-screen">
391
- <Heading variant="page">{t("userDataRights.privacyCenter.title")}</Heading>
392
- <p className="text-sm text-muted-foreground">{t("userDataRights.privacyCenter.intro")}</p>
393
- <ExportSection />
394
- <RestrictionSection me={me} onChanged={refetch} />
395
- {showDeletion && <DeletionSection me={me} onChanged={refetch} />}
396
- </FormScreenShell>
182
+ <StatusBanner status={status} />
183
+ </>
397
184
  );
398
185
  }
@@ -0,0 +1,97 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createFilesFeature } from "@cosmicdrift/kumiko-framework/files";
4
+ import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
5
+ import { authFoundationFeature } from "../../auth-foundation";
6
+ import { createComplianceProfilesFeature } from "../../compliance-profiles/feature";
7
+ import { createConfigFeature } from "../../config/feature";
8
+ import { createDataRetentionFeature } from "../../data-retention/feature";
9
+ import { createSessionsFeature } from "../../sessions/feature";
10
+ import { createTenantFeature } from "../../tenant/feature";
11
+ import { createUserFeature } from "../../user/feature";
12
+ import { createUserDataRightsFeature } from "../../user-data-rights/feature";
13
+ import { createUserDataRightsDefaultsFeature } from "../../user-data-rights-defaults/feature";
14
+ import { createUserProfileFeature } from "../feature";
15
+
16
+ // fw#2312 — the `profile` screen converted from a custom React component to
17
+ // a declarative projectionDetail (change-password/change-email stay
18
+ // EditExtensionSection components, deletion is fields+actions). `validateBoot`
19
+ // below proves the screen clears every boot-validator rule for
20
+ // projectionDetail extension sections (entityName set, contributesToFormSubmit
21
+ // not true, action handlers resolve) — same pattern as user-data-rights'
22
+ // privacy-center screen (inspector-screens.boot.test.ts), which this mirrors.
23
+ describe("user-profile screen (fw#2312 projectionDetail conversion)", () => {
24
+ const features = [
25
+ createConfigFeature(),
26
+ createUserFeature(),
27
+ createTenantFeature(),
28
+ createAuthEmailPasswordFeature(),
29
+ createDataRetentionFeature(),
30
+ createComplianceProfilesFeature(),
31
+ authFoundationFeature,
32
+ createSessionsFeature(),
33
+ createFilesFeature(),
34
+ createUserDataRightsFeature(),
35
+ createUserDataRightsDefaultsFeature(),
36
+ createUserProfileFeature(),
37
+ ];
38
+
39
+ test("the assembled feature set boot-validates", () => {
40
+ expect(() => validateBoot(features)).not.toThrow();
41
+ });
42
+
43
+ test("is a projectionDetail bound to the user's own `me` query", () => {
44
+ const f = createUserProfileFeature();
45
+ const screen = f.screens["profile"];
46
+ expect(screen?.type).toBe("projectionDetail");
47
+ if (screen?.type === "projectionDetail") {
48
+ expect(screen.query).toBe("user:query:user:me");
49
+ expect(screen.access).toEqual({ openToAll: true });
50
+ }
51
+ });
52
+
53
+ test("change-email/change-password are self-persisting extensions scoped to `user`", () => {
54
+ const f = createUserProfileFeature();
55
+ const screen = f.screens["profile"];
56
+ if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
57
+ const extensionSections = screen.layout.sections.filter((s) => s.kind === "extension");
58
+ expect(extensionSections).toHaveLength(2);
59
+ for (const section of extensionSections) {
60
+ expect(section.entityName).toBe("user");
61
+ expect(section.contributesToFormSubmit).not.toBe(true);
62
+ }
63
+ });
64
+
65
+ test("gracePeriodEnd is hidden unless a deletion is actually pending", () => {
66
+ const f = createUserProfileFeature();
67
+ const screen = f.screens["profile"];
68
+ if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
69
+ const graceField = screen.layout.sections
70
+ .flatMap((s) => ("fields" in s ? s.fields : []))
71
+ .find((field) => typeof field === "object" && field.field === "gracePeriodEnd");
72
+ expect(typeof graceField === "object" ? graceField.visible : undefined).toEqual({
73
+ field: "status",
74
+ eq: "deletionRequested",
75
+ });
76
+ });
77
+
78
+ test("request-deletion/cancel-deletion actions dispatch user-data-rights handlers with the original confirm+visibility rules", () => {
79
+ const f = createUserProfileFeature();
80
+ const screen = f.screens["profile"];
81
+ if (screen?.type !== "projectionDetail") throw new Error("expected projectionDetail");
82
+ const byId = Object.fromEntries((screen.actions ?? []).map((a) => [a.id, a]));
83
+
84
+ expect(byId["request-deletion"]).toMatchObject({
85
+ handler: "user-data-rights:write:request-deletion",
86
+ confirm: "profile.danger.dialogDescription",
87
+ visible: { field: "status", ne: "deletionRequested" },
88
+ style: "danger",
89
+ });
90
+ // Cancel has no confirm — the pre-#2312 custom screen executed it directly.
91
+ expect(byId["cancel-deletion"]).toMatchObject({
92
+ handler: "user-data-rights:write:cancel-deletion",
93
+ visible: { field: "status", eq: "deletionRequested" },
94
+ });
95
+ expect((byId["cancel-deletion"] as { readonly confirm?: string }).confirm).toBeUndefined();
96
+ });
97
+ });