@cosmicdrift/kumiko-samples 0.343.0 → 0.345.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 (44) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +1 -1
  3. package/packages/bundled-features/src/audit/changes.json +6 -0
  4. package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +31 -12
  5. package/packages/bundled-features/src/auth-email-password/changes.json +24 -0
  6. package/packages/bundled-features/src/auth-email-password/constants.ts +1 -0
  7. package/packages/bundled-features/src/auth-email-password/feature.ts +9 -2
  8. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +33 -10
  9. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +32 -15
  10. package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +60 -0
  11. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -1
  12. package/packages/bundled-features/src/auth-email-password/i18n.ts +2 -0
  13. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +20 -7
  14. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -1
  15. package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
  16. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +35 -7
  17. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +36 -22
  18. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +12 -2
  19. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-locale.ts +1 -1
  20. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +34 -1
  21. package/packages/bundled-features/src/billing-foundation/events.ts +2 -0
  22. package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +14 -5
  23. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +61 -36
  24. package/packages/bundled-features/src/billing-foundation/index.ts +11 -0
  25. package/packages/bundled-features/src/billing-foundation/types.ts +25 -7
  26. package/packages/bundled-features/src/billing-foundation/validate-options.ts +36 -3
  27. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +1 -1
  28. package/packages/bundled-features/src/billing-foundation/web/cancel-contract-dialog.tsx +1 -2
  29. package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +16 -6
  30. package/packages/bundled-features/src/billing-foundation/web/index.ts +12 -0
  31. package/packages/bundled-features/src/legal-pages/changes.json +6 -0
  32. package/packages/bundled-features/src/legal-pages/constants.ts +2 -0
  33. package/packages/bundled-features/src/legal-pages/feature.ts +8 -3
  34. package/packages/bundled-features/src/renderer-simple/changes.json +6 -0
  35. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +10 -0
  36. package/packages/bundled-features/src/step-dispatcher/changes.json +6 -0
  37. package/packages/bundled-features/src/step-dispatcher/feature.ts +2 -1
  38. package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +10 -0
  39. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +10 -11
  40. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +2 -2
  41. package/samples/apps/use-all-bundled/feature-manifest.json +2 -1
  42. package/samples/recipes/i18n/README.md +1 -1
  43. package/samples/recipes/rate-limiting/README.md +1 -1
  44. package/samples/recipes/webhook-step/README.md +8 -1
@@ -13,11 +13,23 @@ export {
13
13
  type SubscriptionStatus,
14
14
  SubscriptionStatuses,
15
15
  } from "../constants.js";
16
+ export {
17
+ CONSENT_LOCALES,
18
+ type ConsentLocale,
19
+ resolveConsentLocale,
20
+ } from "../consumer-protection/consent-locale.js";
16
21
  export type {
17
22
  BillingPlanBenefit,
18
23
  BillingPlanCatalog,
19
24
  BillingPlansResult,
20
25
  BillingPlanView,
26
+ LegalLinkSet,
21
27
  } from "../types.js";
22
28
  export { BillingPlansPanel } from "./billing-plans-panel.js";
