@cosmicdrift/kumiko-samples 0.350.0 → 0.352.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 (119) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +2 -2
  3. package/packages/bundled-features/src/audit/handlers/list.query.ts +1 -1
  4. package/packages/bundled-features/src/auth-email-password/email-templates.ts +1 -1
  5. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -1
  6. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +1 -1
  7. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -1
  8. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +1 -1
  10. package/packages/bundled-features/src/auth-email-password/reset-token.ts +1 -1
  11. package/packages/bundled-features/src/auth-email-password/unlock-token.ts +1 -1
  12. package/packages/bundled-features/src/auth-email-password/verification-token.ts +1 -1
  13. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
  14. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm.write.ts +1 -1
  15. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +1 -1
  16. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +1 -1
  17. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +1 -1
  18. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +1 -1
  19. package/packages/bundled-features/src/billing-foundation/constants.ts +3 -17
  20. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +1 -1
  21. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +1 -1
  22. package/packages/bundled-features/src/billing-foundation/feature.ts +2 -9
  23. package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +3 -11
  24. package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +96 -55
  25. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +247 -0
  26. package/packages/bundled-features/src/cap-counter/changes.json +28 -0
  27. package/packages/bundled-features/src/cap-counter/constants.ts +8 -0
  28. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +1 -9
  29. package/packages/bundled-features/src/cap-counter/feature.ts +7 -0
  30. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +5 -0
  31. package/packages/bundled-features/src/cap-counter/index.ts +9 -0
  32. package/packages/bundled-features/src/cap-counter/payload-field.ts +10 -0
  33. package/packages/bundled-features/src/cap-counter/reservation-chain.ts +51 -0
  34. package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +3 -0
  35. package/packages/bundled-features/src/cap-counter/tables.ts +53 -0
  36. package/packages/bundled-features/src/cap-counter/value-cap-guard.ts +70 -0
  37. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +63 -70
  38. package/packages/bundled-features/src/channel-discord/feature.ts +1 -0
  39. package/packages/bundled-features/src/channel-slack/feature.ts +1 -0
  40. package/packages/bundled-features/src/channel-teams/feature.ts +1 -0
  41. package/packages/bundled-features/src/delivery/attempt-log-retention.ts +40 -0
  42. package/packages/bundled-features/src/delivery/attempt-log.ts +26 -3
  43. package/packages/bundled-features/src/delivery/changes.json +12 -0
  44. package/packages/bundled-features/src/delivery/constants.ts +1 -0
  45. package/packages/bundled-features/src/delivery/events.ts +3 -0
  46. package/packages/bundled-features/src/delivery/feature.ts +50 -2
  47. package/packages/bundled-features/src/delivery/handlers/log.query.ts +1 -1
  48. package/packages/bundled-features/src/delivery/index.ts +6 -0
  49. package/packages/bundled-features/src/delivery/mask-recipient-address.ts +40 -0
  50. package/packages/bundled-features/src/delivery/public-names.ts +5 -0
  51. package/packages/bundled-features/src/delivery/types.ts +12 -2
  52. package/packages/bundled-features/src/derivatives-sharp/changes.json +6 -0
  53. package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +1 -1
  54. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +1 -1
  55. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +1 -1
  56. package/packages/bundled-features/src/form-draft/lookup.ts +1 -1
  57. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
  58. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +1 -1
  59. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +1 -1
  60. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
  61. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +1 -1
  62. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +1 -1
  63. package/packages/bundled-features/src/jobs/job-instant.ts +1 -1
  64. package/packages/bundled-features/src/legal-pages/README.md +3 -3
  65. package/packages/bundled-features/src/legal-pages/changes.json +6 -0
  66. package/packages/bundled-features/src/metrics/compute.ts +1 -1
  67. package/packages/bundled-features/src/metrics/feature.ts +1 -1
  68. package/packages/bundled-features/src/metrics/types.ts +1 -1
  69. package/packages/bundled-features/src/metrics/window.ts +1 -1
  70. package/packages/bundled-features/src/personal-access-tokens/expiry.ts +1 -1
  71. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -1
  72. package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
  73. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +1 -1
  74. package/packages/bundled-features/src/renderer-foundation/types.ts +1 -1
  75. package/packages/bundled-features/src/renderer-simple/changes.json +6 -0
  76. package/packages/bundled-features/src/renderer-simple/feature.ts +4 -6
  77. package/packages/bundled-features/src/renderer-simple/index.ts +1 -0
  78. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +138 -0
  79. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +33 -4
  80. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
  81. package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
  82. package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
  83. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -1
  84. package/packages/bundled-features/src/shared/grace-period.ts +3 -10
  85. package/packages/bundled-features/src/shared/row-bound-grant.ts +1 -1
  86. package/packages/bundled-features/src/shared/signed-token.ts +1 -1
  87. package/packages/bundled-features/src/subscription-mollie/verify-webhook.ts +1 -1
  88. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +1 -1
  89. package/packages/bundled-features/src/template-resolver/api.ts +1 -1
  90. package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -1
  91. package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +1 -1
  92. package/packages/bundled-features/src/template-resolver/table.ts +1 -1
  93. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +1 -1
  94. package/packages/bundled-features/src/tenant/invited-membership.ts +1 -1
  95. package/packages/bundled-features/src/tenant-handover/grant.ts +1 -1
  96. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  97. package/packages/bundled-features/src/tier-engine/index.ts +7 -0
  98. package/packages/bundled-features/src/tier-engine/tier-option-gate.ts +148 -0
  99. package/packages/bundled-features/src/user-data-rights/db/queries/export-jobs.ts +1 -1
  100. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +3 -9
  101. package/packages/bundled-features/src/user-data-rights/deletion-token.ts +1 -1
  102. package/packages/bundled-features/src/user-data-rights/email-templates.ts +1 -1
  103. package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
  104. package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
  105. package/packages/bundled-features/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +36 -6
  106. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +1 -1
  107. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +1 -1
  108. package/samples/apps/marketing-demo/package.json +1 -0
  109. package/samples/apps/marketing-demo/src/app/seed-data.ts +2 -2
  110. package/samples/apps/marketing-demo/src/app/seed.ts +1 -1
  111. package/samples/apps/styleguide/src/app/shell.tsx +1 -1
  112. package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +9 -3
  113. package/samples/apps/ui-walkthrough/src/app/assistant-action.tsx +6 -5
  114. package/samples/apps/ui-walkthrough/src/app/shell.tsx +3 -2
  115. package/samples/apps/ui-walkthrough/src/features/tasks/i18n.ts +8 -0
  116. package/samples/apps/use-all-bundled/package.json +1 -0
  117. package/samples/apps/use-all-bundled/src/app/seed.ts +1 -1
  118. package/samples/recipes/files-post-processing/package.json +1 -1
  119. package/samples/recipes/seed-migration/README.md +13 -1
