@cosmicdrift/kumiko-bundled-features 0.342.0 → 0.344.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 (123) hide show
  1. package/dist/audit/constants.d.ts +5 -1
  2. package/dist/audit/constants.js +5 -1
  3. package/dist/audit/escape-hatch-audit-sink.d.ts +1 -0
  4. package/dist/audit/escape-hatch-audit-sink.js +1 -0
  5. package/dist/audit/run-escape-hatch-retention.d.ts +3 -2
  6. package/dist/audit/run-escape-hatch-retention.js +71 -7
  7. package/dist/auth-email-password/email-templates.d.ts +4 -0
  8. package/dist/auth-email-password/email-templates.js +69 -16
  9. package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
  10. package/dist/auth-email-password/handlers/signup-request.write.js +4 -1
  11. package/dist/auth-email-password/handlers/token-request-handler.js +3 -0
  12. package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
  13. package/dist/auth-email-password/magic-link-mail.js +2 -0
  14. package/dist/billing-foundation/checkout-core.d.ts +15 -4
  15. package/dist/billing-foundation/checkout-core.js +18 -6
  16. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +13 -3
  17. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +23 -20
  18. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +6 -1
  19. package/dist/billing-foundation/consumer-protection/consent-gate.js +6 -1
  20. package/dist/billing-foundation/consumer-protection/consent-locale.js +1 -1
  21. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +24 -2
  22. package/dist/billing-foundation/events.d.ts +2 -0
  23. package/dist/billing-foundation/events.js +2 -0
  24. package/dist/billing-foundation/handlers/billing-plans.query.d.ts +1 -1
  25. package/dist/billing-foundation/handlers/billing-plans.query.js +5 -1
  26. package/dist/billing-foundation/handlers/create-checkout-session.write.js +46 -24
  27. package/dist/billing-foundation/index.d.ts +3 -1
  28. package/dist/billing-foundation/index.js +2 -0
  29. package/dist/billing-foundation/types.d.ts +16 -7
  30. package/dist/billing-foundation/types.js +3 -0
  31. package/dist/billing-foundation/validate-options.d.ts +1 -1
  32. package/dist/billing-foundation/validate-options.js +24 -2
  33. package/dist/billing-foundation/web/billing-plans-panel.js +1 -1
  34. package/dist/billing-foundation/web/cancel-contract-dialog.js +2 -2
  35. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +1 -1
  36. package/dist/billing-foundation/web/checkout-consent-dialog.js +4 -4
  37. package/dist/billing-foundation/web/index.d.ts +3 -1
  38. package/dist/billing-foundation/web/index.js +2 -0
  39. package/dist/channel-discord/feature.js +1 -0
  40. package/dist/channel-email/feature.js +1 -0
  41. package/dist/channel-in-app/feature.js +1 -0
  42. package/dist/channel-push/feature.js +1 -0
  43. package/dist/channel-slack/feature.js +1 -0
  44. package/dist/channel-teams/feature.js +1 -0
  45. package/dist/channel-teams/teams-channel.d.ts +2 -1
  46. package/dist/channel-teams/teams-channel.js +12 -0
  47. package/dist/channel-telegram/feature.js +1 -0
  48. package/dist/delivery/chat-webhook-channel.d.ts +2 -1
  49. package/dist/delivery/chat-webhook-channel.js +5 -2
  50. package/dist/delivery/chat-webhook-sender.d.ts +8 -1
  51. package/dist/delivery/chat-webhook-sender.js +45 -4
  52. package/dist/delivery/constants.d.ts +1 -1
  53. package/dist/delivery/constants.js +1 -1
  54. package/dist/delivery/delivery-service.d.ts +2 -1
  55. package/dist/delivery/delivery-service.js +55 -4
  56. package/dist/delivery/events.d.ts +19 -1
  57. package/dist/delivery/events.js +11 -1
  58. package/dist/delivery/feature.js +29 -5
  59. package/dist/delivery/handlers/log.query.d.ts +4 -2
  60. package/dist/delivery/handlers/log.query.js +46 -18
  61. package/dist/delivery/i18n.js +36 -0
  62. package/dist/delivery/index.d.ts +2 -1
  63. package/dist/delivery/index.js +1 -0
  64. package/dist/delivery/jobs.js +24 -10
  65. package/dist/delivery/notify-factory.d.ts +5 -0
  66. package/dist/delivery/notify-factory.js +6 -0
  67. package/dist/delivery/public-names.d.ts +4 -0
  68. package/dist/delivery/public-names.js +4 -0
  69. package/dist/delivery/tables.js +3 -0
  70. package/dist/delivery/testing.d.ts +2 -0
  71. package/dist/delivery/testing.js +2 -1
  72. package/dist/delivery/types.d.ts +5 -3
  73. package/dist/delivery/web/client-plugin.js +9 -1
  74. package/dist/delivery/web/delivery-channel-cell.d.ts +3 -0
  75. package/dist/delivery/web/delivery-channel-cell.js +10 -0
  76. package/dist/delivery/web/delivery-error-cell.d.ts +3 -0
  77. package/dist/delivery/web/delivery-error-cell.js +16 -0
  78. package/dist/delivery/web/delivery-status-cell.d.ts +1 -1
  79. package/dist/delivery/web/delivery-status-cell.js +11 -1
  80. package/dist/delivery/web/delivery-time-cell.d.ts +3 -0
  81. package/dist/delivery/web/delivery-time-cell.js +6 -0
  82. package/dist/delivery/web/delivery-type-cell.d.ts +3 -0
  83. package/dist/delivery/web/delivery-type-cell.js +17 -0
  84. package/dist/legal-pages/constants.d.ts +2 -0
  85. package/dist/legal-pages/feature.js +7 -2
  86. package/dist/renderer-simple/simple-renderer.js +8 -0
  87. package/dist/secrets/constants.d.ts +6 -0
  88. package/dist/secrets/constants.js +8 -0
  89. package/dist/secrets/feature.d.ts +3 -0
  90. package/dist/secrets/feature.js +3 -0
  91. package/dist/secrets/i18n.js +4 -3
  92. package/dist/secrets/index.d.ts +3 -2
  93. package/dist/secrets/index.js +3 -2
  94. package/dist/secrets/write-gate.d.ts +1 -0
  95. package/dist/secrets/write-gate.js +14 -6
  96. package/dist/shared/index.d.ts +1 -0
  97. package/dist/shared/index.js +1 -0
  98. package/dist/shared/redact.d.ts +4 -0
  99. package/dist/shared/redact.js +21 -0
  100. package/dist/step-dispatcher/feature.js +2 -1
  101. package/dist/step-dispatcher/mail-runner.d.ts +0 -1
  102. package/dist/step-dispatcher/mail-runner.js +0 -5
  103. package/dist/tenant/web/member-roles-cell.js +1 -1
  104. package/dist/tenant/web/member-status-cell.js +1 -1
  105. package/dist/tenant-lifecycle/feature.js +8 -2
  106. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +2 -1
  107. package/dist/tenant-lifecycle/run-tenant-destroy.js +16 -7
  108. package/dist/user/index.d.ts +1 -0
  109. package/dist/user/index.js +1 -0
  110. package/dist/user/resolve-display-names.d.ts +2 -0
  111. package/dist/user/resolve-display-names.js +24 -0
  112. package/package.json +9 -9
  113. package/src/audit/changes.json +6 -0
  114. package/src/auth-email-password/changes.json +7 -0
  115. package/src/billing-foundation/changes.json +7 -0
  116. package/src/channel-teams/changes.json +8 -0
  117. package/src/delivery/changes.json +32 -0
  118. package/src/jobs/changes.json +6 -0
  119. package/src/legal-pages/changes.json +6 -0
  120. package/src/renderer-simple/changes.json +6 -0
  121. package/src/secrets/changes.json +6 -0
  122. /package/dist/{tenant → shared}/web/translate-or-raw.d.ts +0 -0
  123. /package/dist/{tenant → shared}/web/translate-or-raw.js +0 -0
