@cosmicdrift/kumiko-samples 0.363.0 → 0.364.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 (140) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +7 -1
  3. package/packages/bundled-features/src/audit/changes.json +6 -0
  4. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  5. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
  6. package/packages/bundled-features/src/auth-email-password/changes.json +37 -0
  7. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  8. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  9. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  10. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  11. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
  12. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  13. package/packages/bundled-features/src/billing-foundation/changes.json +36 -0
  14. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
  15. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  16. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
  17. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  18. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
  19. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  20. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +10 -1
  21. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
  22. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  23. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  24. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  25. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
  26. package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
  27. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +17 -5
  28. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  29. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  30. package/packages/bundled-features/src/cap-counter/changes.json +6 -0
  31. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  32. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  33. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  34. package/packages/bundled-features/src/channel-email/email-channel.ts +17 -4
  35. package/packages/bundled-features/src/config/changes.json +6 -0
  36. package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
  37. package/packages/bundled-features/src/config/resolver.ts +15 -12
  38. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  39. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  40. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +48 -27
  41. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  42. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  43. package/packages/bundled-features/src/delivery/attachment-ref.ts +21 -0
  44. package/packages/bundled-features/src/delivery/changes.json +18 -0
  45. package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
  46. package/packages/bundled-features/src/delivery/index.ts +2 -0
  47. package/packages/bundled-features/src/delivery/jobs.ts +51 -14
  48. package/packages/bundled-features/src/delivery/types.ts +7 -1
  49. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  50. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  51. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
  52. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  53. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  54. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  55. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  56. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
  57. package/packages/bundled-features/src/jobs/changes.json +19 -0
  58. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  59. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  60. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  61. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  62. package/packages/bundled-features/src/jobs/job-run-logger.ts +52 -17
  63. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  64. package/packages/bundled-features/src/ledger/changes.json +7 -0
  65. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  66. package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
  67. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  68. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  69. package/packages/bundled-features/src/secrets/changes.json +6 -0
  70. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  71. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  72. package/packages/bundled-features/src/shared/changes.json +6 -0
  73. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  74. package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
  75. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  76. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  77. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  78. package/packages/bundled-features/src/template-resolver/README.md +4 -2
  79. package/packages/bundled-features/src/template-resolver/api.ts +3 -2
  80. package/packages/bundled-features/src/template-resolver/changes.json +68 -0
  81. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +77 -13
  82. package/packages/bundled-features/src/template-resolver/constants.ts +12 -2
  83. package/packages/bundled-features/src/template-resolver/document-schema.ts +26 -4
  84. package/packages/bundled-features/src/template-resolver/feature.ts +4 -1
  85. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +31 -13
  86. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +5 -44
  87. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  88. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
  89. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
  90. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
  91. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
  92. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  93. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +90 -17
  94. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +1 -1
  95. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
  96. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
  97. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
  98. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
  99. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
  100. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
  101. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
  102. package/packages/bundled-features/src/template-resolver/index.ts +12 -1
  103. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
  104. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  105. package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
  106. package/packages/bundled-features/src/template-resolver/seeding.ts +25 -4
  107. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +2 -2
  108. package/packages/bundled-features/src/template-resolver/table.ts +13 -1
  109. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  110. package/packages/bundled-features/src/template-resolver/template-meta.ts +87 -24
  111. package/packages/bundled-features/src/template-resolver/template-preview.ts +159 -56
  112. package/packages/bundled-features/src/template-resolver/template-resources.ts +44 -4
  113. package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
  114. package/packages/bundled-features/src/template-resolver/template-validation.ts +1 -0
  115. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
  116. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +63 -10
  117. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +12 -3
  118. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +349 -134
  119. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +11 -0
  120. package/packages/bundled-features/src/template-resolver/web/i18n.ts +25 -4
  121. package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
  122. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +10 -3
  123. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  124. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  125. package/packages/bundled-features/src/user/changes.json +6 -0
  126. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  127. package/packages/bundled-features/src/user/seeding.ts +19 -5
  128. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  129. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  130. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  131. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  132. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  133. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  134. package/samples/apps/ui-walkthrough/package.json +9 -0
  135. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  136. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
  137. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
  138. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
  139. package/samples/apps/ui-walkthrough/src/run-config.ts +4 -0
  140. package/samples/apps/use-all-bundled/feature-manifest.json +5 -3
