@cosmicdrift/kumiko-samples 0.383.1 → 0.385.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 (83) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +6 -1
  3. package/packages/bundled-features/src/audit/changes.json +6 -0
  4. package/packages/bundled-features/src/audit/event-classification.ts +83 -0
  5. package/packages/bundled-features/src/audit/feature.ts +3 -2
  6. package/packages/bundled-features/src/audit/handlers/audit-events.ts +63 -5
  7. package/packages/bundled-features/src/audit/handlers/details-all-tenants.query.ts +6 -5
  8. package/packages/bundled-features/src/audit/handlers/details.query.ts +12 -6
  9. package/packages/bundled-features/src/audit/handlers/list-all-tenants.query.ts +20 -12
  10. package/packages/bundled-features/src/audit/handlers/list.query.ts +14 -3
  11. package/packages/bundled-features/src/audit/screens.ts +32 -4
  12. package/packages/bundled-features/src/audit/web/audit-event-cell.tsx +22 -0
  13. package/packages/bundled-features/src/audit/web/audit-event-labels.ts +30 -0
  14. package/packages/bundled-features/src/audit/web/client-plugin.tsx +10 -1
  15. package/packages/bundled-features/src/audit/web/constants.ts +11 -0
  16. package/packages/bundled-features/src/audit/web/i18n.ts +48 -1
  17. package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
  18. package/packages/bundled-features/src/auth-email-password/feature.ts +3 -2
  19. package/packages/bundled-features/src/auth-email-password/handlers/login-options.query.ts +19 -0
  20. package/packages/bundled-features/src/auth-email-password/index.ts +7 -1
  21. package/packages/bundled-features/src/auth-email-password/password-policy.ts +196 -2087
  22. package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +9 -12
  23. package/packages/bundled-features/src/auth-email-password/{auth-paths.ts → web/auth-paths.ts} +7 -4
  24. package/packages/bundled-features/src/auth-email-password/web/client-plugin.ts +7 -0
  25. package/packages/bundled-features/src/auth-email-password/web/constants.ts +1 -0
  26. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +31 -0
  27. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +68 -42
  28. package/packages/bundled-features/src/channel-in-app/handlers/unread-count.query.ts +1 -1
  29. package/packages/bundled-features/src/custom-fields/db/queries/projection.ts +1 -1
  30. package/packages/bundled-features/src/file-derivatives/feature.ts +1 -1
  31. package/packages/bundled-features/src/jobs/changes.json +6 -0
  32. package/packages/bundled-features/src/jobs/feature.ts +34 -127
  33. package/packages/bundled-features/src/jobs/handlers/consumer.write.ts +98 -0
  34. package/packages/bundled-features/src/jobs/handlers/consumers.query.ts +137 -0
  35. package/packages/bundled-features/src/jobs/screens.ts +262 -0
  36. package/packages/bundled-features/src/jobs/web/constants.ts +29 -0
  37. package/packages/bundled-features/src/jobs/web/i18n.ts +41 -0
  38. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  39. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  40. package/packages/bundled-features/src/personal-access-tokens/changes.json +7 -0
  41. package/packages/bundled-features/src/personal-access-tokens/i18n.ts +4 -0
  42. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -0
  43. package/packages/bundled-features/src/secrets/i18n.ts +0 -3
  44. package/packages/bundled-features/src/sessions/changes.json +6 -0
  45. package/packages/bundled-features/src/sessions/constants.ts +3 -1
  46. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +7 -3
  47. package/packages/bundled-features/src/sessions/handlers/list.query.ts +16 -10
  48. package/packages/bundled-features/src/sessions/handlers/mine.query.ts +15 -9
  49. package/packages/bundled-features/src/sessions/screens.ts +23 -8
  50. package/packages/bundled-features/src/sessions/web/client-plugin.tsx +17 -0
  51. package/packages/bundled-features/src/sessions/web/constants.ts +3 -0
  52. package/packages/bundled-features/src/sessions/web/default-translations.ts +8 -0
  53. package/packages/bundled-features/src/sessions/web/i18n.ts +5 -1
  54. package/packages/bundled-features/src/sessions/web/index.ts +1 -0
  55. package/packages/bundled-features/src/sessions/web/session-device-cell.tsx +19 -0
  56. package/packages/bundled-features/src/sessions/web/user-agent.ts +74 -0
  57. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +1 -1
  58. package/packages/bundled-features/src/template-resolver/changes.json +6 -0
  59. package/packages/bundled-features/src/template-resolver/feature.ts +2 -0
  60. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +34 -1
  61. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +53 -0
  62. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +51 -0
  63. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +7 -38
  64. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  65. package/packages/bundled-features/src/user-data-rights/changes.json +18 -0
  66. package/packages/bundled-features/src/user-data-rights/feature.ts +33 -33
  67. package/packages/bundled-features/src/user-data-rights/web/i18n.ts +4 -1
  68. package/samples/apps/showcase/src/features/items/schema/item.ts +8 -2
  69. package/samples/apps/ui-walkthrough/src/features/tasks/schema/task.ts +2 -2
  70. package/samples/apps/use-all-bundled/bin/main.ts +7 -1
  71. package/samples/apps/use-all-bundled/feature-manifest.json +19 -1
  72. package/samples/apps/use-all-bundled/kumiko/schema.ts +3 -1
  73. package/samples/apps/use-all-bundled/src/app/client.tsx +2 -0
  74. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +4 -2
  75. package/samples/apps/use-all-bundled/src/app/invoice-reports-feature.ts +30 -0
  76. package/samples/apps/use-all-bundled/src/app/pat-scope-labels-feature.ts +21 -0
  77. package/samples/apps/use-all-bundled/src/app/seed.ts +51 -0
  78. package/samples/apps/use-all-bundled/src/run-config.ts +5 -0
  79. package/samples/apps/workspaces/src/features/demo/schema/order.ts +2 -2
  80. package/samples/recipes/custom-fields-basic/src/feature.ts +13 -3
  81. package/samples/recipes/designer-demo/src/feature.ts +6 -2
  82. package/samples/recipes/i18n/README.md +15 -0
  83. package/samples/recipes/i18n/src/feature.ts +4 -0