@@ -0,0 +1,5 @@
1
+ import type { NotifyFactory } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { DeliveryService } from "./types.js";
3
+ export declare function createDeliveryNotifyFactory(deliveryService: Pick<DeliveryService, "notify">, options?: {
4
+ readonly deliverQueuedInline?: boolean;
5
+ }): NotifyFactory;
@@ -0,0 +1,6 @@
1
+ // The same binding runProdApp wires into the app context: the calling context's
2
+ // job dispatcher is handed to notify() per call, so queued channels go through
3
+ // the delivery jobs. deliverQueuedInline drops the dispatcher and sends inline.
4
+ export function createDeliveryNotifyFactory(deliveryService, options = {}) {
5
+ return (user, tenantId, jobDispatcher) => (notificationType, notifyOptions) => deliveryService.notify(notificationType, notifyOptions, user, tenantId, options.deliverQueuedInline === true ? undefined : jobDispatcher);
6
+ }
@@ -14,6 +14,10 @@ export declare const DeliveryQueries: {
14
14
  };
15
15
  export declare const DELIVERY_LOG_SCREEN_ID: "delivery-log";
16
16
  export declare const DELIVERY_STATUS_CELL_COMPONENT: "DeliveryStatusCell";
17
+ export declare const DELIVERY_TYPE_CELL_COMPONENT: "DeliveryTypeCell";
18
+ export declare const DELIVERY_CHANNEL_CELL_COMPONENT: "DeliveryChannelCell";
19
+ export declare const DELIVERY_TIME_CELL_COMPONENT: "DeliveryTimeCell";
20
+ export declare const DELIVERY_ERROR_CELL_COMPONENT: "DeliveryErrorCell";
17
21
  export declare const DeliveryErrors: {
18
22
  readonly noRecipient: "delivery_no_recipient";
19
23
  readonly channelFailed: "delivery_channel_failed";
@@ -26,6 +26,10 @@ export const DELIVERY_LOG_SCREEN_ID = "delivery-log";
26
26
  // column — see `screen.columns[].renderer.react.__component` in feature.ts
27
27
  // and `deliveryClient()`'s `columnRenderers` map.
28
28
  export const DELIVERY_STATUS_CELL_COMPONENT = "DeliveryStatusCell";
29
+ export const DELIVERY_TYPE_CELL_COMPONENT = "DeliveryTypeCell";
30
+ export const DELIVERY_CHANNEL_CELL_COMPONENT = "DeliveryChannelCell";
31
+ export const DELIVERY_TIME_CELL_COMPONENT = "DeliveryTimeCell";
32
+ export const DELIVERY_ERROR_CELL_COMPONENT = "DeliveryErrorCell";
29
33
  export const DeliveryErrors = {
30
34
  noRecipient: "delivery_no_recipient",
31
35
  channelFailed: "delivery_channel_failed",
@@ -23,6 +23,8 @@ export const deliveryAttemptsTable = pgTable("store_delivery_attempts", {
23
23
  recipientAddress: text("recipient_address"),
24
24
  status: text("status").notNull().$type(),
25
25
  error: text("error"),
26
+ // false = provider accepted but did not confirm; null = confirmed or not applicable.
27
+ confirmed: boolean("confirmed"),
26
28
  // Default covers rows that predate the column; new rows always carry the
27
29
  // notify() priority from the event payload.
28
30
  priority: text("priority").notNull().default("normal").$type(),
@@ -48,6 +50,7 @@ export const deliveryAttemptsTableMeta = defineUnmanagedTable({
48
50
  { name: "recipient_address", pgType: "text", notNull: false },
49
51
  { name: "status", pgType: "text", notNull: true },
50
52
  { name: "error", pgType: "text", notNull: false },
53
+ { name: "confirmed", pgType: "boolean", notNull: false },
51
54
  { name: "priority", pgType: "text", notNull: true, defaultSql: "'normal'" },
52
55
  { name: "created_at", pgType: "timestamptz", notNull: true, defaultSql: "now()" },
53
56
  ],
@@ -3,6 +3,7 @@ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { EscapeHatchAuditSink, Registry } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
5
5
  import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
6
+ import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
6
7
  import type { Redis } from "ioredis";
7
8
  import type { KillSwitchResolver, RateLimitConfig } from "./delivery-service.js";
8
9
  import type { DeliveryService } from "./types.js";
@@ -13,6 +14,7 @@ export type CreateDeliveryTestContextOptions = {
13
14
  readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
14
15
  readonly log?: Logger;
15
16
  readonly jobRunner?: JobRunner;
17
+ readonly secrets?: SecretsContext;
16
18
  };
17
19
  /**
18
20
  * Helper for setupTestStack: creates a DeliveryService + _notifyFactory for the test context.
@@ -1,4 +1,5 @@
1
1
  import { collectChannels, createDeliveryService } from "./delivery-service.js";
2
+ import { createDeliveryNotifyFactory } from "./notify-factory.js";
2
3
  /**
3
4
  * Helper for setupTestStack: creates a DeliveryService + _notifyFactory for the test context.
4
5
  * Abstracts the boilerplate that every Delivery-using test needs.
@@ -21,6 +22,6 @@ export function createDeliveryTestContext(deps, options = {}) {
21
22
  });
22
23
  return {
23
24
  deliveryService, // exposed so tests can inspect/call directly if needed
24
- _notifyFactory: (user, tenantId) => (notificationType, notifyOptions) => deliveryService.notify(notificationType, notifyOptions, user, tenantId), // @cast-boundary engine-bridge
25
+ _notifyFactory: createDeliveryNotifyFactory(deliveryService),
25
26
  };
26
27
  }
@@ -1,6 +1,6 @@
1
1
  import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
- import type { NotifyDeliveryStatus, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type { DeliveryErrorCode, NotifyDeliveryStatus, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, Registry, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
5
5
  import { DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
6
6
  export type ChannelContext = {
@@ -19,7 +19,8 @@ export type ChannelMessage = {
19
19
  };
20
20
  export type ChannelResult = {
21
21
  readonly status: "sent" | "failed" | "skipped";
22
- readonly error?: string;
22
+ readonly error?: DeliveryErrorCode;
23
+ readonly confirmed?: false;
23
24
  readonly address?: string;
24
25
  };
25
26
  export type RenderedMessage = {
@@ -51,8 +52,9 @@ export type DeliveryLogEntry = {
51
52
  readonly recipientId: string | null;
52
53
  readonly recipientAddress: string | null;
53
54
  readonly status: NotifyDeliveryStatus;
54
- readonly error: string | null;
55
+ readonly error: DeliveryErrorCode | null;
55
56
  readonly priority: NotifyPriority;
57
+ readonly confirmed?: false;
56
58
  };
57
59
  export type DeliveryService = {
58
60
  notify(notificationType: string, options: NotifyOptions, user: SessionUser, tenantId: TenantId, jobDispatcher?: NotifyJobDispatcher): Promise<NotifyResult>;
@@ -1,10 +1,18 @@
1
- import { DELIVERY_FEATURE, DELIVERY_STATUS_CELL_COMPONENT } from "../public-names.js";
1
+ import { DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, } from "../public-names.js";
2
+ import { DeliveryChannelCell } from "./delivery-channel-cell.js";
3
+ import { DeliveryErrorCell } from "./delivery-error-cell.js";
2
4
  import { DeliveryStatusCell } from "./delivery-status-cell.js";
5
+ import { DeliveryTimeCell } from "./delivery-time-cell.js";
6
+ import { DeliveryTypeCell } from "./delivery-type-cell.js";
3
7
  export function deliveryClient(options) {
4
8
  return {
5
9
  name: DELIVERY_FEATURE,
6
10
  columnRenderers: {
7
11
  [DELIVERY_STATUS_CELL_COMPONENT]: DeliveryStatusCell,
12
+ [DELIVERY_TYPE_CELL_COMPONENT]: DeliveryTypeCell,
13
+ [DELIVERY_CHANNEL_CELL_COMPONENT]: DeliveryChannelCell,
14
+ [DELIVERY_TIME_CELL_COMPONENT]: DeliveryTimeCell,
15
+ [DELIVERY_ERROR_CELL_COMPONENT]: DeliveryErrorCell,
8
16
  },
9
17
  ...(options?.translations !== undefined && { translations: options.translations }),
10
18
  };
@@ -0,0 +1,3 @@
1
+ import { type ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
2
+ import type { ReactNode } from "react";
3
+ export declare function DeliveryChannelCell({ row }: ColumnRendererProps): ReactNode;
@@ -0,0 +1,10 @@
1
+ // @runtime client
2
+ // Channel column: the label comes from `delivery.channel.<name>`, registered by each
3
+ // channel feature; unknown channels show their raw name.
4
+ import { useTranslation } from "@cosmicdrift/kumiko-renderer";
5
+ import { translateOrRaw } from "../../shared/web/translate-or-raw.js";
6
+ export function DeliveryChannelCell({ row }) {
7
+ const t = useTranslation();
8
+ const channel = typeof row["channel"] === "string" ? row["channel"] : "";
9
+ return translateOrRaw(t, `delivery.channel.${channel}`, channel);
10
+ }
@@ -0,0 +1,3 @@
1
+ import { type ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
2
+ import type { ReactNode } from "react";
3
+ export declare function DeliveryErrorCell({ row }: ColumnRendererProps): ReactNode;
@@ -0,0 +1,16 @@
1
+ // @runtime client
2
+ // Error column. The log only carries fixed codes (`DeliveryErrorCode`); `http_<status>`
3
+ // takes the status as a parameter, anything unknown shows as the raw value.
4
+ import { useTranslation } from "@cosmicdrift/kumiko-renderer";
5
+ import { HTTP_ERROR_CODE_PATTERN } from "@cosmicdrift/kumiko-types/handlers";
6
+ import { translateOrRaw } from "../../shared/web/translate-or-raw.js";
7
+ export function DeliveryErrorCell({ row }) {
8
+ const t = useTranslation();
9
+ const error = row["error"];
10
+ if (typeof error !== "string" || error === "")
11
+ return null;
12
+ const http = HTTP_ERROR_CODE_PATTERN.exec(error);
13
+ if (http)
14
+ return t("delivery.error.http", { status: http[1] });
15
+ return translateOrRaw(t, `delivery.error.${error}`, error);
16
+ }
@@ -1,3 +1,3 @@
1
- import type { ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
1
+ import { type ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
2
2
  import type { ReactNode } from "react";
3
3
  export declare function DeliveryStatusCell({ row }: ColumnRendererProps): ReactNode;
@@ -1,5 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @runtime client
3
+ // Column-renderer for the delivery-log screen's status column, registered
4
+ // via `deliveryClient()`'s `columnRenderers` map (screen.columns[].renderer
5
+ // in feature.ts). The list frame itself is the generic declarative
6
+ // projectionList renderer — only this one cell stays TSX.
7
+ import { useTranslation } from "@cosmicdrift/kumiko-renderer";
2
8
  import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
9
+ import { translateOrRaw } from "../../shared/web/translate-or-raw.js";
3
10
  import { DeliveryStatus } from "../public-names.js";
4
11
  const STATUS_TONE = {
5
12
  [DeliveryStatus.sent]: "ok",
@@ -8,6 +15,9 @@ const STATUS_TONE = {
8
15
  [DeliveryStatus.skipped]: "muted",
9
16
  };
10
17
  export function DeliveryStatusCell({ row }) {
18
+ const t = useTranslation();
11
19
  const status = typeof row["status"] === "string" ? row["status"] : "";
12
- return (_jsx(StatusBadge, { tone: STATUS_TONE[status] ?? "muted", testId: "delivery-status-cell", children: status }));
20
+ const unconfirmed = status === DeliveryStatus.sent && row["confirmed"] === false;
21
+ const labelKey = unconfirmed ? "delivery.status.sentUnconfirmed" : `delivery.status.${status}`;
22
+ return (_jsx(StatusBadge, { tone: unconfirmed ? "muted" : (STATUS_TONE[status] ?? "muted"), testId: "delivery-status-cell", children: translateOrRaw(t, labelKey, status) }));
13
23
  }
@@ -0,0 +1,3 @@
1
+ import { type ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
2
+ import type { ReactNode } from "react";
3
+ export declare function DeliveryTimeCell({ row }: ColumnRendererProps): ReactNode;
@@ -0,0 +1,6 @@
1
+ // @runtime client
2
+ import { formatWhen } from "@cosmicdrift/kumiko-renderer";
3
+ export function DeliveryTimeCell({ row }) {
4
+ const createdAt = row["createdAt"];
5
+ return typeof createdAt === "string" ? formatWhen(createdAt) : "";
6
+ }
@@ -0,0 +1,3 @@
1
+ import { type ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
2
+ import type { ReactNode } from "react";
3
+ export declare function DeliveryTypeCell({ row }: ColumnRendererProps): ReactNode;
@@ -0,0 +1,17 @@
1
+ // @runtime client
2
+ // Notification type column. Types are QNs like `<scope>:notify:<name>`; the
3
+ // translation key is `<scope>.notification.<name>`, registered by the app that
4
+ // owns the notification (bundled features register none). Falls back to the short
5
+ // name, or to the raw value for any other format.
6
+ import { useTranslation } from "@cosmicdrift/kumiko-renderer";
7
+ import { translateOrRaw } from "../../shared/web/translate-or-raw.js";
8
+ const NOTIFICATION_QN_PATTERN = /^([^:]+):notify:(.+)$/;
9
+ export function DeliveryTypeCell({ row }) {
10
+ const t = useTranslation();
11
+ const type = typeof row["type"] === "string" ? row["type"] : "";
12
+ const match = NOTIFICATION_QN_PATTERN.exec(type);
13
+ if (!match)
14
+ return type;
15
+ const [, scope, name] = match;
16
+ return translateOrRaw(t, `${scope}.notification.${name}`, name ?? type);
17
+ }
@@ -4,6 +4,8 @@ export type LegalPageRoute = {
4
4
  readonly slug: string;
5
5
  readonly lang: string;
6
6
  readonly titleFallback: string;
7
+ /** Prepend the page title as `<h1>` to the rendered body. Default false. */
8
+ readonly titleHeading?: boolean;
7
9
  };
8
10
  export type LegalRequiredBlock = {
9
11
  readonly slug: string;
@@ -1,5 +1,6 @@
1
1
  import { computeRevisionEtag, etagMatches } from "@cosmicdrift/kumiko-framework/api";
2
2
  import { defineFeature, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { escapeHtml } from "@cosmicdrift/kumiko-headless";
3
4
  import { cachedSecurePageResponse, redirectToCanonicalPath, slashVariantOf, } from "../page-render/index.js";
4
5
  import { LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES, } from "./constants.js";
5
6
  import { runLegalPagesBootCheck } from "./lib/boot-check.js";
@@ -85,9 +86,13 @@ export function createLegalPagesFeature(opts = {}) {
85
86
  extra,
86
87
  });
87
88
  }
89
+ const title = data.title || route.titleFallback;
90
+ const renderedBody = renderMarkdownToHtml(data.content);
88
91
  const html = wrapLayout({
89
- title: data.title || route.titleFallback,
90
- bodyHtml: renderMarkdownToHtml(data.content),
92
+ title,
93
+ bodyHtml: route.titleHeading
94
+ ? `<h1>${escapeHtml(title)}</h1>${renderedBody}`
95
+ : renderedBody,
91
96
  lang: route.lang,
92
97
  slug: route.slug,
93
98
  });
@@ -1,4 +1,5 @@
1
1
  import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
2
+ import { renderSafeMarkdown } from "../page-render/index.js";
2
3
  const DEFAULT_PRIMARY_COLOR = "#2563eb";
3
4
  const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
4
5
  function isHttpUrl(value) {
@@ -95,6 +96,13 @@ function renderSection(section, primaryColor) {
95
96
  if ("text" in section) {
96
97
  return `<p style="margin:0 0 16px;color:#333;font-size:14px;line-height:1.5">${escapeHtml(section.text)}</p>`;
97
98
  }
99
+ if ("heading" in section) {
100
+ return `<h2 style="margin:24px 0 12px;color:#111;font-size:16px;font-weight:600">${escapeHtml(section.heading)}</h2>`;
101
+ }
102
+ if ("markdown" in section) {
103
+ const markdownHtml = renderSafeMarkdown(section.markdown);
104
+ return `<div style="color:#333;font-size:14px;line-height:1.5">${markdownHtml}</div>`;
105
+ }
98
106
  if ("button" in section) {
99
107
  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>`;
100
108
  }
@@ -1,3 +1,9 @@
1
1
  import type { AccessRule } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare const DEFAULT_SECRETS_ROLES: readonly ["TenantAdmin"];
3
3
  export declare const DEFAULT_SECRETS_ACCESS: AccessRule;
4
+ export declare const SECRETS_ERROR_KEYS: {
5
+ readonly unknownKey: "secrets.errors.unknownKey";
6
+ readonly writeDenied: "secrets.errors.writeDenied";
7
+ readonly invalidValue: "secrets.errors.invalidValue";
8
+ };
9
+ export declare const INVALID_SECRET_VALUE_CODE: "invalid_secret_value";
@@ -7,3 +7,11 @@
7
7
  // design, not a per-tenant RBAC surface.
8
8
  export const DEFAULT_SECRETS_ROLES = ["TenantAdmin"];
9
9
  export const DEFAULT_SECRETS_ACCESS = { roles: DEFAULT_SECRETS_ROLES };
10
+ // Error i18n keys of the secrets write path. Shared by the write gate, the
11
+ // bundled translations and callers that need to recognise a specific failure.
12
+ export const SECRETS_ERROR_KEYS = {
13
+ unknownKey: "secrets.errors.unknownKey",
14
+ writeDenied: "secrets.errors.writeDenied",
15
+ invalidValue: "secrets.errors.invalidValue",
16
+ };
17
+ export const INVALID_SECRET_VALUE_CODE = "invalid_secret_value";
@@ -13,6 +13,9 @@ import * as z from "zod";
13
13
  * `KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION` pins one explicitly.
14
14
  */
15
15
  export declare const SECRETS_FEATURE_NAME = "secrets";
16
+ /** Env keys that carry the KEK keyring. Optional when the app supplies its own
17
+ * `masterKey` provider. */
18
+ export declare const SECRETS_MASTER_KEK_ENV_KEYS: readonly ["KUMIKO_SECRETS_MASTER_KEY_V1"];
16
19
  export declare const secretsEnvSchema: z.ZodObject<{
17
20
  KUMIKO_SECRETS_MASTER_KEY_V1: z.ZodString;
18
21
  KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: z.ZodDefault<z.ZodString>;
@@ -23,6 +23,9 @@ const ROTATE_ESCAPE_HATCH_REASON = "re-encrypts every tenant's secrets with the
23
23
  * `KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION` pins one explicitly.
24
24
  */
25
25
  export const SECRETS_FEATURE_NAME = "secrets";
26
+ /** Env keys that carry the KEK keyring. Optional when the app supplies its own
27
+ * `masterKey` provider. */
28
+ export const SECRETS_MASTER_KEK_ENV_KEYS = ["KUMIKO_SECRETS_MASTER_KEY_V1"];
26
29
  export const secretsEnvSchema = z.object({
27
30
  KUMIKO_SECRETS_MASTER_KEY_V1: z
28
31
  .string()
@@ -1,13 +1,14 @@
1
+ import { SECRETS_ERROR_KEYS } from "./constants.js";
1
2
  export const SECRETS_I18N = {
2
- "secrets.errors.unknownKey": {
3
+ [SECRETS_ERROR_KEYS.unknownKey]: {
3
4
  en: "Unknown secret key.",
4
5
  de: "Unbekannter Geheimnis-Schlüssel.",
5
6
  },
6
- "secrets.errors.writeDenied": {
7
+ [SECRETS_ERROR_KEYS.writeDenied]: {
7
8
  en: "You are not allowed to change this secret.",
8
9
  de: "Dieses Geheimnis darf nicht geändert werden.",
9
10
  },
10
- "secrets.errors.invalidValue": {
11
+ [SECRETS_ERROR_KEYS.invalidValue]: {
11
12
  en: "This value is not valid for this secret.",
12
13
  de: "Dieser Wert ist für dieses Geheimnis nicht gültig.",
13
14
  },
@@ -1,6 +1,7 @@
1
- export { DEFAULT_SECRETS_ACCESS, DEFAULT_SECRETS_ROLES } from "./constants.js";
2
- export { createSecretsContext, createSecretsFeature, requireSecretsContext, SECRETS_FEATURE_NAME, type SecretsContext, type SecretsContextOptions, type SecretsFeatureOptions, type StoredEnvelope, type StoredMetadata, secretsEnvSchema, TENANT_SECRET_READ_EVENT, tenantSecretsTable, } from "./feature.js";
1
+ export { DEFAULT_SECRETS_ACCESS, DEFAULT_SECRETS_ROLES, INVALID_SECRET_VALUE_CODE, SECRETS_ERROR_KEYS, } from "./constants.js";
2
+ export { createSecretsContext, createSecretsFeature, requireSecretsContext, SECRETS_FEATURE_NAME, SECRETS_MASTER_KEK_ENV_KEYS, type SecretsContext, type SecretsContextOptions, type SecretsFeatureOptions, type StoredEnvelope, type StoredMetadata, secretsEnvSchema, TENANT_SECRET_READ_EVENT, tenantSecretsTable, } from "./feature.js";
3
3
  export { createDeleteHandler } from "./handlers/delete.write.js";
4
4
  export { createListHandler } from "./handlers/list.query.js";
5
5
  export { type RotateJobPayload, type RotateJobResult, rotateJob, } from "./handlers/rotate.job.js";
6
6
  export { createSetHandler } from "./handlers/set.write.js";
7
+ export { isInvalidSecretValueError } from "./write-gate.js";
@@ -1,6 +1,7 @@
1
- export { DEFAULT_SECRETS_ACCESS, DEFAULT_SECRETS_ROLES } from "./constants.js";
2
- export { createSecretsContext, createSecretsFeature, requireSecretsContext, SECRETS_FEATURE_NAME, secretsEnvSchema, TENANT_SECRET_READ_EVENT, tenantSecretsTable, } from "./feature.js";
1
+ export { DEFAULT_SECRETS_ACCESS, DEFAULT_SECRETS_ROLES, INVALID_SECRET_VALUE_CODE, SECRETS_ERROR_KEYS, } from "./constants.js";
2
+ export { createSecretsContext, createSecretsFeature, requireSecretsContext, SECRETS_FEATURE_NAME, SECRETS_MASTER_KEK_ENV_KEYS, secretsEnvSchema, TENANT_SECRET_READ_EVENT, tenantSecretsTable, } from "./feature.js";
3
3
  export { createDeleteHandler } from "./handlers/delete.write.js";
4
4
  export { createListHandler } from "./handlers/list.query.js";
5
5
  export { rotateJob, } from "./handlers/rotate.job.js";
6
6
  export { createSetHandler } from "./handlers/set.write.js";
7
+ export { isInvalidSecretValueError } from "./write-gate.js";
@@ -10,5 +10,6 @@ export type SecretKeyWriteCheck = {
10
10
  export declare function checkSecretKeyWrite(registry: Registry, userRoles: readonly string[], key: string): SecretKeyWriteCheck;
11
11
  export type SecretValueSchemaSource = Pick<Registry, "getSecretKey" | "findSecretNamespace">;
12
12
  export declare function invalidSecretValueError(): ValidationError;
13
+ export declare function isInvalidSecretValueError(err: unknown): err is ValidationError;
13
14
  export declare function isSecretValueValid(schemas: SecretValueSchemaSource, key: string, value: string): boolean;
14
15
  export declare function checkSecretValue(registry: SecretValueSchemaSource, key: string, value: string): WriteFailure | undefined;
@@ -1,8 +1,9 @@
1
- import { AccessDeniedError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
1
+ import { AccessDeniedError, isKumikoError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
2
+ import { INVALID_SECRET_VALUE_CODE, SECRETS_ERROR_KEYS } from "./constants.js";
2
3
  function unknownKeyFailure(key) {
3
4
  return {
4
5
  ok: false,
5
- failure: writeFailure(new NotFoundError("secretKey", key, { i18nKey: "secrets.errors.unknownKey" })),
6
+ failure: writeFailure(new NotFoundError("secretKey", key, { i18nKey: SECRETS_ERROR_KEYS.unknownKey })),
6
7
  };
7
8
  }
8
9
  function namespaceAcceptsKey(namespace, key) {
@@ -27,7 +28,7 @@ export function checkSecretKeyWrite(registry, userRoles, key) {
27
28
  ok: false,
28
29
  failure: writeFailure(new AccessDeniedError({
29
30
  message: "secret write access denied",
30
- i18nKey: "secrets.errors.writeDenied",
31
+ i18nKey: SECRETS_ERROR_KEYS.writeDenied,
31
32
  details: { requiredRoles: writeRoles },
32
33
  })),
33
34
  };
@@ -40,11 +41,18 @@ export function invalidSecretValueError() {
40
41
  fields: [
41
42
  {
42
43
  path: "value",
43
- code: "invalid_secret_value",
44
- i18nKey: "secrets.errors.invalidValue",
44
+ code: INVALID_SECRET_VALUE_CODE,
45
+ i18nKey: SECRETS_ERROR_KEYS.invalidValue,
45
46
  },
46
47
  ],
47
- }, { i18nKey: "secrets.errors.invalidValue" });
48
+ }, { i18nKey: SECRETS_ERROR_KEYS.invalidValue });
49
+ }
50
+ // Matches on the i18n key and error code rather than instanceof, which is fragile
51
+ // when the error crosses a package boundary with a duplicated framework copy.
52
+ export function isInvalidSecretValueError(err) {
53
+ return (isKumikoError(err) &&
54
+ err.code === "validation_error" &&
55
+ err.i18nKey === SECRETS_ERROR_KEYS.invalidValue);
48
56
  }
49
57
  export function isSecretValueValid(schemas, key, value) {
50
58
  const valueSchema = schemas.getSecretKey(key)?.valueSchema ?? schemas.findSecretNamespace(key)?.valueSchema;
@@ -12,6 +12,7 @@ export { createLockoutCounter, type LockoutCounterState } from "./lockout-counte
12
12
  export { mapWithConcurrency } from "./map-with-concurrency.js";
13
13
  export { denyUnlessJoinRowParentVisible, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
14
14
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
15
+ export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
15
16
  export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
16
17
  export { runInSubTransaction } from "./run-in-sub-transaction.js";
17
18
  export { sessionLocaleField } from "./session-locale-field.js";
@@ -12,6 +12,7 @@ export { createLockoutCounter } from "./lockout-counter.js";
12
12
  export { mapWithConcurrency } from "./map-with-concurrency.js";
13
13
  export { denyUnlessJoinRowParentVisible, joinRowParentIsVisible, parentRowIsVisible, } from "./parent-visibility.js";
14
14
  export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
15
+ export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
15
16
  export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
16
17
  export { runInSubTransaction } from "./run-in-sub-transaction.js";
17
18
  export { sessionLocaleField } from "./session-locale-field.js";
@@ -0,0 +1,4 @@
1
+ export declare function redactEmailAddresses(text: string): string;
2
+ export declare function redactUrls(text: string): string;
3
+ export declare function redactBotTokens(text: string): string;
4
+ export declare function redactErrorText(text: string): string;
@@ -0,0 +1,21 @@
1
+ // Adapter errors usually quote the rejected recipient ("550 <ops@example.com> rejected"); the
2
+ // address must not outlive the crypto-shredded dispatch payload in logs.
3
+ export function redactEmailAddresses(text) {
4
+ return text.replace(/[^\s<>,;"']+@[^\s<>,;"']+/g, "[redacted-address]");
5
+ }
6
+ // Provider URLs carry secrets in the path (Slack hook, Telegram bot token), the query or
7
+ // the userinfo. The origin stays so the log still says which host failed. Any scheme
8
+ // counts: smtp://user:pass@host carries credentials just like https.
9
+ export function redactUrls(text) {
10
+ return text.replace(/([a-z][a-z0-9+.-]*:\/\/)([^\s/?#<>"']*)([^\s<>"']*)/gi, (_match, scheme, authority, rest) => {
11
+ const host = authority.slice(authority.lastIndexOf("@") + 1);
12
+ return rest.length > 0 && rest !== "/" ? `${scheme}${host}/[redacted]` : `${scheme}${host}`;
13
+ });
14
+ }
15
+ // Telegram-style bot tokens outside a URL ("<digits>:<35 chars>"). Times like 12:30 are too short to match.
16
+ export function redactBotTokens(text) {
17
+ return text.replace(/\b\d{6,}:[A-Za-z0-9_-]{30,}\b/g, "[redacted-token]");
18
+ }
19
+ export function redactErrorText(text) {
20
+ return redactBotTokens(redactEmailAddresses(redactUrls(text)));
21
+ }
@@ -12,7 +12,8 @@ import { defineFeature, STEP_DISPATCH_AGGREGATE_TYPE, STEP_DISPATCH_FAILED_TYPE,
12
12
  import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
13
13
  import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-types/identifiers";
14
14
  import * as z from "zod";
15
- import { mailSpecSchema, performMailDispatch, redactEmailAddresses, } from "./mail-runner.js";
15
+ import { redactEmailAddresses } from "../shared/index.js";
16
+ import { mailSpecSchema, performMailDispatch } from "./mail-runner.js";
16
17
  import { performWebhookDispatch, WEBHOOK_ALLOWED_PRIVATE_HOSTS_ENV_VAR, WEBHOOK_AUTH_SECRET_NAMESPACE_OPTIONS, webhookSpecSchema, } from "./webhook-runner.js";
17
18
  const log = createFallbackLogger("step-dispatcher");
18
19
  export const stepDispatcherEnvSchema = z.object({
@@ -15,4 +15,3 @@ export type MailDispatchResult = {
15
15
  };
16
16
  export declare function setMailRunner(fn: (spec: MailSpec) => Promise<MailDispatchResult>): void;
17
17
  export declare function performMailDispatch(spec: MailSpec): Promise<MailDispatchResult>;
18
- export declare function redactEmailAddresses(text: string): string;
@@ -19,8 +19,3 @@ export function setMailRunner(fn) {
19
19
  export async function performMailDispatch(spec) {
20
20
  return mailRunner(spec);
21
21
  }
22
- // Adapter errors usually quote the rejected recipient ("550 <ops@example.com> rejected"); the
23
- // address must not outlive the crypto-shredded dispatch payload in logs.
24
- export function redactEmailAddresses(text) {
25
- return text.replace(/[^\s<>,;"']+@[^\s<>,;"']+/g, "[redacted-address]");
26
- }
@@ -3,7 +3,7 @@
3
3
  // `readonly string[]` on TeamRow (see team-list.query.ts), and DataTable has
4
4
  // no default array-to-cell formatting, so this joins it into one string.
5
5
  import { useTranslation } from "@cosmicdrift/kumiko-renderer";
6
- import { translateOrRaw } from "./translate-or-raw.js";
6
+ import { translateOrRaw } from "../../shared/web/translate-or-raw.js";
7
7
  export function MemberRolesCell({ row }) {
8
8
  const t = useTranslation();
9
9
  const roles = row["roles"];
@@ -6,7 +6,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  // projectionList renderer — only this one cell stays TSX.
7
7
  import { useTranslation } from "@cosmicdrift/kumiko-renderer";
8
8
  import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
9
- import { translateOrRaw } from "./translate-or-raw.js";
9
+ import { translateOrRaw } from "../../shared/web/translate-or-raw.js";
10
10
  const STATUS_TONE = {
11
11
  active: "ok",
12
12
  pending: "muted",
@@ -4,7 +4,7 @@ import { DESTRUCTION_CANCELLED_EVENT_SHORT, DESTRUCTION_REQUESTED_EVENT_SHORT, T
4
4
  import { destructionCancelledSchema, destructionRequestedSchema, tenantDestructionCompletedSchema, tenantDestructionFailedSchema, tenantDestructionStageAbandonedSchema, tenantDestructionStageFailedSchema, tenantDestructionStageStartedSchema, tenantDestructionStageSucceededSchema, tenantDestructionStartedSchema, } from "./events.js";
5
5
  import { cancelDestructionWrite } from "./handlers/cancel-destruction.write.js";
6
6
  import { requestDestructionWrite } from "./handlers/request-destruction.write.js";
7
- import { resolveTenantLifecycleGate, runTenantDestructionSweep } from "./run-tenant-destroy.js";
7
+ import { hasTenantDestructionWork, resolveTenantLifecycleGate, runTenantDestructionSweep, } from "./run-tenant-destroy.js";
8
8
  const tenantLifecycleStatusPlugin = {
9
9
  async resolveStatus(tenantId, { db }) {
10
10
  const gate = await resolveTenantLifecycleGate(db, tenantId);
@@ -73,10 +73,16 @@ export function createTenantLifecycleFeature() {
73
73
  throw new Error("run-tenant-destruction: ctx.registry required (JobContext incomplete)");
74
74
  }
75
75
  const T = (await import("@cosmicdrift/kumiko-framework/time")).getTemporal();
76
+ const now = T.Now.instant();
77
+ // Without crossTenantReads (undeclared hatch) fall through to the sweep.
78
+ if (ctx.crossTenantReads && !(await hasTenantDestructionWork(ctx.crossTenantReads, now))) {
79
+ // skip: no tenant is due, so the sweep would not touch anything
80
+ return;
81
+ }
76
82
  await runTenantDestructionSweep({
77
83
  db: ctx.db.unsafeRaw(), // @cast-boundary db-operator — jobs never run inside a DbTx
78
84
  registry: ctx.registry,
79
- now: T.Now.instant(),
85
+ now,
80
86
  log: (message) => ctx.log?.warn(message),
81
87
  fileProviderResolver: ctx._fileProviderResolver,
82
88
  escapeHatchAuditSink: ctx._escapeHatchAuditSink,
@@ -1,5 +1,5 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
- import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { type EscapeHatchAuditSink, type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
5
5
  export declare function runNextDestructionStage(args: {
@@ -15,6 +15,7 @@ export declare function runNextDestructionStage(args: {
15
15
  readonly error?: string;
16
16
  readonly halted?: boolean;
17
17
  }>;
18
+ export declare function hasTenantDestructionWork(reads: Pick<TenantDb, "count">, now: Temporal.Instant): Promise<boolean>;
18
19
  /** Cron entry: start destruction for tenants past grace, then advance stages. */
19
20
  export declare function runTenantDestructionSweep(args: {
20
21
  readonly db: DbRunner;