@@ -51,7 +51,7 @@ import {
51
51
  } from "@cosmicdrift/kumiko-framework/crypto";
52
52
  import type { DbConnection, EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
53
53
  import type { DistributedLock } from "@cosmicdrift/kumiko-framework/pipeline";
54
- import { Temporal } from "temporal-polyfill";
54
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
55
55
  import { InboundMailAccountStatuses, InboundMailFoundationHandlers } from "./constants.js";
56
56
  import { MAIL_ACCOUNT_PII_FIELDS, syncCursorTable } from "./entities.js";
57
57
  import {
@@ -36,8 +36,8 @@ import {
36
36
  import { requireSecretsContext } from "@cosmicdrift/kumiko-bundled-features/secrets";
37
37
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
38
38
  import { instantToLegacyDate } from "@cosmicdrift/kumiko-framework/time";
39
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
39
40
  import type { ImapFlow, MailboxObject } from "imapflow";
40
- import { Temporal } from "temporal-polyfill";
41
41
  import { type ImapCredentialDocument, parseImapCredentialDocument } from "./credential-document.js";
42
42
  import {
43
43
  assertUidValidity,
@@ -17,9 +17,9 @@ import {
17
17
  } from "@cosmicdrift/kumiko-bundled-features/inbound-mail-foundation";
18
18
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
19
19
  import { legacyDateToInstant } from "@cosmicdrift/kumiko-framework/time";
20
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
20
21
  import { ImapFlow } from "imapflow";
21
22
  import { type AddressObject, type ParsedMail, simpleParser } from "mailparser";
22
- import { Temporal } from "temporal-polyfill";
23
23
  import type { ImapCredentialDocument } from "./credential-document.js";
24
24
 
25
25
  export const IMAP_MAILBOX = "INBOX";
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
 
3
3
  // kumiko-framework#1525: the five value-position Temporal.Instant.from
4
4
  // call sites in feature.ts live inside defineApply callbacks with no
@@ -201,10 +201,10 @@ await seedTextBlock(db, {
201
201
  title: "Impressum",
202
202
  content: `## Angaben gemäß § 5 TMG
203
203
 
204
- **Marc Frost**
204
+ **Alex Morgan**
205
205
 
206
- Slevogtstr. 10
207
- 04159 Leipzig
206
+ Musterstraße 1
207
+ 12345 Musterstadt
208
208
 
209
209
  ## Kontakt
210
210
 
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.352.0",
4
+ "type": "fix",
5
+ "title": "The legal-pages README imprint example uses a fictional demo identity",
6
+ "detail": "The legal-pages README seeds its imprint example with a fictional demo identity and address instead of a real person's details."
7
+ },
2
8
  {
3
9
  "version": "0.344.0",
4
10
  "type": "improvement",
@@ -4,8 +4,8 @@ import type {
4
4
  AggregateKey,
5
5
  AggregateRow,
6
6
  } from "@cosmicdrift/kumiko-types/aggregate-types";
7
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
7
8
  import type { TenantDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
8
- import type { Temporal } from "temporal-polyfill";
9
9
  import { MAX_GROUP_ROWS, type MetricRange } from "./constants.js";
10
10
  import type {
11
11
  MetricDefinition,
@@ -6,7 +6,7 @@ import {
6
6
  type QueryHandlerDefinition,
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
8
  import { InternalError, ValidationError } from "@cosmicdrift/kumiko-framework/errors";
9
- import { Temporal } from "temporal-polyfill";
9
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
10
10
  import * as z from "zod";
11
11
  import { runMetric } from "./compute.js";
12
12
  import {
@@ -5,8 +5,8 @@ import type {
5
5
  AggregateTimeBucket,
6
6
  } from "@cosmicdrift/kumiko-types/aggregate-types";
7
7
  import type { SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
8
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
8
9
  import type { WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
9
- import type { Temporal } from "temporal-polyfill";
10
10
  import type { MetricRange, MetricScope } from "./constants.js";
11
11
 
12
12
  /** Maps a payload value (e.g. a folder id) to the values `column` may have (e.g. the folder and its subfolders' loan ids). Runs with the tenant handler context, so reads stay tenant-scoped; the result only narrows. An empty result yields no rows. */
@@ -1,5 +1,5 @@
1
1
  import type { AggregateTimeBucket } from "@cosmicdrift/kumiko-types/aggregate-types";
2
- import { Temporal } from "temporal-polyfill";
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
3
  import type { MetricRange } from "./constants.js";
4
4
  import type { MetricDefinition } from "./types.js";
5
5
 
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
 
3
3
  export function isExpiredAt(expiresAt: { epochMilliseconds: number } | null): boolean {
4
4
  return (
@@ -7,7 +7,7 @@ import {
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
8
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
9
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
10
- import { Temporal } from "temporal-polyfill";
10
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
11
11
  import * as z from "zod";
12
12
  import { encryptForDirectWrite, verifyPassword } from "../../shared/index.js";
13
13
  import { UserQueries } from "../../user/index.js";
@@ -2,7 +2,7 @@ import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
5
- import { Temporal } from "temporal-polyfill";
5
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
6
  import * as z from "zod";
7
7
  import { API_TOKEN_ENTITY_NAME, PatErrors } from "../constants.js";
8
8
  import { PAT_REVOKED_AGGREGATE_TYPE, PAT_REVOKED_EVENT_QN } from "../pat-revoked-event.js";
@@ -3,7 +3,7 @@ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { append } from "@cosmicdrift/kumiko-framework/event-store";
5
5
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
6
- import { Temporal } from "temporal-polyfill";
6
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
7
7
  import {
8
8
  PAT_REVOKED_AGGREGATE_TYPE,
9
9
  PAT_REVOKED_EVENT_QN,
@@ -12,7 +12,7 @@ export type RenderRequest =
12
12
  | { kind: "image-snapshot"; payload: DocumentPayload; options?: ImageOptions };
13
13
 
14
14
  export type RenderResponse =
15
- | { kind: "notification"; html: string }
15
+ | { kind: "notification"; html: string; text?: string }
16
16
  | { kind: "mail-html"; html: string; text: string }
17
17
  | { kind: "document-pdf"; pdfBytes: Uint8Array; pageCount: number; sizeBytes: number }
18
18
  | {
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.352.0",
4
+ "type": "improvement",
5
+ "title": "Badge, stages track and chips for the simple mail renderer",
6
+ "detail": "Use badge, { stages } and { chips } in the template variables; the notification RenderResponse carries the matching plain text in text."
7
+ },
2
8
  {
3
9
  "version": "0.344.0",
4
10
  "type": "improvement",
@@ -31,12 +31,10 @@ export async function adaptToFoundation(
31
31
  );
32
32
  }
33
33
  const variables = await resolveNotificationVariables(req, ctx);
34
- const html = await renderer.render({
35
- template: req.payload.template ?? "",
36
- variables,
37
- locale: req.payload.locale,
38
- });
39
- return { kind: "notification", html };
34
+ const input = { template: req.payload.template ?? "", variables, locale: req.payload.locale };
35
+ const html = await renderer.render(input);
36
+ const text = renderer.renderText ? await renderer.renderText(input) : undefined;
37
+ return text === undefined ? { kind: "notification", html } : { kind: "notification", html, text };
40
38
  }
41
39
 
42
40
  export type RendererSimpleOptions = {
@@ -1,4 +1,5 @@
1
1
  export { createRendererSimpleFeature, type RendererSimpleOptions } from "./feature.js";
2
+ export type { MailBadge, MailChip, MailStage, MailStageState, MailTone } from "./mail-blocks.js";
2
3
  export {
3
4
  createSimpleRenderer,
4
5
  type LocalizedText,
@@ -0,0 +1,138 @@
1
+ import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
+
3
+ export type MailTone = "neutral" | "info" | "success" | "warning" | "danger";
4
+ export type MailStageState = "done" | "current" | "upcoming";
5
+
6
+ export type MailBadge = { readonly label: string; readonly tone?: MailTone };
7
+ export type MailStage = { readonly label: string; readonly state: MailStageState };
8
+ export type MailChip = { readonly label: string; readonly tone?: MailTone };
9
+
10
+ // Literal types (as const) keep the colors compile-time known, so they can go into style
11
+ // attributes without escaping.
12
+ const TONE_COLORS = {
13
+ neutral: { bg: "#f3f4f6", fg: "#374151" },
14
+ info: { bg: "#dbeafe", fg: "#1e40af" },
15
+ success: { bg: "#dcfce7", fg: "#166534" },
16
+ warning: { bg: "#fef3c7", fg: "#92400e" },
17
+ danger: { bg: "#fee2e2", fg: "#991b1b" },
18
+ } as const satisfies Readonly<Record<MailTone, { readonly bg: string; readonly fg: string }>>;
19
+
20
+ type ToneColors = (typeof TONE_COLORS)[MailTone];
21
+ type PillMargin = "0" | "0 6px 6px 0";
22
+
23
+ const STAGE_STATES: readonly MailStageState[] = ["done", "current", "upcoming"];
24
+ const STAGE_DONE_COLOR = "#16a34a";
25
+ const STAGE_UPCOMING_BG = "#e5e7eb";
26
+ const STAGE_UPCOMING_FG = "#9ca3af";
27
+
28
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
29
+ return typeof value === "object" && value !== null;
30
+ }
31
+
32
+ function isMailTone(value: unknown): value is MailTone {
33
+ return typeof value === "string" && Object.hasOwn(TONE_COLORS, value);
34
+ }
35
+
36
+ function toneColors(tone: unknown): ToneColors {
37
+ return TONE_COLORS[isMailTone(tone) ? tone : "neutral"];
38
+ }
39
+
40
+ function isStageState(value: unknown): value is MailStageState {
41
+ return STAGE_STATES.some((state) => state === value);
42
+ }
43
+
44
+ // Template data is runtime data (variables of a stored template): entries that do not match are
45
+ // skipped instead of failing the whole mail.
46
+ function isStage(value: unknown): value is MailStage {
47
+ return isRecord(value) && typeof value["label"] === "string" && isStageState(value["state"]);
48
+ }
49
+
50
+ function isChip(value: unknown): value is MailChip {
51
+ return isRecord(value) && typeof value["label"] === "string";
52
+ }
53
+
54
+ export function parseBadge(value: unknown): MailBadge | undefined {
55
+ return isChip(value) && value.label !== "" ? value : undefined;
56
+ }
57
+
58
+ function validStages(value: unknown): readonly MailStage[] {
59
+ return Array.isArray(value) ? value.filter(isStage) : [];
60
+ }
61
+
62
+ function validChips(value: unknown): readonly MailChip[] {
63
+ return Array.isArray(value) ? value.filter(isChip) : [];
64
+ }
65
+
66
+ function pill(chip: MailChip, margin: PillMargin): string {
67
+ const { bg, fg } = toneColors(chip.tone);
68
+ return `<span style="display:inline-block;margin:${margin};padding:2px 10px;border-radius:999px;background:${bg};color:${fg};font-size:12px;font-weight:600;line-height:18px">${escapeHtml(chip.label)}</span>`;
69
+ }
70
+
71
+ export function renderBadge(badge: MailBadge): string {
72
+ return `<p style="margin:0 0 12px">${pill(badge, "0")}</p>`;
73
+ }
74
+
75
+ export function renderChips(chips: unknown): string {
76
+ const valid = validChips(chips);
77
+ if (valid.length === 0) return "";
78
+ return `<p style="margin:0 0 16px">${valid.map((chip) => pill(chip, "0 6px 6px 0")).join("")}</p>`;
79
+ }
80
+
81
+ function stageMarker(stage: MailStage, position: number, primaryColor: string): string {
82
+ const base =
83
+ "display:inline-block;width:24px;height:24px;line-height:24px;border-radius:12px;text-align:center;font-size:12px;font-weight:700";
84
+ const primary = escapeHtmlAttr(primaryColor);
85
+ if (stage.state === "done") {
86
+ return `<span style="${base};background:${STAGE_DONE_COLOR};color:#fff">&#10003;</span>`;
87
+ }
88
+ if (stage.state === "current") {
89
+ return `<span style="${base};background:#fff;border:2px solid ${primary};line-height:20px;width:20px;height:20px;color:${primary}">${position}</span>`;
90
+ }
91
+ return `<span style="${base};background:${STAGE_UPCOMING_BG};color:${STAGE_UPCOMING_FG}">${position}</span>`;
92
+ }
93
+
94
+ function stageCell(stage: MailStage, position: number, primaryColor: string): string {
95
+ const labelColor = stage.state === "upcoming" ? STAGE_UPCOMING_FG : "#111";
96
+ const weight = stage.state === "current" ? 700 : 400;
97
+ return `<td align="center" valign="top" style="padding:0 4px;font-family:sans-serif">${stageMarker(stage, position, primaryColor)}<div style="margin:4px 0 0;font-size:12px;font-weight:${weight};color:${labelColor}">${escapeHtml(stage.label)}</div></td>`;
98
+ }
99
+
100
+ function stageConnector(previous: MailStage): string {
101
+ const color = previous.state === "done" ? STAGE_DONE_COLOR : STAGE_UPCOMING_BG;
102
+ return `<td valign="top" style="padding:11px 0 0"><div style="width:24px;height:2px;line-height:2px;font-size:2px;background:${color}">&nbsp;</div></td>`;
103
+ }
104
+
105
+ export function renderStages(stages: unknown, primaryColor: string): string {
106
+ const valid = validStages(stages);
107
+ if (valid.length === 0) return "";
108
+ const cellsHtml = valid
109
+ .map((stage, index) => {
110
+ const previous = valid[index - 1];
111
+ const connector = previous ? stageConnector(previous) : "";
112
+ return `${connector}${stageCell(stage, index + 1, primaryColor)}`;
113
+ })
114
+ .join("");
115
+ return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 16px"><tr>${cellsHtml}</tr></table>`;
116
+ }
117
+
118
+ const STAGE_TEXT_MARKER: Readonly<Record<MailStageState, string>> = {
119
+ done: "✓",
120
+ current: "▶",
121
+ upcoming: "○",
122
+ };
123
+
124
+ export function badgeText(badge: MailBadge): string {
125
+ return `[${badge.label}]`;
126
+ }
127
+
128
+ export function stagesText(stages: unknown): string {
129
+ return validStages(stages)
130
+ .map((stage) => `${STAGE_TEXT_MARKER[stage.state]} ${stage.label}`)
131
+ .join(" → ");
132
+ }
133
+
134
+ export function chipsText(chips: unknown): string {
135
+ return validChips(chips)
136
+ .map((chip) => chip.label)
137
+ .join(" · ");
138
+ }
@@ -1,6 +1,18 @@
1
1
  import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
2
  import type { NotificationRenderer } from "../delivery/index.js";
3
3
  import { renderSafeMarkdown } from "../page-render/index.js";
4
+ import {
5
+ badgeText,
6
+ chipsText,
7
+ type MailBadge,
8
+ type MailChip,
9
+ type MailStage,
10
+ parseBadge,
11
+ renderBadge,
12
+ renderChips,
13
+ renderStages,
14
+ stagesText,
15
+ } from "./mail-blocks.js";
4
16
 
5
17
  export type MailBranding = {
6
18
  /** Shown as text when there is no logo, and as the logo alt text. */
@@ -170,10 +182,13 @@ type Section =
170
182
  | { readonly text: string }
171
183
  | { readonly heading: string }
172
184
  | { readonly markdown: string }
173
- | { readonly button: { readonly label: string; readonly url: string } };
185
+ | { readonly button: { readonly label: string; readonly url: string } }
186
+ | { readonly stages: readonly MailStage[] }
187
+ | { readonly chips: readonly MailChip[] };
174
188
 
175
189
  type EmailTemplateData = {
176
190
  // Preferred: structured email data
191
+ readonly badge?: MailBadge;
177
192
  readonly header?: string;
178
193
  readonly sections?: readonly Section[];
179
194
  readonly footer?: string;
@@ -183,6 +198,7 @@ type EmailTemplateData = {
183
198
  };
184
199
 
185
200
  function templateContent(variables: Readonly<Record<string, unknown>>): {
201
+ readonly badge: MailBadge | undefined;
186
202
  readonly header: string | undefined;
187
203
  readonly sections: readonly Section[] | undefined;
188
204
  readonly footer: string | undefined;
@@ -190,6 +206,7 @@ function templateContent(variables: Readonly<Record<string, unknown>>): {
190
206
  const data = variables as EmailTemplateData; // @cast-boundary render-helper
191
207
  // Without structured fields, title + body become the header and a single text section.
192
208
  return {
209
+ badge: parseBadge(data.badge),
193
210
  header: data.header ?? data.title,
194
211
  sections: data.sections ?? (data.body ? [{ text: data.body }] : undefined),
195
212
  footer: data.footer,
@@ -202,6 +219,8 @@ function sectionText(section: Section): string {
202
219
  if ("heading" in section) return section.heading;
203
220
  if ("markdown" in section) return section.markdown;
204
221
  if ("button" in section) return `${section.button.label}: ${section.button.url}`;
222
+ if ("stages" in section) return stagesText(section.stages);
223
+ if ("chips" in section) return chipsText(section.chips);
205
224
  return "";
206
225
  }
207
226
 
@@ -219,6 +238,8 @@ function renderSection(section: Section, primaryColor: string): string {
219
238
  if ("button" in section) {
220
239
  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>`;
221
240
  }
241
+ if ("stages" in section) return renderStages(section.stages, primaryColor);
242
+ if ("chips" in section) return renderChips(section.chips);
222
243
  return "";
223
244
  }
224
245
 
@@ -232,13 +253,15 @@ export function createSimpleRenderer(branding?: MailBranding): NotificationRende
232
253
  name: "simple",
233
254
 
234
255
  async render(input) {
235
- const { header, sections, footer } = templateContent(input.variables);
256
+ const { badge, header, sections, footer } = templateContent(input.variables);
236
257
 
237
258
  const parts: string[] = [];
238
259
  parts.push('<!DOCTYPE html><html><body style="margin:0;padding:0;font-family:sans-serif">');
239
260
  parts.push('<div style="max-width:600px;margin:0 auto;padding:24px">');
240
261
  parts.push(renderBrandingHeader(branding, primaryColor, logoUrl));
241
262
 
263
+ if (badge) parts.push(renderBadge(badge));
264
+
242
265
  if (header) {
243
266
  parts.push(
244
267
  `<h1 style="margin:0 0 24px;color:#111;font-size:20px;font-weight:600">${escapeHtml(header)}</h1>`,
@@ -263,9 +286,15 @@ export function createSimpleRenderer(branding?: MailBranding): NotificationRende
263
286
  },
264
287
 
265
288
  async renderText(input) {
266
- const { header, sections, footer } = templateContent(input.variables);
289
+ const { badge, header, sections, footer } = templateContent(input.variables);
267
290
  const brandingFooter = brandingFooterLines(branding, input.locale).join("\n");
268
- return [header, ...(sections ?? []).map(sectionText), footer, brandingFooter]
291
+ return [
292
+ badge ? badgeText(badge) : undefined,
293
+ header,
294
+ ...(sections ?? []).map(sectionText),
295
+ footer,
296
+ brandingFooter,
297
+ ]
269
298
  .filter((block): block is string => block !== undefined && block !== "")
270
299
  .join("\n\n");
271
300
  },
@@ -4,7 +4,7 @@ import { access, defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumik
4
4
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
5
5
  import { append } from "@cosmicdrift/kumiko-framework/event-store";
6
6
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
7
- import { Temporal } from "temporal-polyfill";
7
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
8
8
  import * as z from "zod";
9
9
  import { userSessionTable } from "../schema/user-session.js";
10
10
  import {
@@ -2,7 +2,7 @@ import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
5
- import { Temporal } from "temporal-polyfill";
5
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
6
  import * as z from "zod";
7
7
  import { SessionErrors } from "../constants.js";
8
8
  import { userSessionTable } from "../schema/user-session.js";
@@ -2,7 +2,7 @@ import { fetchOne, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
5
- import { Temporal } from "temporal-polyfill";
5
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
6
  import * as z from "zod";
7
7
  import { SessionErrors, USER_SESSION_ENTITY_NAME } from "../constants.js";
8
8
  import { userSessionTable } from "../schema/user-session.js";
@@ -15,7 +15,7 @@ import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine
15
15
  import { buildSessionRoles, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
16
16
  import { append } from "@cosmicdrift/kumiko-framework/event-store";
17
17
  import { generateId, parseRoles } from "@cosmicdrift/kumiko-framework/utils";
18
- import { Temporal } from "temporal-polyfill";
18
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
19
19
  import { encryptForDirectWrite } from "../shared/index.js";
20
20
  import { tenantMembershipsTable } from "../tenant/index.js";
21
21
  import { isPrincipalBlocked, type UserStatus, userTable } from "../user/index.js";
@@ -1,21 +1,14 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  /** Shared grace-period check for user-data-rights + tenant-lifecycle cancel flows. */
3
3
 
4
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
4
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
5
5
 
6
6
  /**
7
7
  * True when `gracePeriodEnd` is still in the future.
8
- * Uses the polyfill Instant API so callers don't depend on `globalThis.Temporal`.
8
+ * Uses the module's Temporal so callers don't depend on `globalThis.Temporal`.
9
9
  */
10
10
  export function isWithinGracePeriod(gracePeriodEnd: Temporal.Instant | null): boolean {
11
- // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
12
- // at runtime — row types resolve against ambient Temporal; polyfill Instant
13
- // is a separate nominal type across the two .d.ts sources.
14
11
  return (
15
- gracePeriodEnd != null &&
16
- TemporalPolyfill.Instant.compare(
17
- gracePeriodEnd as unknown as InstanceType<typeof TemporalPolyfill.Instant>,
18
- TemporalPolyfill.Now.instant(),
19
- ) > 0
12
+ gracePeriodEnd != null && Temporal.Instant.compare(gracePeriodEnd, Temporal.Now.instant()) > 0
20
13
  );
21
14
  }
@@ -31,7 +31,7 @@
31
31
  // (single-use-token-store) instead.
32
32
 
33
33
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
34
- import type { Temporal } from "temporal-polyfill";
34
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
35
35
  import { peekTokenSubject, signToken, verifyToken } from "./signed-token.js";
36
36
 
37
37
  export type RowBoundGrantResult =
@@ -13,7 +13,7 @@
13
13
  // signal through a short-circuit.
14
14
 
15
15
  import { createHmac, timingSafeEqual } from "node:crypto";
16
- import { Temporal } from "temporal-polyfill";
16
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
17
17
 
18
18
  export type VerifyResult =
19
19
  | { readonly ok: true; readonly userId: string; readonly expiresAtMs: number }
@@ -23,11 +23,11 @@ import {
23
23
  SubscriptionStatuses,
24
24
  } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
25
25
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
26
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
26
27
  import type {
27
28
  Payment as MolliePayment,
28
29
  Subscription as MollieSubscription,
29
30
  } from "@mollie/api-client";
30
- import { Temporal } from "temporal-polyfill";
31
31
  import { MOLLIE_PROVIDER_NAME } from "./constants.js";
32
32
  import type { MolliePriceConfig } from "./plugin-methods.js";
33
33
 
@@ -47,8 +47,8 @@ import {
47
47
  import { ExtraRouteRejection } from "@cosmicdrift/kumiko-framework/api";
48
48
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
49
49
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
50
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
50
51
  import Stripe from "stripe";
51
- import { Temporal } from "temporal-polyfill";
52
52
  import { STRIPE_PROVIDER_NAME, StripeEventTypes } from "./constants.js";
53
53
  import type { StripeWebhookRuntime } from "./runtime.js";
54
54
 
@@ -6,7 +6,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
6
6
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
7
7
  import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
8
8
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
9
- import type { Temporal } from "temporal-polyfill";
9
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
10
10
  import type { ContentFormat, TemplateKind } from "./constants.js";
11
11
  import { FALLBACK_LOCALE, SYSTEM_TENANT_ID } from "./constants.js";
12
12
  import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
@@ -4,7 +4,7 @@ import {
4
4
  crossTenantOverrideDenied,
5
5
  defineQueryHandler,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
- import type { Temporal } from "temporal-polyfill";
7
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
8
8
  import * as z from "zod";
9
9
  import { TEXT_BLOCK_KIND } from "../constants.js";
10
10
  import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
@@ -7,7 +7,7 @@ import type {
7
7
  TenantId,
8
8
  } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
10
- import type { Temporal } from "temporal-polyfill";
10
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
11
11
  import { templateResourceEntity, templateResourcesTable } from "../table.js";
12
12
  import { userContentEntriesTable, userContentEntryEntity } from "../user-content-table.js";
13
13
 
@@ -5,7 +5,7 @@ import {
5
5
  createSelectField,
6
6
  createTextField,
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
- import type { Temporal } from "temporal-polyfill";
8
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
9
9
  import {
10
10
  CONTENT_FORMATS,
11
11
  TEMPLATE_KINDS,
@@ -6,7 +6,7 @@ import {
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
8
8
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
9
- import { Temporal } from "temporal-polyfill";
9
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
10
10
  import * as z from "zod";
11
11
  import { userSessionTable } from "../../sessions/index.js";
12
12
  import { decryptStoredPii, mapWithConcurrency } from "../../shared/index.js";
@@ -7,7 +7,7 @@ import {
7
7
  import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
8
8
  import { ConflictError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
9
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
10
- import { Temporal } from "temporal-polyfill";
10
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
11
11
  import { TenantErrors } from "./constants.js";
12
12
  import { findForbiddenMembershipRole, reservedMembershipRoleError } from "./membership-roles.js";
13
13
  import { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table.js";
@@ -12,7 +12,7 @@
12
12
  // put in a URL, logged, or emailed. The run's own photos may show plates —
13
13
  // the grant must stay exactly where the anonymous flow minted it.
14
14
 
15
- import type { Temporal } from "temporal-polyfill";
15
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
16
16
  import { signRowBoundGrant } from "../shared/index.js";
17
17
 
18
18
  const HANDOVER_PURPOSE_PREFIX = "tenant-handover";
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.352.0",
4
+ "type": "improvement",
5
+ "title": "createTierOptionGate builds the availability rows and gates select options by tier on write",
6
+ "detail": "withTierOptionGate rejects a disallowed option with the lowest tier that allows it and lets an unchanged stored value pass on update."
7
+ },
2
8
  {
3
9
  "version": "0.278.0",
4
10
  "type": "breaking",