@@ -399,7 +399,9 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
399
399
  },
400
400
  },
401
401
  dormant: true,
402
- description: "userDataRights.privacyCenter.subtitle",
402
+ description: showDeletion
403
+ ? "userDataRights.privacyCenter.subtitle"
404
+ : "userDataRights.privacyCenter.subtitleWithoutDeletion",
403
405
  fieldLabels: {
404
406
  status: i18nKey("userDataRights.privacyCenter.field.status"),
405
407
  gracePeriodEnd: i18nKey("userDataRights.privacyCenter.field.gracePeriodEnd"),
@@ -425,6 +427,17 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
425
427
  renderer: { format: "enumOption", keyPrefix: STATUS_OPTION_KEY_PREFIX },
426
428
  },
427
429
  ],
430
+ actions: [
431
+ {
432
+ id: "restrict",
433
+ icon: "lock",
434
+ label: i18nKey("userDataRights.privacyCenter.restriction.restrict"),
435
+ handler: UserDataRightsHandlers.restrictAccount,
436
+ confirm: i18nKey("userDataRights.privacyCenter.restriction.dialogDescription"),
437
+ visible: { field: "status", ne: "restricted" },
438
+ style: "secondary",
439
+ },
440
+ ],
428
441
  },
429
442
  ...(showDeletion
430
443
  ? [
@@ -441,43 +454,30 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
441
454
  visible: { field: "status", eq: "deletionRequested" },
442
455
  },
443
456
  ],
457
+ actions: [
458
+ {
459
+ id: "request-deletion",
460
+ label: i18nKey("userDataRights.privacyCenter.deletion.delete"),
461
+ handler: UserDataRightsHandlers.requestDeletion,
462
+ confirm: i18nKey("userDataRights.privacyCenter.deletion.dialogDescription"),
463
+ visible: { field: "status", ne: "deletionRequested" },
464
+ style: "danger" as const,
465
+ },
466
+ {
467
+ id: "cancel-deletion",
468
+ label: i18nKey("userDataRights.privacyCenter.deletion.cancel"),
469
+ handler: UserDataRightsHandlers.cancelDeletion,
470
+ visible: { field: "status", eq: "deletionRequested" },
471
+ style: "secondary" as const,
472
+ // Declared: the id's last segment ("deletion") would otherwise resolve to the destructive trash icon.
473
+ icon: "x" as const,
474
+ },
475
+ ],
444
476
  },