29
+ export {
30
+ CheckoutConsentDialog,
31
+ type CheckoutConsentDialogProps,
32
+ type CheckoutConsentPayload,
33
+ CONSENT_TEXT_OUTDATED_CODE,
34
+ } from "./checkout-consent-dialog.js";
23
35
  export { type BillingFoundationClientOptions, billingFoundationClient } from "./client-plugin.js";
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.344.0",
4
+ "type": "improvement",
5
+ "title": "Optional title heading on legal pages",
6
+ "detail": "`LegalPageRoute` has an optional `titleHeading` flag. When `true`, the page body starts with an `<h1>` holding the escaped page title. The default (`false`) leaves the body unchanged."
7
+ },
2
8
  {
3
9
  "version": "0.335.0",
4
10
  "type": "fix",
@@ -10,6 +10,8 @@ export type LegalPageRoute = {
10
10
  readonly slug: string;
11
11
  readonly lang: string;
12
12
  readonly titleFallback: string;
13
+ /** Prepend the page title as `<h1>` to the rendered body. Default false. */
14
+ readonly titleHeading?: boolean;
13
15
  };
14
16
 
15
17
  // Shape of one boot-check-required block — the type behind
@@ -4,6 +4,7 @@ import {
4
4
  type FeatureDefinition,
5
5
  SYSTEM_TENANT_ID,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
+ import { escapeHtml } from "@cosmicdrift/kumiko-headless";
7
8
  import {
8
9
  cachedSecurePageResponse,
9
10
  type PublicPageWrapLayout,
@@ -87,7 +88,7 @@ export type LegalPagesOptions = {
87
88
 
88
89
  export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDefinition {
89
90
  const wrapLayout = opts.wrapLayout ?? wrapInLayout;
90
- const routes = opts.routes ?? LEGAL_ROUTES;
91
+ const routes: readonly LegalPageRoute[] = opts.routes ?? LEGAL_ROUTES;
91
92
  const requiredBlocks = opts.requiredBlocks ?? LEGAL_REQUIRED_BLOCKS;
92
93
  validateRoutes(routes);
93
94
  if (opts.routes !== undefined && opts.routes.length > 0 && opts.requiredBlocks === undefined) {
@@ -172,9 +173,13 @@ export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDe
172
173
  });
173
174
  }
174
175
 
176
+ const title = data.title || route.titleFallback;
177
+ const renderedBody = renderMarkdownToHtml(data.content);
175
178
  const html = wrapLayout({
176
- title: data.title || route.titleFallback,
177
- bodyHtml: renderMarkdownToHtml(data.content),
179
+ title,
180
+ bodyHtml: route.titleHeading
181
+ ? `<h1>${escapeHtml(title)}</h1>${renderedBody}`
182
+ : renderedBody,
178
183
  lang: route.lang,
179
184
  slug: route.slug,
180
185
  });
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.344.0",
4
+ "type": "improvement",
5
+ "title": "Heading and Markdown sections in the simple mail renderer",
6
+ "detail": "`createSimpleRenderer` accepts two more section kinds: `{ heading: string }` (rendered as an escaped `<h2>`) and `{ markdown: string }` (rendered with the hardened Markdown renderer; raw HTML is escaped and unsafe link targets are replaced by `#`). Existing section kinds render as before."
7
+ },
2
8
  {
3
9
  "version": "0.341.0",
4
10
  "type": "improvement",
@@ -1,5 +1,6 @@
1
1
  import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
2
  import type { NotificationRenderer } from "../delivery/index.js";
3
+ import { renderSafeMarkdown } from "../page-render/index.js";
3
4
 
4
5
  export type MailBranding = {
5
6
  /** Shown as text when there is no logo, and as the logo alt text. */
@@ -141,6 +142,8 @@ function renderBrandingFooter(
141
142
 
142
143
  type Section =
143
144
  | { readonly text: string }
145
+ | { readonly heading: string }
146
+ | { readonly markdown: string }
144
147
  | { readonly button: { readonly label: string; readonly url: string } };
145
148
 
146
149
  type EmailTemplateData = {
@@ -157,6 +160,13 @@ function renderSection(section: Section, primaryColor: string): string {
157
160
  if ("text" in section) {
158
161
  return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
159
162
  }
163
+ if ("heading" in section) {
164
+ return `<h2 style="margin:24px 0 12px;color:#111;font-size:16px;font-weight:600">${escapeHtml(section.heading)}</h2>`;
165
+ }
166
+ if ("markdown" in section) {
167
+ const markdownHtml = renderSafeMarkdown(section.markdown);
168
+ return `<div style="color:#333;font-size:14px;line-height:1.5">${markdownHtml}</div>`;
169
+ }
160
170
  if ("button" in section) {
161
171
  return `<p style="margin:0 0 16px"><a href="${escapeHtml(section.button.url)}" style="display:inline-block;padding:10px 24px;background:${escapeHtmlAttr(primaryColor)};color:#fff;text-decoration:none;border-radius:4px;font-size:14px">${escapeHtml(section.button.label)}</a></p>`;
162
172
  }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.345.0",
4
+ "type": "improvement",
5
+ "title": "Webhook sends carry an Idempotency-Key header that is stable across redeliveries",
6
+ "detail": "Webhook sends carry an Idempotency-Key header\nEvery `r.step.webhook.send` request now has `Idempotency-Key: <dispatch stream id>`. The value stays the same when the same dispatch request is delivered again, so receivers can deduplicate. An explicit `Idempotency-Key` in `headers` (any casing) takes precedence."
7
+ },
2
8
  {
3
9
  "version": "0.335.0",
4
10
  "type": "fix",
@@ -193,7 +193,7 @@ function buildDispatchSpec(
193
193
  export function createStepDispatcherFeature(): FeatureDefinition {
194
194
  return defineFeature("step-dispatcher", (r) => {
195
195
  r.describe(
196
- "Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`. Requires the `secrets` feature (`createSecretsFeature()`) to be mounted — `webhook.send` auth resolves per-tenant through it, under `step-dispatcher:webhook-auth.<name>`.",
196
+ "Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`. Requires the `secrets` feature (`createSecretsFeature()`) to be mounted — `webhook.send` auth resolves per-tenant through it, under `step-dispatcher:webhook-auth.<name>`. Every `webhook.send` request carries `Idempotency-Key: <dispatch stream id>`, stable across redeliveries of the same dispatch request; an explicit `Idempotency-Key` in `headers` takes precedence.",
197
197
  );
198
198
  r.secretNamespace("webhook-auth", WEBHOOK_AUTH_SECRET_NAMESPACE_OPTIONS);
199
199
  r.uiHints({
@@ -270,6 +270,7 @@ export function createStepDispatcherFeature(): FeatureDefinition {
270
270
  tenantId: event.tenantId,
271
271
  userId: event.metadata.userId || SYSTEM_USER_ID,
272
272
  secrets: ctx.secrets,
273
+ idempotencyKey: event.aggregateId,
273
274
  })
274
275
  : await performMailDispatch(dispatchSpec.spec);
275
276
  if (result.ok) {
@@ -114,6 +114,7 @@ export type WebhookDispatchDeps = {
114
114
  readonly tenantId: TenantId;
115
115
  readonly userId: string;
116
116
  readonly secrets: SecretsContext | undefined;
117
+ readonly idempotencyKey: string;
117
118
  };
118
119
 
119
120
  // Never includes the secret name or value — spec.auth.secret is a
@@ -121,11 +122,20 @@ export type WebhookDispatchDeps = {
121
122
  // dispatch-failed event, so it stays generic.
122
123
  const WEBHOOK_AUTH_SECRET_UNAVAILABLE_ERROR = "webhook auth secret is not available";
123
124
 
125
+ export const WEBHOOK_IDEMPOTENCY_KEY_HEADER = "idempotency-key";
126
+
127
+ function hasIdempotencyKeyHeader(headers: Readonly<Record<string, string>>): boolean {
128
+ return Object.keys(headers).some((name) => name.toLowerCase() === WEBHOOK_IDEMPOTENCY_KEY_HEADER);
129
+ }
130
+
124
131
  async function buildWebhookHeaders(
125
132
  spec: WebhookSpec,
126
133
  deps: WebhookDispatchDeps,
127
134
  ): Promise<{ ok: true; headers: Record<string, string> } | { ok: false; error: string }> {
128
135
  const headers: Record<string, string> = { "content-type": "application/json", ...spec.headers };
136
+ if (!hasIdempotencyKeyHeader(headers)) {
137
+ headers[WEBHOOK_IDEMPOTENCY_KEY_HEADER] = deps.idempotencyKey;
138
+ }
129
139
  if (!spec.auth) return { ok: true, headers };
130
140
  if (!deps.secrets) return { ok: false, error: WEBHOOK_AUTH_SECRET_UNAVAILABLE_ERROR };
131
141
  // A throwing get (corrupt envelope, rotated key, KMS error) must become a
@@ -69,8 +69,8 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
69
69
  baseUrl,
70
70
  catalog: {
71
71
  plans: ["starter", "pro"],
72
- tierLabelKey: (tier) => `gallery.plan.${tier}.label`,
73
- benefits: (tier) => [{ labelKey: `gallery.plan.${tier}.benefit` }],
72
+ tierLabelKey: (tier) => `gallery-screens:plan.${tier}.label`,
73
+ benefits: (tier) => [{ labelKey: `gallery-screens:plan.${tier}.benefit` }],
74
74
  resolveCurrentTier,
75
75
  viewRoles: ["TenantAdmin", "SystemAdmin"],
76
76
  successPath: GALLERY_PLAN_PATH,
@@ -83,9 +83,8 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
83
83
  operatorEmail: GALLERY_OPERATOR_EMAIL,
84
84
  terminationScope: "platform",
85
85
  legalLinks: {
86
- terms: "/legal/terms",
87
- withdrawal: "/legal/withdrawal",
88
- privacy: "/legal/privacy",
86
+ de: { terms: "/legal/terms", withdrawal: "/legal/withdrawal", privacy: "/legal/privacy" },
87
+ en: { terms: "/legal/terms", withdrawal: "/legal/withdrawal", privacy: "/legal/privacy" },
89
88
  },
90
89
  },
91
90
  });
@@ -140,12 +139,12 @@ export const galleryScreensFeature: FeatureDefinition = defineFeature("gallery-s
140
139
  r.translations({
141
140
  keys: {
142
141
  "gallery:nav.billing": { de: "Abrechnung", en: "Billing" },
143
- "gallery.plan.free.label": { de: "Free", en: "Free" },
144
- "gallery.plan.starter.label": { de: "Starter", en: "Starter" },
145
- "gallery.plan.pro.label": { de: "Pro", en: "Pro" },
146
- "gallery.plan.starter.benefit": { de: "Für kleine Teams", en: "For small teams" },
147
- "gallery.plan.pro.benefit": { de: "Für wachsende Teams", en: "For growing teams" },
148
- "gallery.plan.free.benefit": { de: "Zum Ausprobieren", en: "To try things out" },
142
+ "gallery-screens:plan.free.label": { de: "Free", en: "Free" },
143
+ "gallery-screens:plan.starter.label": { de: "Starter", en: "Starter" },
144
+ "gallery-screens:plan.pro.label": { de: "Pro", en: "Pro" },
145
+ "gallery-screens:plan.starter.benefit": { de: "Für kleine Teams", en: "For small teams" },
146
+ "gallery-screens:plan.pro.benefit": { de: "Für wachsende Teams", en: "For growing teams" },
147
+ "gallery-screens:plan.free.benefit": { de: "Zum Ausprobieren", en: "To try things out" },
149
148
  },
150
149
  });
151
150
  return {};
@@ -42,8 +42,8 @@ const features = [
42
42
  ];
43
43
 
44
44
  const TERMS_TEXT: Readonly<Record<string, string>> = {
45
- de: "## Allgemeine Geschäftsbedingungen\n\nDiese Bedingungen gelten für alle Verträge über die Plattform.",
46
- en: "## Terms and conditions\n\nThese terms apply to every contract concluded through the platform.",
45
+ de: "Diese Bedingungen gelten für alle Verträge über die Plattform.",
46
+ en: "These terms apply to every contract concluded through the platform.",
47
47
  };
48
48
 
49
49
  const seedTerms: SeedFn = async (stack) => {
@@ -127,6 +127,7 @@
127
127
  "auth-email-password:write:reset-password",
128
128
  "auth-email-password:write:signup-confirm",
129
129
  "auth-email-password:write:signup-request",
130
+ "auth-email-password:write:switch-tenant-mfa-gate",
130
131
  "auth-email-password:write:system-invite-create",
131
132
  "auth-email-password:write:verify-email"
132
133
  ],
@@ -2301,7 +2302,7 @@
2301
2302
  },
2302
2303
  {
2303
2304
  "name": "step-dispatcher",
2304
- "description": "Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`. Requires the `secrets` feature (`createSecretsFeature()`) to be mounted — `webhook.send` auth resolves per-tenant through it, under `step-dispatcher:webhook-auth.<name>`.",
2305
+ "description": "Internal system feature that drains deferred Tier-2 side-effects (currently `webhook.send` and `mail.send`) after their originating transaction commits. Listens via `r.multiStreamProjection` on the `kumiko:system:step.dispatch-requested` system event, performs the actual HTTP or mail delivery, then appends `kumiko:system:step.dispatched` or `kumiko:system:step.dispatch-failed` back onto the same stream so the outcome is recorded in the event log without a separate status table. Mount this feature explicitly via `createStepDispatcherFeature()` in your app's feature list alongside any features that use `r.step.webhook.send` or `r.step.mail.send`. Requires the `secrets` feature (`createSecretsFeature()`) to be mounted — `webhook.send` auth resolves per-tenant through it, under `step-dispatcher:webhook-auth.<name>`. Every `webhook.send` request carries `Idempotency-Key: <dispatch stream id>`, stable across redeliveries of the same dispatch request; an explicit `Idempotency-Key` in `headers` takes precedence.",
2305
2306
  "toggleableDefault": null,
2306
2307
  "requires": [
2307
2308
  "secrets"
@@ -13,7 +13,7 @@ A translation value can be a CLDR plural-forms object instead of a plain
13
13
  string. `other` is required; the rest (`zero`/`one`/`two`/`few`/`many`) only
14
14
  exist where the locale's grammar needs them:
15
15
 
16
- ```ts
16
+ ```ts illustration
17
17
  "greeting.unread_count": {
18
18
  de: { one: "{count} ungelesene Nachricht", other: "{count} ungelesene Nachrichten" },
19
19
  en: { one: "{count} unread message", other: "{count} unread messages" },
@@ -12,7 +12,7 @@ End-to-end rate limiting: L1 global-IP + L2 auth + L3 handler opt-in.
12
12
  A public write handler that mails an address typed by the caller is also limited per
13
13
  address, so rotating IPs does not help against mail flooding. Declare it next to `rateLimit`:
14
14
 
15
- ```ts
15
+ ```ts illustration
16
16
  r.writeHandler({
17
17
  // ...
18
18
  access: { roles: ["anonymous"] },
@@ -16,7 +16,7 @@ never a process-wide env var. The `secrets` feature must be mounted
16
16
  (`createSecretsFeature()` + a `MasterKeyProvider`) alongside
17
17
  `step-dispatcher`. Set the secret per tenant before dispatching:
18
18
 
19
- ```ts
19
+ ```ts illustration
20
20
  await stack.http.writeOk(
21
21
  "secrets:write:set",
22
22
  { key: "step-dispatcher:webhook-auth.incident-hook", value: "<token>" },
@@ -33,6 +33,13 @@ Never combine `auth.secret` with a caller-controlled `url`: the caller would
33
33
  receive the secret as the `Authorization` header. `incident:open-authenticated`
34
34
  therefore posts to a fixed URL.
35
35
 
36
+ ## Idempotency-Key
37
+
38
+ Every webhook request carries `Idempotency-Key: <dispatch stream id>`. The
39
+ value is the same when the same dispatch request is delivered again, so a
40
+ receiver can deduplicate redelivered calls. An `Idempotency-Key` set explicitly in
41
+ `headers` (any casing) wins.
42
+
36
43
  ## Source
37
44
 
38
45
  Feature entry point: `src/feature.ts`.