@cosmicdrift/kumiko-bundled-features 0.126.0 → 0.128.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 (61) hide show
  1. package/package.json +6 -6
  2. package/src/audit/__tests__/audit-security.integration.test.ts +50 -0
  3. package/src/audit/i18n.ts +9 -0
  4. package/src/audit/web/audit-log-screen.tsx +112 -2
  5. package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +195 -2
  6. package/src/billing-foundation/entities.ts +28 -2
  7. package/src/billing-foundation/events.ts +6 -2
  8. package/src/billing-foundation/feature.ts +2 -0
  9. package/src/billing-foundation/get-subscription-for-tenant.ts +16 -5
  10. package/src/billing-foundation/handlers/create-portal-session.write.ts +17 -1
  11. package/src/billing-foundation/handlers/list-subscriptions.query.ts +15 -1
  12. package/src/billing-foundation/handlers/process-event.write.ts +39 -3
  13. package/src/billing-foundation/tenant-destroy-hook.ts +30 -7
  14. package/src/cap-counter/entity.ts +1 -1
  15. package/src/cap-counter/feature.ts +10 -2
  16. package/src/cap-counter/i18n.ts +12 -0
  17. package/src/compliance-profiles/feature.ts +3 -0
  18. package/src/compliance-profiles/i18n.ts +6 -0
  19. package/src/config/feature.ts +3 -0
  20. package/src/config/i18n.ts +9 -0
  21. package/src/delivery/feature.ts +3 -0
  22. package/src/delivery/i18n.ts +6 -0
  23. package/src/feature-toggles/feature.ts +3 -0
  24. package/src/feature-toggles/i18n.ts +6 -0
  25. package/src/jobs/i18n.ts +4 -0
  26. package/src/jobs/web/job-runs-screen.tsx +9 -9
  27. package/src/managed-pages/i18n.ts +5 -0
  28. package/src/managed-pages/screens/page-screens.ts +5 -1
  29. package/src/personal-access-tokens/feature.ts +2 -0
  30. package/src/personal-access-tokens/i18n.ts +5 -0
  31. package/src/personal-access-tokens/web/pat-tokens-screen.tsx +7 -3
  32. package/src/subscription-stripe/feature.ts +2 -0
  33. package/src/tags/feature.ts +2 -0
  34. package/src/tags/i18n.ts +5 -0
  35. package/src/tenant/__tests__/tenant-security.integration.test.ts +20 -5
  36. package/src/tenant/feature.ts +3 -0
  37. package/src/tenant/handlers/members.query.ts +19 -4
  38. package/src/tenant/i18n.ts +28 -0
  39. package/src/tenant/index.ts +1 -1
  40. package/src/tenant/schema/tenant.ts +2 -2
  41. package/src/tenant/screens.ts +3 -2
  42. package/src/tenant/web/members-screen.tsx +8 -3
  43. package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +161 -1
  44. package/src/tenant-lifecycle/feature.ts +1 -0
  45. package/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -0
  46. package/src/tenant-lifecycle/handlers/request-destruction.write.ts +2 -0
  47. package/src/tenant-lifecycle/lifecycle-gate.ts +58 -0
  48. package/src/tenant-lifecycle/run-tenant-destroy.ts +50 -39
  49. package/src/tenant-lifecycle/stages.ts +36 -6
  50. package/src/tier-engine/feature.ts +2 -0
  51. package/src/tier-engine/i18n.ts +8 -0
  52. package/src/tier-engine/web/tier-admin-screen.tsx +20 -14
  53. package/src/user/feature.ts +3 -0
  54. package/src/user/i18n.ts +20 -0
  55. package/src/user/schema/user.ts +1 -0
  56. package/src/user/screens.ts +3 -15
  57. package/src/user-data-rights/feature.ts +17 -0
  58. package/src/user-data-rights/i18n.ts +32 -33
  59. package/src/user-data-rights/schema/export-job.ts +2 -0
  60. package/src/user-data-rights/screens.ts +2 -1
  61. package/src/user-data-rights/web/i18n.ts +9 -0