445
477
  ]
446
478
  : []),
447
479
  ],
448
480
  },
449
- actions: [
450
- {
451
- id: "restrict",
452
- icon: "lock",
453
- label: i18nKey("userDataRights.privacyCenter.restriction.restrict"),
454
- handler: UserDataRightsHandlers.restrictAccount,
455
- confirm: i18nKey("userDataRights.privacyCenter.restriction.dialogDescription"),
456
- visible: { field: "status", ne: "restricted" },
457
- style: "danger",
458
- },
459
- ...(showDeletion
460
- ? [
461
- {
462
- id: "request-deletion",
463
- label: i18nKey("userDataRights.privacyCenter.deletion.delete"),
464
- handler: UserDataRightsHandlers.requestDeletion,
465
- confirm: i18nKey("userDataRights.privacyCenter.deletion.dialogDescription"),
466
- visible: { field: "status", ne: "deletionRequested" },
467
- style: "danger" as const,
468
- },
469
- {
470
- id: "cancel-deletion",
471
- label: i18nKey("userDataRights.privacyCenter.deletion.cancel"),
472
- handler: UserDataRightsHandlers.cancelDeletion,
473
- visible: { field: "status", eq: "deletionRequested" },
474
- style: "secondary" as const,
475
- // Declared: the id's last segment ("deletion") would otherwise resolve to the destructive trash icon.
476
- icon: "x" as const,
477
- },
478
- ]
479
- : []),
480
- ],
481
481
  });
482
482
 
483
483
  // Magic-link path (anonymous): the email link carries the token as a
@@ -14,7 +14,10 @@ export const USER_DATA_RIGHTS_I18N: Readonly<Record<string, LocalizedString>> =
14
14
  "screen:download-attempt-list.title": { en: "Download attempts" },
15
15
  "screen:privacy-center.title": { en: "Privacy" },
16
16
  "userDataRights.privacyCenter.subtitle": {
17
- en: "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).",
17
+ en: "Download a copy of your data (Art. 20), restrict processing of your account (Art. 18) or delete it (Art. 17).",
18
+ },
19
+ "userDataRights.privacyCenter.subtitleWithoutDeletion": {
20
+ en: "Download a copy of your data (Art. 20) or restrict processing of your account (Art. 18).",
18
21
  },
19
22
  "user-data-rights:entity:export-job:field:userId": { en: "User" },
20
23
  "user-data-rights:entity:export-job:field:status": { en: "Status" },