@@ -17,6 +17,7 @@ export const defaultTranslations: TranslationsByLocale = {
17
17
  "template-resolver.documentEditor.divider": "Divider",
18
18
  "template-resolver.documentEditor.heading3": "Heading 3",
19
19
  "template-resolver.documentEditor.image": "Image",
20
+ "template-resolver.documentEditor.imageBlock": "Image · inline",
20
21
  "template-resolver.documentEditor.imageAlt": "Alt text (optional)",
21
22
  "template-resolver.documentEditor.insert": "Insert",
22
23
  "template-resolver.documentEditor.insertPlaceholder": "Placeholder",
@@ -36,7 +37,6 @@ export const defaultTranslations: TranslationsByLocale = {
36
37
  "template-resolver.documentEditor.alignJustify": "Justified",
37
38
  "template-resolver.documentEditor.repeatBarList": "Repeat · {list}",
38
39
  "template-resolver.documentEditor.repeatHeader": "Repeat header row",
39
- "template-resolver.documentEditor.keepRowsTogether": "Keep rows together",
40
40
  "template-resolver.documentEditor.paragraph": "Paragraph",
41
41
  "template-resolver.documentEditor.paragraphStyle": "Paragraph style",
42
42
  "template-resolver.documentEditor.placeholder": "Write your text…",
@@ -82,11 +82,31 @@ export const defaultTranslations: TranslationsByLocale = {
82
82
  "templateResolver.errors.resource.unsupportedImage":
83
83
  "This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
84
84
  "templateResolver.errors.template.systemResourcesNeedSystemTenant":
85
- "Resource files can only be imported as system templates by a user of the system tenant.",
85
+ "Resource files of system templates can only be imported or exported by a user of the system tenant.",
86
+ "templateResolver.errors.template.bundleLocalesDiffer":
87
+ "The locales of this template differ in content format or variables, so they cannot be exported as one bundle.",
86
88
  "templateResolver.errors.collection.kindNotAllowed":
87
89
  "“{detail}” templates are written through their own editor, not through a collection.",
88
90
  "templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
89
- "templateResolver.errors.template.invalidMeta": "Invalid template settings: {detail}",
91
+ "templateResolver.errors.noDraft": "There is no draft to discard.",
92
+ "templateResolver.errors.template.invalidMeta": "The template settings are invalid.",
93
+ "templateResolver.errors.template.meta.subject.required": "The subject is missing.",
94
+ "templateResolver.errors.template.meta.subject.invalid":
95
+ "The subject is invalid (at most 200 characters, no line breaks).",
96
+ "templateResolver.errors.template.meta.previewText.required": "The preview text is missing.",
97
+ "templateResolver.errors.template.meta.previewText.invalid":
98
+ "The preview text is invalid (at most 200 characters, no line breaks).",
99
+ "templateResolver.errors.template.meta.senderName.required": "The sender name is missing.",
100
+ "templateResolver.errors.template.meta.senderName.invalid":
101
+ "The sender name is invalid (at most 80 characters, no line breaks).",
102
+ "templateResolver.errors.template.meta.replyTo.required": "The reply-to address is missing.",
103
+ "templateResolver.errors.template.meta.replyTo.invalid":
104
+ "The reply-to address is not a valid email address.",
105
+ "templateResolver.errors.template.meta.layoutSlug.required": "The layout is missing.",
106
+ "templateResolver.errors.template.meta.layoutSlug.invalid": "The chosen layout is invalid.",
107
+ "templateResolver.errors.template.meta.fileName.required": "The file name is missing.",
108
+ "templateResolver.errors.template.meta.fileName.invalid":
109
+ "The file name is invalid: no slashes, quotes, semicolons or two dots in a row.",
90
110
  "templateResolver.errors.template.unknownPlaceholders": "Unknown placeholders: {detail}",
91
111
  "templateResolver.errors.template.missingRequiredPlaceholders":
92
112
  "Required placeholders are not used: {detail}",
@@ -112,7 +132,8 @@ export const defaultTranslations: TranslationsByLocale = {
112
132
  "There is no PDF template “{slug}” for this tenant or the system.",
113
133
  "template-resolver.documentEditor.htmlSource": "HTML source",
114
134
  "template-resolver.documentEditor.removeBlock": "Remove block",
115
- "template-resolver.documentEditor.selectedBlock": "Selected block",
135
+ "template-resolver.documentEditor.list": "List",
136
+ "template-resolver.documentEditor.more": "More",
116
137
  "template-resolver.documentEditor.showHtml": "HTML",
117
138
  "template-resolver.documentEditor.underline": "Underline",
118
139
  "template-resolver.editor.saveFailed": "Save failed.",
@@ -5,6 +5,13 @@
5
5
  // `@cosmicdrift/kumiko-bundled-features/template-resolver` and has no React or
6
6
  // DOM deps.
7
7
 
8
+ export {
9
+ PDF_DEFAULT_MARGIN_MM,
10
+ PDF_FOOTER_MARGIN_MM,
11
+ PDF_HEADER_MARGIN_MM,
12
+ PDF_ORIENTATIONS,
13
+ PDF_PAGE_FORMATS,
14
+ } from "../pdf-constants.js";
8
15
  export {
9
16
  TemplateResolverErrors,
10
17
  TemplateResolverHandlers,
@@ -26,6 +26,8 @@ export type PlaceholderLineEditorProps = {
26
26
  /** Placeholders shown as system chips (a different tint). */
27
27
  readonly systemVariables?: readonly string[];
28
28
  readonly readOnly?: boolean;
29
+ /** Drops the input frame so the line sits flat inside a label/value row. */
30
+ readonly flat?: boolean;
29
31
  readonly onEditorReady?: (handle: DocumentEditorHandle) => void;
30
32
  readonly "aria-label": string;
31
33
  };
@@ -50,6 +52,7 @@ function LineEditorBody({
50
52
  variableLabels,
51
53
  systemVariables,
52
54
  readOnly,
55
+ flat,
53
56
  onEditorReady,
54
57
  ariaLabel,
55
58
  }: {
@@ -59,6 +62,7 @@ function LineEditorBody({
59
62
  readonly variableLabels: Readonly<Record<string, string>>;
60
63
  readonly systemVariables: readonly string[];
61
64
  readonly readOnly: boolean;
65
+ readonly flat: boolean;
62
66
  readonly onEditorReady: ((handle: DocumentEditorHandle) => void) | undefined;
63
67
  readonly ariaLabel: string;
64
68
  }): ReactNode {
@@ -88,12 +92,12 @@ function LineEditorBody({
88
92
  undoRedo: false,
89
93
  }),
90
94
  DocumentVariable.extend({
91
- addNodeView: () => createVariableView(variableLabels, systemVariables),
95
+ addNodeView: () => createVariableView(variableLabels, systemVariables, flat),
92
96
  }),
93
97
  UndoRedo,
94
98
  BlockLineBreaks,
95
99
  ],
96
- [variableLabels, systemVariables],
100
+ [variableLabels, systemVariables, flat],
97
101
  );
98
102
 
99
103
  const editor = useEditor({
@@ -118,7 +122,8 @@ function LineEditorBody({
118
122
  <EditorContent
119
123
  editor={editor}
120
124
  className={cn(
121
- "flex min-h-9 items-center rounded-md border border-input bg-transparent px-3 text-base md:text-sm",
125
+ "flex items-center bg-transparent text-base md:text-sm",
126
+ flat ? "min-h-10" : "min-h-9 rounded-md border border-input px-3",
122
127
  "[&_.ProseMirror]:w-full [&_.ProseMirror]:whitespace-nowrap [&_.ProseMirror]:outline-none",
123
128
  "[&_p]:m-0",
124
129
  readOnly && "opacity-60",
@@ -134,6 +139,7 @@ export default function PlaceholderLineEditor({
134
139
  variableLabels = NO_LABELS,
135
140
  systemVariables = NO_NAMES,
136
141
  readOnly = false,
142
+ flat = false,
137
143
  onEditorReady,
138
144
  "aria-label": ariaLabel,
139
145
  }: PlaceholderLineEditorProps): ReactNode {
@@ -162,6 +168,7 @@ export default function PlaceholderLineEditor({
162
168
  variableLabels={stableLabels}
163
169
  systemVariables={stableSystem}
164
170
  readOnly={readOnly}
171
+ flat={flat}
165
172
  onEditorReady={onEditorReady}
166
173
  ariaLabel={ariaLabel}
167
174
  />
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Tier cache: stale single reload cannot overwrite a newer full resync",
6
+ "detail": "A single-tenant reload that started before a full resync and resolved after it could restore an outdated tier. The resync now advances the per-tenant ticket for deleted and adopted tenants too, and assignment changes are published remote-only."
7
+ },
2
8
  {
3
9
  "version": "0.354.0",
4
10
  "type": "fix",
@@ -338,7 +338,8 @@ export function createTierEngineFeature<
338
338
  };
339
339
  const publishAssignmentChange = (tenantId: TenantId): void => {
340
340
  const message: TierAssignmentSyncMessage = { tenantId };
341
- busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message);
341
+ // The writer already updated its own cache; only other pods need to re-read.
342
+ busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message, { remoteOnly: true });
342
343
  };
343
344
 
344
345
  // Trial: zeit-abgeleitet aus tenant.inserted_at (≈ Signup), live am
@@ -533,10 +534,17 @@ export function createTierEngineFeature<
533
534
  const isNewerThanSnapshot = (tenantId: TenantId): boolean =>
534
535
  (latestTicket.get(tenantId) ?? 0) > startedAtTicket;
535
536
  for (const tenantId of [...cache.keys()]) {
536
- if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) cache.delete(tenantId);
537
+ if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) {
538
+ cache.delete(tenantId);
539
+ latestTicket.set(tenantId, startedAtTicket);
540
+ }
537
541
  }
538
542
  for (const [tenantId, features] of loaded) {
539
- if (!isNewerThanSnapshot(tenantId)) cache.set(tenantId, features);
543
+ if (!isNewerThanSnapshot(tenantId)) {
544
+ cache.set(tenantId, features);
545
+ // Makes a single reload that started before this snapshot discard its older result.
546
+ latestTicket.set(tenantId, startedAtTicket);
547
+ }
540
548
  }
541
549
  };
542
550
 
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "resolveUserDisplayNames no longer swallows decrypt errors",
6
+ "detail": "A misconfigured or unavailable KMS made the delivery log silently show addresses or ids instead of names. Decrypt errors now propagate like in the member directory; users that are missing or have no display name are still left out."
7
+ },
2
8
  {
3
9
  "version": "0.361.0",
4
10
  "type": "improvement",
@@ -6,9 +6,10 @@ import { userTable } from "./schema/user.js";
6
6
  // Shares the KMS adapter's small dedicated pool, same as member-directory.
7
7
  const KMS_POOL_CONCURRENCY = 4;
8
8
 
9
- // One batched lookup for a page of user ids. Users that are missing, have no
10
- // display name, or whose name cannot be decrypted (e.g. crypto-shredded) are
11
- // left out, so callers fall back to the raw id instead of failing the page.
9
+ // One batched lookup for a page of user ids. Users that are missing or have no
10
+ // display name are left out, so callers fall back to the raw id. Decrypt errors
11
+ // propagate (a misconfigured or unavailable KMS fails loudly, like member-directory);
12
+ // crypto-shredding is handled by the KMS layer.
12
13
  export async function resolveUserDisplayNames(
13
14
  db: TenantDb,
14
15
  userIds: readonly string[],
@@ -18,18 +19,10 @@ export async function resolveUserDisplayNames(
18
19
  const users = await selectMany(db, userTable, { id: [...userIds] });
19
20
  const named = users.filter((user) => typeof user.displayName === "string");
20
21
  await mapWithConcurrency(named, KMS_POOL_CONCURRENCY, async (user) => {
21
- try {
22
- names.set(
23
- String(user.id),
24
- await decryptStoredPii(
25
- String(user.displayName),
26
- "displayName",
27
- "user:resolve-display-names",
28
- ),
29
- );
30
- } catch {
31
- // Unreadable name: the caller's fallback applies.
32
- }
22
+ names.set(
23
+ String(user.id),
24
+ await decryptStoredPii(String(user.displayName), "displayName", "user:resolve-display-names"),
25
+ );
33
26
  });
34
27
  return names;
35
28
  }
@@ -11,6 +11,7 @@
11
11
  // das Event UND schreibt die Projection-Zeile in einer TX.
12
12
 
13
13
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { computeBlindIndex, configuredBlindIndexKey } from "@cosmicdrift/kumiko-framework/crypto";
14
15
  import {
15
16
  createEventStoreExecutor,
16
17
  createTenantDb,
@@ -139,12 +140,25 @@ export async function rewriteSeededUserEmail(
139
140
  if (typeof id !== "string") {
140
141
  throw new Error("rewriteSeededUserEmail: existing user row has no string id");
141
142
  }
143
+ const version = existingRow["version"];
144
+ if (typeof version !== "number") {
145
+ throw new Error("rewriteSeededUserEmail: existing user row has no numeric version");
146
+ }
147
+ // Every executor.update appends a user:updated event; skip when the stored
148
+ // blind index already matches so repeated boots do not grow the stream.
149
+ // Queried by the bidx column itself: fetchOne does not return it, and an
150
+ // `email` condition would also match a plaintext row with a blank index.
151
+ const blindIndexKey = configuredBlindIndexKey();
152
+ if (blindIndexKey !== undefined) {
153
+ const indexed = await fetchOne(db, userTable, {
154
+ id,
155
+ emailBidx: computeBlindIndex(blindIndexKey, email),
156
+ });
157
+ // skip: the stored blind index already matches, a rewrite would only append a no-op event.
158
+ if (indexed) return;
159
+ }
142
160
  const tdb = createTenantDb(db, by.tenantId, "system");
143
- const result = await userExecutor.update(
144
- { id, version: existingRow["version"] as number, changes: { email } },
145
- by,
146
- tdb,
147
- );
161
+ const result = await userExecutor.update({ id, version, changes: { email } }, by, tdb);
148
162
  // version_conflict: a concurrent write already changed the row — fine for a seed helper.
149
163
  // unique_violation: a duplicate row already holds the blind index; the repair
150
164
  // has to wait for a manual duplicate cleanup.
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Regression tests prove that forget also shreds the previous email in user.updated events",
6
+ "detail": "No runtime change. Integration tests with a configured subject KMS show that after an email change followed by forget, the old and new email in user.updated changes and previous decrypt only to the erased sentinel and the subject key is gone. Without a configured KMS, event payloads stay plaintext by design."
7
+ },
2
8
  {
3
9
  "version": "0.363.0",
4
10
  "type": "breaking",
@@ -16,7 +16,7 @@
16
16
  "type": "breaking",
17
17
  "title": "user-profile now registers its own profile screen (id user-profile:screen:profile); app-side profile screen registrations must be removed (fw#2312).",
18
18
  "detail": "`profile` is now a declarative `projectionDetail` screen bound to `user:query:user:me` instead of a custom React component: change-password and change-email stay `EditExtensionSection` components (re-auth flows a declarative action can't express), account deletion (request/cancel via `user-data-rights`, grace period) is now fully declarative fields + `actions: RowAction[]`, preserving the original confirm dialog and `visible: {field, eq/ne}` toggle 1:1. `userProfileClient()` now registers the two extension-section components via `extensionSectionComponents` instead of exposing a `ProfileScreen` component for apps to place in a `components` map. `ProfileScreen` is a dead export. New exports: `ChangeEmailSection`, `ChangePasswordSection` (the two surviving extension components, importable for tests but not meant to be placed manually). Known UI regression, accepted for this pass (same trade-off as `privacy-center`): the deletion confirm-dialog title is now always the action's `label`; the dynamically composed grace-period banner sentence is gone, replaced by a plain `gracePeriodEnd` date field; there is no cancel-deletion success toast.",
19
- "migration": "The bundled feature now registers the `profile` screen itself (id `user-profile:screen:profile`) — an app's own `profile` custom-screen registration collides on boot with a duplicate short-id error. Delete the app-side registration entirely: `r.screen({ id: \"profile\", type: \"custom\", renderer: { react: { __component: \"UserProfileScreen\" } }, access: { openToAll: true } })` has no replacement to write, just remove the call. If you have an `r.nav({ screen: \"profile\" })` (or any other reference to the app-local `profile` screen id), repoint it at `\"user-profile:screen:profile\"`. Also remove `components: { UserProfileScreen: ProfileScreen }` from your renderer/client setup and the now-unused `ProfileScreen` import — the client no longer exposes that component. Known affected consumers (not modified here, out of this PR's scope): `money-horse` (`src/features/money-horse/feature.ts:286-292`) and `offlot-app` (`src/features/account/feature.ts:24-30`) both currently register their own `profile` custom screen this way."
19
+ "migration": "The bundled feature now registers the `profile` screen itself (id `user-profile:screen:profile`) — an app's own `profile` custom-screen registration collides on boot with a duplicate short-id error. Delete the app-side registration entirely: `r.screen({ id: \"profile\", type: \"custom\", renderer: { react: { __component: \"UserProfileScreen\" } }, access: { openToAll: true } })` has no replacement to write, just remove the call. If you have an `r.nav({ screen: \"profile\" })` (or any other reference to the app-local `profile` screen id), repoint it at `\"user-profile:screen:profile\"`. Also remove `components: { UserProfileScreen: ProfileScreen }` from your renderer/client setup and the now-unused `ProfileScreen` import — the client no longer exposes that component. Known affected consumers (not modified here, out of this PR's scope): `money-horse` and `offlot-app` both currently register their own `profile` custom screen this way."
20
20
  },
21
21
  {
22
22
  "version": "0.123.2",
@@ -38,5 +38,5 @@ export const DEFAULT_SUBMIT_RATE_LIMIT: RateLimitOption = {
38
38
  };
39
39
 
40
40
  export const DEFAULT_SUBMIT_EMAIL_RATE_LIMITS: readonly PayloadRateLimitOption[] = [
41
- { per: { payloadField: "email" }, limit: 3, windowSeconds: 24 * 60 * 60 },
41
+ { per: { payloadField: "email" }, limit: 3, windowSeconds: 24 * 60 * 60, normalize: "email" },
42
42
  ];
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Workflow run-failed and retry logs carry the original cause",
6
+ "detail": "The ops log used to print the generic `workflow step N failed` wrapper. It now prints the underlying error, while the persisted event still carries the error class only."
7
+ },
2
8
  {
3
9
  "version": "0.353.0",
4
10
  "type": "fix",
@@ -22,6 +22,7 @@ import {
22
22
  describeWorkflowStepError,
23
23
  getStep,
24
24
  SYSTEM_TENANT_ID,
25
+ unwrapWorkflowStepError,
25
26
  WORKFLOW_AGGREGATE_TYPE,
26
27
  WORKFLOW_RUN_FAILED_TYPE,
27
28
  WorkflowStepError,
@@ -167,7 +168,7 @@ export function registerEventTrigger(r: FeatureRegistrar, workflow: WorkflowDefi
167
168
  runId,
168
169
  workflowName: workflow.name,
169
170
  stepIndex,
170
- error: String(error),
171
+ error: String(unwrapWorkflowStepError(error)),
171
172
  });
172
173
  const failedPayload: WorkflowRunFailedPayload = {
173
174
  workflowName: workflow.name,
@@ -40,6 +40,7 @@ import {
40
40
  runStepList,
41
41
  type StepInstance,
42
42
  SYSTEM_ROLE,
43
+ unwrapWorkflowStepError,
43
44
  WORKFLOW_AGGREGATE_TYPE,
44
45
  WORKFLOW_RESUMED_TYPE,
45
46
  WORKFLOW_RETRY_SCHEDULED_TYPE,
@@ -392,7 +393,12 @@ async function runResumedPipeline(ctx: HandlerContext, input: ResumedPipelineInp
392
393
  });
393
394
  return { isSuccess: true as const, data: { outcome: "completed" as const } };
394
395
  } catch (error) {
395
- log.warn("workflow run failed", { runId, workflowName, stepIndex, error: String(error) });
396
+ log.warn("workflow run failed", {
397
+ runId,
398
+ workflowName,
399
+ stepIndex,
400
+ error: String(unwrapWorkflowStepError(error)),
401
+ });
396
402
  // The input stepIndex is the suspended step; the failure happened at the step the resumed pass reached.
397
403
  const failedPayload: WorkflowRunFailedPayload = {
398
404
  workflowName,
@@ -15,6 +15,7 @@
15
15
  "@cosmicdrift/kumiko-framework": "workspace:*",
16
16
  "@cosmicdrift/kumiko-headless": "workspace:*",
17
17
  "@cosmicdrift/kumiko-renderer-mail-html": "workspace:*",
18
+ "@cosmicdrift/kumiko-renderer-puppeteer-client": "workspace:*",
18
19
  "@cosmicdrift/kumiko-renderer-web": "workspace:*",
19
20
  "@cosmicdrift/kumiko-server-runtime": "workspace:*",
20
21
  "react": "^19.2.6",
@@ -24,5 +25,13 @@
24
25
  },
25
26
  "devDependencies": {
26
27
  "@cosmicdrift/kumiko-testing": "workspace:*"
28
+ },
29
+ "kumiko": {
30
+ "assets": [
31
+ {
32
+ "name": "logo-nordlicht.png",
33
+ "source": "src/app/assets/logo-nordlicht.png"
34
+ }
35
+ ]
27
36
  }
28
37
  }
@@ -7,18 +7,51 @@
7
7
  // setupTestStack-Default ist ephemeral (fresh kumiko_test_<random> DB),
8
8
  // deshalb braucht's keine DB-Reset-Logik hier.
9
9
 
10
- import { defineAppE2eConfig, PLAYWRIGHT_DEMO_ENV } from "@cosmicdrift/kumiko-testing/e2e";
10
+ import {
11
+ defineAppE2eConfig,
12
+ E2E_TIMEOUT_MS,
13
+ PLAYWRIGHT_DEMO_ENV,
14
+ } from "@cosmicdrift/kumiko-testing/e2e";
15
+ import { chromium } from "@playwright/test";
11
16
  import { E2E_PORTS } from "../../e2e/e2e-ports";
12
17
 
13
18
  const PORT = E2E_PORTS["framework/ui-walkthrough"];
19
+ const PRINT_SERVICE_PORT = E2E_PORTS["framework/ui-walkthrough-puppeteer-service"];
20
+ // Both sides of the print service handshake; any value of 16+ characters will do for a local run.
21
+ const PRINT_SERVICE_TOKEN = "ui-walkthrough-e2e-print-service";
14
22
 
23
+ const appConfig = defineAppE2eConfig({
24
+ port: PORT,
25
+ serverEntry: "src/app/server.ts",
26
+ locale: "en-US",
27
+ env: {
28
+ ...PLAYWRIGHT_DEMO_ENV,
29
+ PUPPETEER_SERVICE_URL: `http://127.0.0.1:${PRINT_SERVICE_PORT}`,
30
+ PUPPETEER_AUTH_TOKEN: PRINT_SERVICE_TOKEN,
31
+ },
32
+ });
33
+
34
+ // The PDF preview of the template editor prints through the real puppeteer-service,
35
+ // on the Chromium that `playwright install chromium` provides.
15
36
  export default {
16
- ...defineAppE2eConfig({
17
- port: PORT,
18
- serverEntry: "src/app/server.ts",
19
- locale: "en-US",
20
- env: PLAYWRIGHT_DEMO_ENV,
21
- }),
37
+ ...appConfig,
38
+ webServer: [
39
+ {
40
+ command: "bun run src/index.ts",
41
+ cwd: "../../../apps/puppeteer-service",
42
+ url: `http://127.0.0.1:${PRINT_SERVICE_PORT}/healthz`,
43
+ env: {
44
+ PORT: String(PRINT_SERVICE_PORT),
45
+ PUPPETEER_AUTH_TOKEN: PRINT_SERVICE_TOKEN,
46
+ PUPPETEER_EXECUTABLE_PATH: chromium.executablePath(),
47
+ },
48
+ reuseExistingServer: false,
49
+ timeout: E2E_TIMEOUT_MS.webServer,
50
+ stdout: "pipe",
51
+ stderr: "pipe",
52
+ },
53
+ ...[appConfig.webServer ?? []].flat(),
54
+ ],
22
55
  // globalSetup spawnt vor allen Tests einen bun-Subprozess der die
23
56
  // Registry auswertet und `e2e/.e2e-data.json` schreibt. Der eigent-
24
57
  // liche generated.spec.ts-Runner liest nur die JSON — framework-
@@ -12,6 +12,7 @@ import {
12
12
  import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
13
13
  import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
14
14
  import { DEFAULT_MAIL_LAYOUT_MJML } from "@cosmicdrift/kumiko-renderer-mail-html";
15
+ import { readBundledAsset } from "@cosmicdrift/kumiko-server-runtime";
15
16
  import {
16
17
  NORDLICHT_TENANT_ID,
17
18
  NORDLICHT_TENANT_NAME,
@@ -23,6 +24,7 @@ import {
23
24
  import {
24
25
  INVOICE_CORRECTED_DRAFT,
25
26
  INVOICE_ISSUED_DRAFT_VERSIONS,
27
+ LOGO_RESOURCE_KEY,
26
28
  PAYMENT_REMINDER_TEMPLATES,
27
29
  type SeedTemplate,
28
30
  SYSTEM_AUTH_MAILS,
@@ -30,12 +32,18 @@ import {
30
32
  SYSTEM_INVOICE_ISSUED,
31
33
  SYSTEM_PDF_LAYOUT,
32
34
  SYSTEM_TENANCY_CERTIFICATE,
35
+ TENANT_INVOICE_OVERRIDES,
33
36
  TENANT_MAIL_LAYOUT_VERSIONS,
34
37
  TENANT_UTILITY_STATEMENT,
35
38
  } from "./template-seed-data";
36
39
 
37
40
  const NORDLICHT = NORDLICHT_TENANT_ID as TenantId;
38
41
 
42
+ // Declared under `kumiko.assets` in this sample's package.json; looked up from the
43
+ // package dir so the seed does not depend on the cwd the dev server was started in.
44
+ const LOGO_ASSET_NAME = "logo-nordlicht.png";
45
+ const SAMPLE_APP_DIR = new URL("../../", import.meta.url).pathname;
46
+
39
47
  const BRANDING = [
40
48
  [BRANDING_QN.title, "Nordlicht Mieterportal"],
41
49
  [BRANDING_QN.accentColor, "#205265"],
@@ -108,6 +116,35 @@ export const seedTemplates: SeedFn = async (stack) => {
108
116
  },
109
117
  user,
110
118
  );
119
+ // Real multipart upload; every resource link and the branding logo consume their own file.
120
+ const uploadLogo = async (user: SessionUser): Promise<string> => {
121
+ const logoPng = await readBundledAsset(LOGO_ASSET_NAME, { cwd: SAMPLE_APP_DIR });
122
+ const form = new FormData();
123
+ form.append(
124
+ "file",
125
+ new File([new Uint8Array(logoPng)], LOGO_RESOURCE_KEY, { type: "image/png" }),
126
+ );
127
+ const response = await stack.app.request("/api/files", {
128
+ method: "POST",
129
+ headers: {
130
+ Authorization: `Bearer ${await stack.jwt.sign(user)}`,
131
+ },
132
+ body: form,
133
+ });
134
+ if (response.status !== 201) throw new Error(`logo upload failed: HTTP ${response.status}`);
135
+ return ((await response.json()) as { id: string }).id;
136
+ };
137
+ const linkInvoicePdf = (templateId: string): Promise<unknown> =>
138
+ stack.http.writeOk(
139
+ TemplateResolverHandlers.resourceLinkPdf,
140
+ {
141
+ templateId,
142
+ key: "rechnung",
143
+ templateSlug: "billing:invoice",
144
+ fileName: "rechnung-{{invoiceNumber}}.pdf",
145
+ },
146
+ tenantAdmin,
147
+ );
111
148
  const upsertSystem = (template: SeedTemplate) =>
112
149
  upsert(TemplateResolverHandlers.upsertSystem, template, platformAdmin);
113
150
  const upsertTenant = (template: SeedTemplate, status: "draft" | "active") =>
@@ -139,16 +176,64 @@ export const seedTemplates: SeedFn = async (stack) => {
139
176
  for (const [key, value] of BRANDING) {
140
177
  await stack.http.writeOk("config:write:set", { key, value }, tenantAdmin);
141
178
  }
179
+ // The built-in "logo" placeholder of the layouts resolves to this file.
180
+ await stack.http.writeOk(
181
+ "config:write:set",
182
+ { key: BRANDING_QN.logoFile, value: await uploadLogo(tenantAdmin) },
183
+ tenantAdmin,
184
+ );
142
185
  // Each save is a new version of the same row; the second one makes the
143
186
  // layout "customised" for the overview and the first leaves history to diff.
144
187
  for (const content of TENANT_MAIL_LAYOUT_VERSIONS) {
145
188
  await upsertTenant(
146
- { slug: "default", kind: "mail-layout", locale: "en", title: "Mail-Layout", content },
189
+ {
190
+ slug: "default",
191
+ kind: "mail-layout",
192
+ locale: "en",
193
+ title: "Mail-Layout",
194
+ content,
195
+ meta: { accentLine: "branding" },
196
+ },
147
197
  "active",
148
198
  );
149
199
  }
150
- for (const draft of INVOICE_ISSUED_DRAFT_VERSIONS) await upsertTenant(draft, "draft");
200
+ let invoiceDraftId: string | null = null;
201
+ for (const draft of INVOICE_ISSUED_DRAFT_VERSIONS) {
202
+ const saved = await upsertTenant(draft, "draft");
203
+ if (invoiceDraftId === null) {
204
+ invoiceDraftId = saved.id;
205
+ await linkInvoicePdf(saved.id);
206
+ }
207
+ }
151
208
  await upsertTenant(INVOICE_CORRECTED_DRAFT, "draft");
152
209
  for (const reminder of PAYMENT_REMINDER_TEMPLATES) await upsertTenant(reminder, "active");
153
210
  await upsertTenant(TENANT_UTILITY_STATEMENT, "active");
211
+
212
+ // Other tenants with their own invoice copy: what the SystemAdmin sees in the overrides drawer.
213
+ for (const override of TENANT_INVOICE_OVERRIDES) {
214
+ const account = await seedAdmin(stack.db, {
215
+ email: override.adminEmail,
216
+ password: TENANT_ADMIN_PASSWORD,
217
+ displayName: override.adminName,
218
+ emailVerified: true,
219
+ memberships: [
220
+ {
221
+ tenantId: override.tenantId as TenantId,
222
+ tenantKey: override.tenantKey,
223
+ tenantName: override.tenantName,
224
+ roles: ["TenantAdmin"],
225
+ },
226
+ ],
227
+ });
228
+ const admin: SessionUser = {
229
+ id: account.id as SessionUser["id"],
230
+ tenantId: override.tenantId as TenantId,
231
+ roles: ["TenantAdmin"],
232
+ };
233
+ for (const version of override.versions) {
234
+ await upsert(TemplateResolverHandlers.upsertTenant, version, admin, {
235
+ status: override.status,
236
+ });
237
+ }
238
+ }
154
239
  };