@@ -37,7 +37,7 @@ type Status =
37
37
 
38
38
  export function TierAdminScreen(): ReactNode {
39
39
  const t = useTranslation();
40
- const { Section, Field, Input, Button, Banner, Heading, Text } = usePrimitives();
40
+ const { Field, Input, Button, Banner, Form, Text } = usePrimitives();
41
41
  const dispatcher = useDispatcher();
42
42
 
43
43
  // ponytail: nur die erste Seite (default-limit, nextCursor ignoriert) —
@@ -92,8 +92,7 @@ export function TierAdminScreen(): ReactNode {
92
92
  const canSubmit = tenantId !== "" && tier !== "" && !submitting;
93
93
 
94
94
  return (
95
- <FormScreenShell testId="tier-admin-screen" className="flex max-w-xl flex-col gap-6">
96
- <Heading variant="page">{t("tier-admin.title")}</Heading>
95
+ <FormScreenShell testId="tier-admin-screen" className="flex flex-col gap-6">
97
96
  <Text variant="small">{t("tier-admin.explainer")}</Text>
98
97
 
99
98
  {tenantsQuery.error !== null && (
@@ -107,7 +106,23 @@ export function TierAdminScreen(): ReactNode {
107
106
  </Banner>
108
107
  )}
109
108
 
110
- <Section>
109
+ <Form
110
+ testId="tier-admin-form"
111
+ onSubmit={(e) => {
112
+ e?.preventDefault();
113
+ void onSave();
114
+ }}
115
+ actions={
116
+ <Button
117
+ type="submit"
118
+ disabled={!canSubmit}
119
+ loading={submitting}
120
+ testId="tier-admin-submit"
121
+ >
122
+ {t("tier-admin.submit")}
123
+ </Button>
124
+ }
125
+ >
111
126
  <Field id="tier-admin-tenant" label={t("tier-admin.tenant.label")} required>
112
127
  <Input
113
128
  kind="select"
@@ -147,16 +162,7 @@ export function TierAdminScreen(): ReactNode {
147
162
  {t(status.messageKey)}
148
163
  </Banner>
149
164
  )}
150
-
151
- <Button
152
- onClick={() => void onSave()}
153
- disabled={!canSubmit}
154
- loading={submitting}
155
- testId="tier-admin-submit"
156
- >
157
- {t("tier-admin.submit")}
158
- </Button>
159
- </Section>
165
+ </Form>
160
166
  </FormScreenShell>
161
167
  );
162
168
  }
@@ -5,6 +5,7 @@ import { findForAuthQuery } from "./handlers/find-for-auth.query";
5
5
  import { listQuery } from "./handlers/list.query";
6
6
  import { meQuery } from "./handlers/me.query";
7
7
  import { updateWrite } from "./handlers/update.write";
8
+ import { USER_I18N } from "./i18n";
8
9
  import { userEntity } from "./schema/user";
9
10
  import { userEditScreen, userListScreen } from "./screens";
10
11
 
@@ -43,6 +44,8 @@ export function createUserFeature(): FeatureDefinition {
43
44
  r.screen(userListScreen);
44
45
  r.screen(userEditScreen);
45
46
 
47
+ r.translations({ keys: USER_I18N });
48
+
46
49
  return { handlers, queries };
47
50
  });
48
51
  }