@@ -19,8 +19,14 @@ export const itemEntity = {
19
19
  description:
20
20
  "A showcase task item carrying a title, free-text notes, a numeric priority, a done flag, a due date and a draft/active/blocked/done status, plus an optional parent item and a list of related items.",
21
21
  fields: {
22
- title: { type: "text", required: true, sortable: true, searchable: true },
23
- notes: { type: "text", multiline: { rows: 4 } },
22
+ title: {
23
+ type: "text",
24
+ required: true,
25
+ sortable: true,
26
+ searchable: true,
27
+ allowPlaintext: "is_business_data",
28
+ },
29
+ notes: { type: "text", multiline: { rows: 4 }, allowPlaintext: "is_business_data" },
24
30
  priority: { type: "number", default: 1, sortable: true, filterable: true },
25
31
  isDone: { type: "boolean", default: false, sortable: true, filterable: true },
26
32
  dueDate: { type: "date", sortable: true, filterable: true },
@@ -29,12 +29,12 @@ export const taskEntity = {
29
29
  table: "read_ui_walkthrough_tasks",
30
30
 
31
31
  fields: {
32
- title: { type: "text", required: true, sortable: true },
32
+ title: { type: "text", required: true, sortable: true, allowPlaintext: "is_business_data" },
33
33
  status: { type: "select", options: TASK_STATUSES, display: "radio", sortable: true },
34
34
  area: { type: "select", options: TASK_AREAS },
35
35
  priority: { type: "number" },
36
36
  isUrgent: { type: "boolean", default: false },
37
- notes: { type: "text" },
37
+ notes: { type: "text", allowPlaintext: "is_business_data" },
38
38
  },
39
39
  } as unknown as EntityDefinition;
40
40
 
@@ -8,7 +8,12 @@ import { InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
8
8
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
10
10
  import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
11
- import { APP_FEATURES, AUTH_COMPOSE_OPTIONS, BOOT_FEATURES } from "../src/run-config";
11
+ import {
12
+ APP_FEATURES,
13
+ AUTH_COMPOSE_OPTIONS,
14
+ BOOT_FEATURES,
15
+ VALIDATE_BOOT_OPTIONS,
16
+ } from "../src/run-config";
12
17
 
13
18
  const SMOKE_TENANT_ID = "00000000-0000-4000-8000-000000000001" as TenantId;
14
19
 
@@ -21,6 +26,7 @@ await runProdApp({
21
26
  features: BOOT_FEATURES,
22
27
  featureToggles: true,
23
28
  envSchema,
29
+ validateBootOptions: VALIDATE_BOOT_OPTIONS,
24
30
  // Smoke-only: exercises the hard PII boot gate + blind-index gate with
25
31
  // every bundled feature mounted. Real apps wire createPgKmsAdapter(...).
26
32
  kms: new InMemoryKmsAdapter(),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "samples/apps/use-all-bundled APP_FEATURES (composeFeatures includeBundled + signup)",
3
- "featureCount": 86,
3
+ "featureCount": 87,
4
4
  "features": [
5
5
  {
6
6
  "name": "account-security",
@@ -1473,6 +1473,10 @@
1473
1473
  "configKeys": [],
1474
1474
  "secrets": [],
1475
1475
  "writeHandlers": [
1476
+ "jobs:write:consumer-disable",
1477
+ "jobs:write:consumer-enable",
1478
+ "jobs:write:consumer-restart",
1479
+ "jobs:write:consumer-skip",
1476
1480
  "jobs:write:retry",
1477
1481
  "jobs:write:trigger"
1478
1482
  ],
@@ -2066,6 +2070,20 @@
2066
2070
  "secrets": [],
2067
2071
  "writeHandlers": []
2068
2072
  },
2073
+ {
2074
+ "name": "pat-scope-labels",
2075
+ "description": "German labels for the token scopes this sample app declares.",
2076
+ "toggleableDefault": null,
2077
+ "requires": [],
2078
+ "optionalRequires": [],
2079
+ "configReads": [],
2080
+ "exposesApis": [],
2081
+ "usesApis": [],
2082
+ "extensionsUsed": [],
2083
+ "configKeys": [],
2084
+ "secrets": [],
2085
+ "writeHandlers": []
2086
+ },
2069
2087
  {
2070
2088
  "name": "personal-access-tokens",
2071
2089
  "description": "Long-lived, revocable Personal Access Tokens for headless HTTP-API access. Stores SHA-256 token hashes in the `store_api_tokens` direct-write table; the plaintext is returned once at creation. `create`/`revoke`/`mine` manage a user's own tokens and `available-scopes` lists the app-declared scope catalog. Bearer tokens carrying the PAT prefix are resolved before jwt.verify (roles resolved live, granted scopes enforced fail-closed at the API boundary) — registered as an auth-foundation tokenVerifier provider, resolved generically by the middleware. Pass { toggleable: { default: false } } to tier-gate the whole feature.",
@@ -11,7 +11,9 @@
11
11
  import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
12
12
  import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
13
13
  import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
14
- import { APP_FEATURES, AUTH_COMPOSE_OPTIONS } from "../src/run-config";
14
+ import { APP_FEATURES, AUTH_COMPOSE_OPTIONS, VALIDATE_BOOT_OPTIONS } from "../src/run-config";
15
+
16
+ export { VALIDATE_BOOT_OPTIONS };
15
17
 
16
18
  export const FEATURES: readonly FeatureDefinition[] = composeFeatures(APP_FEATURES, {
17
19
  includeBundled: true,
@@ -27,6 +27,7 @@ import { deliveryClient } from "@cosmicdrift/kumiko-bundled-features/delivery/we
27
27
  import { featureTogglesClient } from "@cosmicdrift/kumiko-bundled-features/feature-toggles/web";
28
28
  import { foldersClient } from "@cosmicdrift/kumiko-bundled-features/folders/web";
29
29
  import { jobsClient } from "@cosmicdrift/kumiko-bundled-features/jobs/web";
30
+ import { sessionsClient } from "@cosmicdrift/kumiko-bundled-features/sessions/web";
30
31
  import { tagsClient } from "@cosmicdrift/kumiko-bundled-features/tags/web";
31
32
  import { textBlocksClient } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
32
33
  import { tenantClient } from "@cosmicdrift/kumiko-bundled-features/tenant/web";
@@ -103,6 +104,7 @@ createKumikoApp({
103
104
  adminShellClient(),
104
105
  tenantClient(),
105
106
  auditClient(),
107
+ sessionsClient(),
106
108
  billingFoundationClient(),
107
109
  jobsClient(),
108
110
  deliveryClient(),
@@ -13,8 +13,9 @@
13
13
  import { createTemplateResolverApi } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
14
14
  import type { RunDevAppOptions } from "@cosmicdrift/kumiko-dev-server";
15
15
  import { SYSTEM_TENANT_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
16
- import { AUTH_COMPOSE_OPTIONS, BOOT_FEATURES } from "../run-config";
16
+ import { AUTH_COMPOSE_OPTIONS, BOOT_FEATURES, VALIDATE_BOOT_OPTIONS } from "../run-config";
17
17
  import { ADMIN_EMAIL, ADMIN_PASSWORD, BETA_TENANT_ID, DEV_TENANT_ID } from "./auth-constants";
18
+ import { invoiceReportsFeature } from "./invoice-reports-feature";
18
19
  import { notesFeature } from "./notes-feature";
19
20
  import { appScreensFeature } from "./screens-feature";
20
21
  import { seedScreenshotData } from "./seed";
@@ -27,8 +28,9 @@ import { seedScreenshotData } from "./seed";
27
28
  export const devAppOptions = {
28
29
  // notesFeature is dev-only (app-owned tag host) — see notes-feature.ts for why
29
30
  // it stays out of APP_FEATURES. appScreensFeature places the user-profile screen.
30
- features: [...BOOT_FEATURES, appScreensFeature, notesFeature],
31
+ features: [...BOOT_FEATURES, appScreensFeature, notesFeature, invoiceReportsFeature],
31
32
  featureToggles: true,
33
+ validateBootOptions: VALIDATE_BOOT_OPTIONS,
32
34
  port: Number.parseInt(process.env["PORT"] ?? "4186", 10),
33
35
  clientEntry: "./src/app/client.tsx",
34
36
  htmlPath: "./public/index.html",
@@ -0,0 +1,30 @@
1
+ // @runtime dev
2
+ //
3
+ // Dev-/screenshot-only host for realistic event consumers. The consumers never
4
+ // receive an event here; the screenshot seed parks them in the dead/disabled
5
+ // states the jobs:event-consumers screen is meant to show.
6
+
7
+ import { entityEventName } from "@cosmicdrift/kumiko-framework/db";
8
+ import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
9
+
10
+ export const INVOICE_TOTALS_CONSUMER = "invoice-totals";
11
+ export const OVERDUE_REMINDERS_CONSUMER = "overdue-reminders";
12
+ export const CUSTOMER_STATEMENTS_CONSUMER = "customer-statements";
13
+
14
+ const noop = async (): Promise<void> => {};
15
+
16
+ export const invoiceReportsFeature: FeatureDefinition = defineFeature("invoice-reports", (r) => {
17
+ r.describe(
18
+ "Dev-only host for the invoice read models whose consumer state the screenshot seed sets.",
19
+ );
20
+ for (const name of [
21
+ INVOICE_TOTALS_CONSUMER,
22
+ OVERDUE_REMINDERS_CONSUMER,
23
+ CUSTOMER_STATEMENTS_CONSUMER,
24
+ ]) {
25
+ r.multiStreamProjection({
26
+ name,
27
+ apply: { [entityEventName("invoice", "created")]: noop },
28
+ });
29
+ }
30
+ });
@@ -0,0 +1,21 @@
1
+ import { patScopeOptionKey } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
2
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
3
+
4
+ // The scope domains in run-config.ts are the app's choice, so their German
5
+ // labels are too: personal-access-tokens only generates the English ones.
6
+ export const patScopeLabelsFeature = defineFeature("pat-scope-labels", (r) => {
7
+ r.describe("German labels for the token scopes this sample app declares.");
8
+ r.translations({
9
+ keys: {
10
+ [patScopeOptionKey("pages", "read")]: { de: "Seiten (lesen)" },
11
+ [patScopeOptionKey("pages", "write")]: { de: "Seiten (lesen & schreiben)" },
12
+ [patScopeOptionKey("tags", "read")]: { de: "Tags (lesen)" },
13
+ [patScopeOptionKey("tags", "write")]: { de: "Tags (lesen & schreiben)" },
14
+ [patScopeOptionKey("notesHistory", "read")]: { de: "Notizen (lesen)" },
15
+ [patScopeOptionKey("notesHistory", "write")]: { de: "Notizen (lesen & schreiben)" },
16
+ [patScopeOptionKey("ledger", "read")]: { de: "Hauptbuch (lesen)" },
17
+ [patScopeOptionKey("ledger", "write")]: { de: "Hauptbuch (lesen & schreiben)" },
18
+ },
19
+ });
20
+ return {};
21
+ });
@@ -30,6 +30,7 @@ import {
30
30
  type DbConnection,
31
31
  fetchOne,
32
32
  insertOne,
33
+ upsertOnConflict,
33
34
  } from "@cosmicdrift/kumiko-framework/db";
34
35
  import {
35
36
  createEntityExecutor,
@@ -37,11 +38,17 @@ import {
37
38
  SYSTEM_TENANT_ID,
38
39
  type TenantId,
39
40
  } from "@cosmicdrift/kumiko-framework/engine";
41
+ import {
42
+ ConsumerStatuses,
43
+ eventConsumerStateTable,
44
+ SHARED_INSTANCE_SENTINEL,
45
+ } from "@cosmicdrift/kumiko-framework/pipeline";
40
46
  import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
41
47
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
42
48
  import { unsafeCreateEntityTable } from "@cosmicdrift/kumiko-testing/stack";
43
49
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
44
50
  import { ADMIN_EMAIL, BETA_TENANT_ID, DEMO_NOTE_ID, DEV_TENANT_ID } from "./auth-constants";
51
+ import { INVOICE_TOTALS_CONSUMER, OVERDUE_REMINDERS_CONSUMER } from "./invoice-reports-feature";
45
52
  import { noteEntity } from "./notes-feature";
46
53
  import { seedPdfDocuments } from "./seed-documents";
47
54
 
@@ -266,6 +273,48 @@ const PRIVACY_BODY = [
266
273
  "Access, rectification, erasure, restriction, portability, objection.",
267
274
  ].join("\n");
268
275
 
276
+ // Dispatcher state, not an event-sourced table: parks two invoice consumers the
277
+ // way a failing handler and an operator pause would leave them, so the
278
+ // jobs:event-consumers screens have a dead and a paused row to show.
279
+ async function seedEventConsumerStates(stack: Parameters<SeedFn>[0]): Promise<void> {
280
+ const qualifiedNameOf = (consumer: string): string => {
281
+ const found = [...stack.registry.getAllMultiStreamProjections().keys()].find(
282
+ (name) => name === consumer || name.endsWith(`:${consumer}`),
283
+ );
284
+ if (!found) throw new Error(`seedEventConsumerStates: consumer "${consumer}" not registered`);
285
+ return found;
286
+ };
287
+ const parked = [
288
+ {
289
+ consumer: INVOICE_TOTALS_CONSUMER,
290
+ status: ConsumerStatuses.dead,
291
+ attempts: 5,
292
+ lastError:
293
+ 'TypeError: Cannot read properties of undefined (reading "amount") while applying invoice.created (event 48213): payload.lineItems[2].netPrice.amount is missing',
294
+ },
295
+ {
296
+ consumer: OVERDUE_REMINDERS_CONSUMER,
297
+ status: ConsumerStatuses.disabled,
298
+ attempts: 0,
299
+ lastError: null,
300
+ },
301
+ ];
302
+ for (const { consumer, status, attempts, lastError } of parked) {
303
+ await upsertOnConflict(
304
+ stack.db,
305
+ eventConsumerStateTable,
306
+ {
307
+ name: qualifiedNameOf(consumer),
308
+ instanceId: SHARED_INSTANCE_SENTINEL,
309
+ status,
310
+ attempts,
311
+ lastError,
312
+ },
313
+ { conflictKeys: ["name", "instanceId"], update: { status, attempts, lastError } },
314
+ );
315
+ }
316
+ }
317
+
269
318
  export const seedScreenshotData: SeedFn = async (stack) => {
270
319
  const devTenant = DEV_TENANT_ID as TenantId;
271
320
  const betaTenant = BETA_TENANT_ID as TenantId;
@@ -384,6 +433,8 @@ export const seedScreenshotData: SeedFn = async (stack) => {
384
433
  // custom-fields + folders extension sections on note-edit.
385
434
  await seedCustomFieldsAndFolders(stack, devTenant, devAdminId);
386
435
 
436
+ await seedEventConsumerStates(stack);
437
+
387
438
  // personal-access-tokens list for the admin (active tenant = dev).
388
439
  await seedApiTokens(stack.db, devTenant);
389
440
 
@@ -102,6 +102,7 @@ import { DEV_TENANT_ID } from "./app/auth-constants";
102
102
  import { CAP_OVERVIEW_CAPS } from "./app/cap-overview-caps";
103
103
  import { capOverviewLabelsFeature } from "./app/cap-overview-labels-feature";
104
104
  import { collectionLabelsFeature } from "./app/collection-labels-feature";
105
+ import { patScopeLabelsFeature } from "./app/pat-scope-labels-feature";
105
106
 
106
107
  // Smoke-only stubs. Boot-mode skipt jede operative Methode — diese werden
107
108
  // nie aufgerufen, nur typecheck'd.
@@ -161,6 +162,7 @@ export const APP_FEATURES = [
161
162
  ledger: { label: "Ledger", read: ["ledger:query:*"], write: ["ledger:write:*"] },
162
163
  },
163
164
  }),
165
+ patScopeLabelsFeature,
164
166
  createReadinessFeature(),
165
167
 
166
168
  // delivery + channels
@@ -417,6 +419,9 @@ export const BOOT_FEATURES = APP_FEATURES.filter(
417
419
  // Smoke signup — enables createAuthSelfRegistrationToggleFeature via
418
420
  // composeFeatures when passed as authOptions (#1521 Option A). Same shape
419
421
  // as run{Dev,Prod}App's auth.signup; appUrl is a stub (no real mail in boot).
422
+ // locale-de is mounted, so every translation key must exist in German and English.
423
+ export const VALIDATE_BOOT_OPTIONS = { requiredLocales: ["de", "en"] } as const;
424
+
420
425
  export const AUTH_COMPOSE_OPTIONS = {
421
426
  signup: {
422
427
  appUrl: "http://localhost:4186/signup",
@@ -12,8 +12,8 @@ import type {
12
12
  export const orderEntity = {
13
13
  table: "read_ws_orders",
14
14
  fields: {
15
- label: { type: "text", required: true, sortable: true },
16
- status: { type: "text", sortable: true },
15
+ label: { type: "text", required: true, sortable: true, allowPlaintext: "is_business_data" },
16
+ status: { type: "text", sortable: true, allowPlaintext: "is_business_data" },
17
17
  notes: { type: "text", allowPlaintext: "is_business_data" },
18
18
  },
19
19
  } as unknown as EntityDefinition;
@@ -24,7 +24,7 @@ import {
24
24
  wireCustomFieldsFor,
25
25
  } from "@cosmicdrift/kumiko-bundled-features/custom-fields";
26
26
  import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
27
- import { createEntity, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
27
+ import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
28
28
  import { z } from "zod";
29
29
 
30
30
  // --- Entity ---
@@ -39,7 +39,12 @@ import { z } from "zod";
39
39
  export const propertyEntity = createEntity({
40
40
  table: "read_sample_cf_properties",
41
41
  fields: {
42
- name: { type: "text", required: true, maxLength: 200 },
42
+ name: createTextField({
43
+ required: true,
44
+ maxLength: 200,
45
+ personal: false,
46
+ reason: "property_name_is_business_data",
47
+ }),
43
48
  customFields: customFieldsField(),
44
49
  },
45
50
  });
@@ -59,7 +64,12 @@ export const propertyFeature = defineFeature("property-management", (r) => {
59
64
  name: "property",
60
65
  table: "read_sample_cf_properties",
61
66
  fields: {
62
- name: { type: "text", required: true, maxLength: 200 },
67
+ name: createTextField({
68
+ required: true,
69
+ maxLength: 200,
70
+ personal: false,
71
+ reason: "property_name_is_business_data",
72
+ }),
63
73
  customFields: { type: "jsonb" },
64
74
  },
65
75
  });
@@ -6,7 +6,7 @@
6
6
  // - nav → ID/Label/Screen-Card
7
7
  // Plus optionalRequires + metric als JSON-Dump-Fallback.
8
8
 
9
- import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
9
+ import { createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
10
10
  import { z } from "zod";
11
11
 
12
12
  defineFeature("designerDemo", (r) => {
@@ -15,7 +15,11 @@ defineFeature("designerDemo", (r) => {
15
15
  r.entity({
16
16
  name: "task",
17
17
  fields: {
18
- title: { type: "text", required: true },
18
+ title: createTextField({
19
+ required: true,
20
+ personal: false,
21
+ reason: "demo_task_title_is_not_personal_data",
22
+ }),
19
23
  done: { type: "boolean", default: false },
20
24
  priority: { type: "select", options: ["low", "medium", "high"], default: "medium" },
21
25
  },
@@ -6,6 +6,7 @@ Feature-scoped translations — 'Hallo' and 'Hello' in the same app.
6
6
 
7
7
  - `r.translations()` for multi-language feature keys
8
8
  - Plurals
9
+ - `requiredLocales`: boot fails when a key misses one of the app's languages
9
10
 
10
11
  ## Plurals
11
12
 
@@ -24,6 +25,20 @@ exist where the locale's grammar needs them:
24
25
  `Intl.PluralRules(locale)`, then interpolates `{count}` (and any other
25
26
  `{name}` placeholder) into the chosen form.
26
27
 
28
+ ## Required locales
29
+
30
+ Without further options boot only checks that each feature is consistent with
31
+ itself. An app that ships fixed languages lists them, and boot then fails on any
32
+ key that misses one of them, including framework keys. Locale packages such as
33
+ `locale-de` count:
34
+
35
+ ```ts illustration
36
+ runProdApp({ ...options, validateBootOptions: { requiredLocales: ["de", "en"] } });
37
+ ```
38
+
39
+ Define the options once and re-export them as `VALIDATE_BOOT_OPTIONS` from
40
+ `kumiko/schema.ts`, so `kumiko-schema validate` checks the same list in CI.
41
+
27
42
  ## Source
28
43
 
29
44
  Feature entry point: `src/feature.ts`.
@@ -11,20 +11,24 @@ export const greetingFeature = defineFeature("greeting", (r) => {
11
11
  de: "Willkommen",
12
12
  en: "Welcome",
13
13
  fr: "Bienvenue",
14
+ pl: "Witamy",
14
15
  },
15
16
  "greeting.goodbye": {
16
17
  de: "Auf Wiedersehen",
17
18
  en: "Goodbye",
18
19
  fr: "Au revoir",
20
+ pl: "Do widzenia",
19
21
  },
20
22
  "greeting.hello_name": {
21
23
  de: "Hallo, {name}!",
22
24
  en: "Hello, {name}!",
23
25
  fr: "Bonjour, {name}!",
26
+ pl: "Cześć, {name}!",
24
27
  },
25
28
  "greeting.unread_count": {
26
29
  de: { one: "{count} ungelesene Nachricht", other: "{count} ungelesene Nachrichten" },
27
30
  en: { one: "{count} unread message", other: "{count} unread messages" },
31
+ fr: { one: "{count} message non lu", other: "{count} messages non lus" },
28
32
  pl: {
29
33
  one: "{count} nieprzeczytana wiadomość",
30
34
  few: "{count} nieprzeczytane wiadomości",