@@ -0,0 +1,20 @@
1
+ type LocalizedString = { readonly de: string; readonly en: string };
2
+
3
+ export const USER_I18N: Readonly<Record<string, LocalizedString>> = {
4
+ "screen:user-list.title": { de: "Benutzer", en: "Users" },
5
+ "screen:user-edit.title": { de: "Benutzer bearbeiten", en: "Edit user" },
6
+ "user:entity:user:field:email": { de: "E-Mail", en: "Email" },
7
+ "user:entity:user:field:displayName": { de: "Anzeigename", en: "Display name" },
8
+ "user:entity:user:field:status": { de: "Status", en: "Status" },
9
+ "user:entity:user:field:emailVerified": { de: "E-Mail bestätigt", en: "Email verified" },
10
+ "user:entity:user:field:locale": { de: "Sprache", en: "Locale" },
11
+ "user:entity:user:field:status:option:active": { de: "Aktiv", en: "Active" },
12
+ "user:entity:user:field:status:option:restricted": { de: "Eingeschränkt", en: "Restricted" },
13
+ "user:entity:user:field:status:option:deletionRequested": {
14
+ de: "Löschung angefordert",
15
+ en: "Deletion requested",
16
+ },
17
+ "user:entity:user:field:status:option:deleted": { de: "Gelöscht", en: "Deleted" },
18
+ "user:entity:user:field:emailVerified:option:true": { de: "Ja", en: "Yes" },
19
+ "user:entity:user:field:emailVerified:option:false": { de: "Nein", en: "No" },
20
+ };
@@ -136,6 +136,7 @@ export const userEntity = createEntity({
136
136
  required: true,
137
137
  default: USER_STATUS.Active,
138
138
  options: USER_STATUS_OPTIONS,
139
+ sortable: true,
139
140
  access: { write: access.privileged },
140
141
  }),
141
142
 
@@ -7,10 +7,6 @@ import type {
7
7
  // feature runs with `r.systemScope()`, the entityList query returns every
8
8
  // user across all tenants — the SystemAdmin platform roster. Both screens are
9
9
  // SystemAdmin-gated and stay inert until an app navs them (no auto-nav).
10
- //
11
- // Field labels come from the renderer's humanizeSlug fallback (no i18n keys
12
- // registered) — "Display Name", "Email Verified" etc. Apps can override via
13
- // their own translations under the `user:entity:user:field:*` convention.
14
10
 
15
11
  export const userListScreen: EntityListScreenDefinition = {
16
12
  id: "user-list",
@@ -26,9 +22,9 @@ export const userListScreen: EntityListScreenDefinition = {
26
22
  entityId: "id",
27
23
  },
28
24
  ],
29
- // No SearchAdapter assumption: search is opt-in per app infra, not a
30
- // universal default for a bundled screen.
31
- searchable: false,
25
+ defaultSort: { field: "status", dir: "asc" },
26
+ // ponytail: screen searchable but email is encrypted PII — list search uses non-encrypted columns only.
27
+ searchable: true,
32
28
  access: { roles: ["SystemAdmin"] },
33
29
  };
34
30
 
@@ -44,14 +40,6 @@ export const userEditScreen: EntityEditScreenDefinition = {
44
40
  },
45
41
  ],
46
42
  },
47
- // `roles` is deliberately NOT editable here: it is a raw JSON text column
48
- // (`["SystemAdmin"]`) — a free-text input would let a typo corrupt the
49
- // privilege column on a live platform. Role management needs a dedicated
50
- // surface; the list still shows status for triage.
51
- //
52
- // Create dispatches user:write:user:create (email + displayName required —
53
- // both in the form). Delete is suppressed: there is no user:write:user:delete
54
- // — user removal is the GDPR status/forget flow, not a hard delete.
55
43
  allowDelete: false,
56
44
  access: { roles: ["SystemAdmin"] },
57
45
  };
@@ -28,6 +28,7 @@ import {
28
28
  import { requestExportWrite } from "./handlers/request-export.write";
29
29
  import { restrictAccountWrite } from "./handlers/restrict-account.write";
30
30
  import { createRunForgetCleanupHandler } from "./handlers/run-forget-cleanup.write";
31
+ import { USER_DATA_RIGHTS_I18N } from "./i18n";
31
32
  import {
32
33
  type GdprMailDefaults,
33
34
  isMailTransportAvailable,
@@ -257,6 +258,22 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
257
258
  r.screen(exportJobDetailScreen);
258
259
  r.screen(downloadAttemptListScreen);
259
260
 
261
+ r.nav({
262
+ id: "export-job-list",
263
+ label: "user-data-rights:nav.exportJobs",
264
+ icon: "download",
265
+ screen: "user-data-rights:screen:export-job-list",
266
+ order: 25,
267
+ access: { roles: ["SystemAdmin"] },
268
+ });
269
+
270
+ r.translations({
271
+ keys: {
272
+ ...USER_DATA_RIGHTS_I18N,
273
+ "user-data-rights:nav.exportJobs": { de: "DSGVO-Exporte", en: "GDPR exports" },
274
+ },
275
+ });
276
+
260
277
  // Dormant Self-Service-Screen (Art. 15/17/18/20): Export, Aktivitäts-
261
278
  // protokoll, Einschränkung, Löschung in einem Screen. Kein r.nav — die
262
279
  // App platziert ihn im eingeloggten Bereich. Die React-Component kommt
@@ -1,37 +1,36 @@
1
- // @runtime client
2
- //
3
- // Default-Translations fuer user-data-rights Error-Wire-Keys (S2.U3 Atom 4b.fix3).
4
- //
5
- // Wire-Errors aus den download-handlers tragen i18nKeys statt fixe
6
- // Strings — UI rendert die Keys ueber den Renderer-LocaleProvider. Apps
7
- // koennen einzelne Keys via `userDataRightsClient({ translations: { de:
8
- // {...} } })` ueberschreiben (Pattern matched auth-email-password).
9
- //
10
- // **Scope 4b.fix3:** nur die download-error-keys. Andere Keys (UI-
11
- // Texte fuer Forget-Pfad, Status-Labels, Banner) kommen mit Atom 6+
12
- // (UI-Integration).
1
+ type LocalizedString = { readonly de: string; readonly en: string };
13
2
 
14
- import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
15
-
16
- export const defaultTranslations: TranslationsByLocale = {
17
- de: {
18
- "userDataRights.errors.download.notFound":
19
- "Der Download-Link ist ungültig oder gehört zu einem anderen Konto.",
20
- "userDataRights.errors.download.expired":
21
- "Dein Download ist abgelaufen. Bitte fordere einen neuen Export an.",
22
- "userDataRights.errors.download.unavailable":
23
- "Der Export ist noch nicht fertig oder fehlgeschlagen. Bitte schau im Status-Polling nach.",
24
- "userDataRights.errors.download.signedUrlNotSupported":
25
- "Der Download steht aufgrund einer Server-Konfiguration aktuell nicht zur Verfügung. Der Operator wurde benachrichtigt.",
3
+ export const USER_DATA_RIGHTS_I18N: Readonly<Record<string, LocalizedString>> = {
4
+ "screen:export-job-list.title": { de: "DSGVO-Exporte", en: "GDPR exports" },
5
+ "screen:export-job-detail.title": { de: "Export-Job", en: "Export job" },
6
+ "screen:download-attempt-list.title": { de: "Download-Versuche", en: "Download attempts" },
7
+ "screen:privacy-center.title": { de: "Datenschutz", en: "Privacy" },
8
+ "user-data-rights:entity:export-job:field:userId": { de: "Benutzer", en: "User" },
9
+ "user-data-rights:entity:export-job:field:status": { de: "Status", en: "Status" },
10
+ "user-data-rights:entity:export-job:field:requestedAt": { de: "Angefordert", en: "Requested" },
11
+ "user-data-rights:entity:export-job:field:completedAt": { de: "Abgeschlossen", en: "Completed" },
12
+ "user-data-rights:entity:export-job:field:expiresAt": { de: "Läuft ab", en: "Expires" },
13
+ "user-data-rights:entity:export-job:field:requestedFromTenantId": {
14
+ de: "Mandant",
15
+ en: "Tenant",
16
+ },
17
+ "user-data-rights:entity:export-job:field:startedAt": { de: "Gestartet", en: "Started" },
18
+ "user-data-rights:entity:export-job:field:downloadStorageKey": {
19
+ de: "Speicher-Schlüssel",
20
+ en: "Storage key",
21
+ },
22
+ "user-data-rights:entity:export-job:field:bytesWritten": { de: "Bytes", en: "Bytes" },
23
+ "user-data-rights:entity:export-job:field:errorMessage": { de: "Fehler", en: "Error" },
24
+ "user-data-rights:entity:download-attempt:field:attemptedAt": {
25
+ de: "Zeitpunkt",
26
+ en: "Attempted at",
26
27
  },
27
- en: {
28
- "userDataRights.errors.download.notFound":
29
- "The download link is invalid or belongs to a different account.",
30
- "userDataRights.errors.download.expired":
31
- "Your download has expired. Please request a new export.",
32
- "userDataRights.errors.download.unavailable":
33
- "The export is not yet ready or has failed. Please check the status endpoint.",
34
- "userDataRights.errors.download.signedUrlNotSupported":
35
- "The download is currently unavailable due to a server configuration issue. The operator has been notified.",
28
+ "user-data-rights:entity:download-attempt:field:result": { de: "Ergebnis", en: "Result" },
29
+ "user-data-rights:entity:download-attempt:field:via": { de: "Via", en: "Via" },
30
+ "user-data-rights:entity:download-attempt:field:ip": { de: "IP", en: "IP" },
31
+ "user-data-rights:entity:download-attempt:field:attemptedByUserId": {
32
+ de: "Benutzer",
33
+ en: "User",
36
34
  },
35
+ "user-data-rights:entity:download-attempt:field:jobId": { de: "Job", en: "Job" },
37
36
  };
@@ -73,6 +73,7 @@ export const exportJobEntity = createEntity({
73
73
  // nur Storage.
74
74
  userId: createTextField({
75
75
  required: true,
76
+ searchable: true,
76
77
  }),
77
78
 
78
79
  // **requestedFromTenantId** — der Tenant aus dem der User den Antrag
@@ -102,6 +103,7 @@ export const exportJobEntity = createEntity({
102
103
  // entsteht ohne Klick.
103
104
  requestedAt: createTimestampField({
104
105
  required: true,
106
+ sortable: true,
105
107
  }),
106
108
 
107
109
  // Wann hat der Worker mit dem Pickup angefangen. NULL solange
@@ -28,7 +28,8 @@ export const exportJobListScreen: EntityListScreenDefinition = {
28
28
  entityId: "id",
29
29
  },
30
30
  ],
31
- searchable: false,
31
+ defaultSort: { field: "requestedAt", dir: "desc" },
32
+ searchable: true,
32
33
  access: { roles: access.systemAdmin },
33
34
  };
34
35
 
@@ -141,5 +141,14 @@ export const defaultTranslations: TranslationsByLocale = {
141
141
  "userDataRights.privacyCenter.deletion.dialogTitle": "Delete your account?",
142
142
  "userDataRights.privacyCenter.deletion.dialogDescription":
143
143
  "Confirming starts the deletion grace period. You can cancel the deletion until it ends.",
144
+
145
+ "userDataRights.errors.download.notFound":
146
+ "The download link is invalid or belongs to a different account.",
147
+ "userDataRights.errors.download.expired":
148
+ "Your download has expired. Please request a new export.",
149
+ "userDataRights.errors.download.unavailable":
150
+ "The export is not yet ready or has failed. Please check the status endpoint.",
151
+ "userDataRights.errors.download.signedUrlNotSupported":
152
+ "The download is currently unavailable due to a server configuration issue. The operator has been notified.",
144
153
  },
145
154
  };