@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
@@ -167,14 +167,25 @@ export async function runNextDestructionStage(args) {
167
167
  return { done: false, error: message };
168
168
  }
169
169
  }
170
+ function destroyRequestedDueWhere(now) {
171
+ return {
172
+ status: "destroyRequested",
173
+ gracePeriodEnd: { lte: now.subtract({ milliseconds: TEARDOWN_GATE_SETTLE_MS }) },
174
+ };
175
+ }
176
+ const destroyingWhere = { status: "destroying" };
177
+ export async function hasTenantDestructionWork(reads, now) {
178
+ const [due, destroying] = await Promise.all([
179
+ reads.count(tenantTable, destroyRequestedDueWhere(now)),
180
+ reads.count(tenantTable, destroyingWhere),
181
+ ]);
182
+ return due > 0 || destroying > 0;
183
+ }
170
184
  /** Cron entry: start destruction for tenants past grace, then advance stages. */
171
185
  export async function runTenantDestructionSweep(args) {
172
186
  const T = getTemporal();
173
187
  const now = args.now ?? T.Now.instant();
174
- const due = await selectMany(args.db, tenantTable, {
175
- status: "destroyRequested",
176
- gracePeriodEnd: { lte: now.subtract({ milliseconds: TEARDOWN_GATE_SETTLE_MS }) },
177
- });
188
+ const due = await selectMany(args.db, tenantTable, destroyRequestedDueWhere(now));
178
189
  let triggered = 0;
179
190
  for (const row of due) {
180
191
  const tenantId = row.id;
@@ -195,9 +206,7 @@ export async function runTenantDestructionSweep(args) {
195
206
  triggered++;
196
207
  }
197
208
  let advanced = 0;
198
- const destroying = await selectMany(args.db, tenantTable, {
199
- status: "destroying",
200
- });
209
+ const destroying = await selectMany(args.db, tenantTable, destroyingWhere);
201
210
  for (const row of destroying) {
202
211
  try {
203
212
  const result = await runNextDestructionStage({
@@ -3,5 +3,6 @@ export { USER_FEATURE, UserErrors, UserHandlers, UserQueries } from "./constants
3
3
  export { backfillUserStreamTenants, type UserStreamBackfillResult, } from "./db/queries/stream-tenant-backfill.js";
4
4
  export { createUserFeature } from "./feature.js";
5
5
  export { isPrincipalBlocked, principalStatusPlugin } from "./principal-status.js";
6
+ export { resolveUserDisplayNames } from "./resolve-display-names.js";
6
7
  export type { UserStatus } from "./schema/user.js";
7
8
  export { USER_ANONYMIZED_DISPLAY_NAME, USER_ANONYMIZED_EMAIL_DOMAIN, USER_ANONYMIZED_EMAIL_PREFIX, USER_DELETED_DISPLAY_NAME, USER_DELETED_EMAIL_PREFIX, USER_STATUS, userEntity, userTable, } from "./schema/user.js";
@@ -3,4 +3,5 @@ export { USER_FEATURE, UserErrors, UserHandlers, UserQueries } from "./constants
3
3
  export { backfillUserStreamTenants, } from "./db/queries/stream-tenant-backfill.js";
4
4
  export { createUserFeature } from "./feature.js";
5
5
  export { isPrincipalBlocked, principalStatusPlugin } from "./principal-status.js";
6
+ export { resolveUserDisplayNames } from "./resolve-display-names.js";
6
7
  export { USER_ANONYMIZED_DISPLAY_NAME, USER_ANONYMIZED_EMAIL_DOMAIN, USER_ANONYMIZED_EMAIL_PREFIX, USER_DELETED_DISPLAY_NAME, USER_DELETED_EMAIL_PREFIX, USER_STATUS, userEntity, userTable, } from "./schema/user.js";
@@ -0,0 +1,2 @@
1
+ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ export declare function resolveUserDisplayNames(db: TenantDb, userIds: readonly string[]): Promise<ReadonlyMap<string, string>>;
@@ -0,0 +1,24 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { decryptStoredPii, mapWithConcurrency } from "../shared/index.js";
3
+ import { userTable } from "./schema/user.js";
4
+ // Shares the KMS adapter's small dedicated pool, same as member-directory.
5
+ const KMS_POOL_CONCURRENCY = 4;
6
+ // One batched lookup for a page of user ids. Users that are missing, have no
7
+ // display name, or whose name cannot be decrypted (e.g. crypto-shredded) are
8
+ // left out, so callers fall back to the raw id instead of failing the page.
9
+ export async function resolveUserDisplayNames(db, userIds) {
10
+ const names = new Map();
11
+ if (userIds.length === 0)
12
+ return names;
13
+ const users = await selectMany(db, userTable, { id: [...userIds] });
14
+ const named = users.filter((user) => typeof user.displayName === "string");
15
+ await mapWithConcurrency(named, KMS_POOL_CONCURRENCY, async (user) => {
16
+ try {
17
+ names.set(String(user.id), await decryptStoredPii(String(user.displayName), "displayName", "user:resolve-display-names"));
18
+ }
19
+ catch {
20
+ // Unreadable name: the caller's fallback applies.
21
+ }
22
+ });
23
+ return names;
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.342.0",
3
+ "version": "0.344.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -503,12 +503,12 @@
503
503
  }
504
504
  },
505
505
  "dependencies": {
506
- "@cosmicdrift/kumiko-dispatcher-live": "0.342.0",
507
- "@cosmicdrift/kumiko-framework": "0.342.0",
508
- "@cosmicdrift/kumiko-headless": "0.342.0",
509
- "@cosmicdrift/kumiko-renderer": "0.342.0",
510
- "@cosmicdrift/kumiko-renderer-web": "0.342.0",
511
- "@cosmicdrift/kumiko-types": "0.342.0",
506
+ "@cosmicdrift/kumiko-dispatcher-live": "0.344.0",
507
+ "@cosmicdrift/kumiko-framework": "0.344.0",
508
+ "@cosmicdrift/kumiko-headless": "0.344.0",
509
+ "@cosmicdrift/kumiko-renderer": "0.344.0",
510
+ "@cosmicdrift/kumiko-renderer-web": "0.344.0",
511
+ "@cosmicdrift/kumiko-types": "0.344.0",
512
512
  "@mollie/api-client": "^4.5.0",
513
513
  "@node-rs/argon2": "^2.0.2",
514
514
  "@types/mailparser": "^3.4.6",
@@ -1025,8 +1025,8 @@
1025
1025
  ],
1026
1026
  "devDependencies": {
1027
1027
  "@testing-library/user-event": "^14.6.1",
1028
- "@cosmicdrift/kumiko-locale-de": "0.342.0",
1029
- "@cosmicdrift/kumiko-locale-es": "0.342.0",
1028
+ "@cosmicdrift/kumiko-locale-de": "0.344.0",
1029
+ "@cosmicdrift/kumiko-locale-es": "0.344.0",
1030
1030
  "jsqr": "^1.4.0"
1031
1031
  }
1032
1032
  }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.343.0",
4
+ "type": "improvement",
5
+ "title": "escape-hatch retention follows the tenant compliance profile",
6
+ "detail": "The audit job `escape-hatch-retention` now deletes `escapeHatchUse` events after the `auditLog.retention` of the tenant's compliance profile. A tenant override can only lengthen that period, never go below the base retention of the selected profile. Cross-tenant audits (with `targetTenantId`, e.g. `identity-switch`) additionally follow the profile of the target tenant. The config key `audit:config:escape-hatch-retention-days` is only the fallback when compliance-profiles is not mounted.\n`pruneEvents` gains the optional `aggregateIds` option to restrict the prune to specific aggregates. `@cosmicdrift/kumiko-framework/compliance` exports `subtractRetentionSpec` and the `RetentionSpec` type for calendar-aware cutoffs (months and years)."
7
+ },
2
8
  {
3
9
  "version": "0.313.0",
4
10
  "type": "fix",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.343.0",
4
+ "type": "improvement",
5
+ "title": "Mail expiry shows duration and local time with time zone",
6
+ "detail": "Auth mails state the link validity as a duration and in the user's time zone\nThe five token mail renderers replace the UTC timestamp with `{duration}` (for example \"1 day\") and `{when}` (medium date and short time with zone abbreviation, in the recipient's locale). `RenderTokenContentArgs` gains `issuedAt` (default now) and `timeZone` (default UTC; an invalid zone falls back to UTC); the handlers pass the token issue time and `ctx.tz.user`. German strings use \"Konto\" instead of \"Account\".",
7
+ "migration": "Custom `auth.mail.*.expiry` translations should use the new `{duration}` and `{when}` placeholders; `{when}` now includes the time and zone abbreviation instead of a UTC timestamp."
8
+ },
2
9
  {
3
10
  "version": "0.342.0",
4
11
  "type": "improvement",
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "version": "0.344.0",
4
+ "type": "breaking",
5
+ "title": "Consumer-protection gaps - per-locale legal links, gate order, confirmation mail, exports",
6
+ "detail": "All changes below apply only when `createBillingFoundationFeature` gets `consumerProtection`. Apps without it behave as before.\n`ConsumerProtectionOptions.legalLinks` takes either one `LegalLinkSet` or one set per consent locale (`{ de, en }`, both required). `BillingPlansResult.consumerProtection.legalLinks` is now always `Record<\"de\" | \"en\", LegalLinkSet>`; a single set is returned for both locales. Code that reads `legalLinks.terms` from the billing-plans result must read `legalLinks[locale].terms`. `CheckoutConsentDialog` picks the set for the consent locale.\n`FALLBACK_CONSENT_LOCALE` is now `\"en\"` (was `\"de\"`). A locale other than de/en, or none, resolves to English in `resolveConsentLocale`, the consent gate, the confirmation mail's VAT note and the termination record.\n`create-checkout-session` with consumer protection now checks in this order: provider, billing enabled, redirect origins, price and subscription gates, then the live price (new `UnprocessableError` `price_unavailable` when the provider cannot return it), then the consent, then the provider call. A missing consent no longer hides a disabled provider, an existing subscription or an unknown price. Its payload schema is strict, so unknown fields are rejected. `assertCheckoutAllowed` and `startProviderCheckout` split `openCheckout` in checkout-core.\nNew option `ConsumerProtectionOptions.oneOffItemLabel(ctx, priceId)`: the server resolves the label of a one-off item (for example a credit pack). It is stored on the consent event as `itemLabelKey` / `itemLabelParams` and shown in the confirmation mail as \"Leistung\" / \"Item\". The client cannot set it.\nThe terms text block is read from the system tenant, not from the buyer's tenant, in the consent gate and the confirmation mail. A tenant-owned block with the same slug no longer changes `termsHash` or the mailed text.\nThe confirmation mail shows dates in the recipient's time zone (user zone, else tenant zone, else UTC) with the zone name, renders the terms as Markdown with real headings and shows the translated plan name (`catalog.tierLabelKey`, looked up through the registry i18n, so the key must be qualified with the registering feature; untranslated keys fall back to the raw tier). Its opening sentence starts with a capital letter.\nNew exports: `consentPayloadSchema`, `ConsentPayload`, `OrderItem`, `CONSENT_LOCALES`, `ConsentLocale`, `resolveConsentLocale`, `LegalLinkSet`. The `/web` entry adds `CheckoutConsentDialog`, `CheckoutConsentDialogProps`, `CheckoutConsentPayload`, `CONSENT_TEXT_OUTDATED_CODE`, `resolveConsentLocale`, `ConsentLocale` and `LegalLinkSet`. `CheckoutConsentDialog` `price.count` is optional, and the \"cancel at any time\" note only shows for a recurring price (`price.renewalKey` set).\nThe checkout and cancel dialogs no longer repeat the modal title as a heading, and the cancel-contract button is hidden once a cancellation is scheduled.",
7
+ "migration": "Only apps that pass consumerProtection are affected. BillingPlansResult.consumerProtection.legalLinks is now keyed by locale (legalLinks.de / legalLinks.en). FALLBACK_CONSENT_LOCALE changed from de to en. create-checkout-session rejects unknown payload fields when consumer protection is on."
8
+ },
2
9
  {
3
10
  "version": "0.341.0",
4
11
  "type": "improvement",
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "version": "0.343.0",
4
+ "type": "fix",
5
+ "title": "Teams no longer reports any 2xx as delivered",
6
+ "detail": "`channel-teams` no longer reports every 2xx answer as sent. A 200 with the body `1` (classic Office connector) is a confirmed send. A 202 (Power Automate / Workflows endpoints) means the message was accepted but not confirmed: the attempt is `sent` and `NotifyResult.deliveries[].confirmed` is `false`. Any other 2xx, including an empty 200 as a made-up URL returns, fails with `unexpected_response`.\n`NotifyDelivery.confirmed` and `ChannelResult.confirmed` are new and only ever `false`. `createChatWebhookChannel` takes an optional `classifyResponse`, which `postChatWebhook` calls for 2xx answers with a status and a lazy `readBodyPrefix()` (at most 64 bytes; the body is always cancelled afterwards). `ChatSendResult` success now carries `confirmed`. `confirmed` is returned for inline delivery only; jobs do not carry it yet."
7
+ }
8
+ ]
@@ -1,4 +1,36 @@
1
1
  [
2
+ {
3
+ "version": "0.343.0",
4
+ "type": "improvement",
5
+ "title": "Delivery log marks sends the provider did not confirm",
6
+ "detail": "The delivery log now records when a provider accepted a send without confirming delivery (for example a Teams webhook answering 202). `store_delivery_attempts` gets a nullable boolean column `confirmed`: `false` means accepted but unconfirmed, `null` means confirmed or not applicable. The `deliveryAttempt` event carries `confirmed: false` on both the inline and the job path, `delivery:query:log` returns `confirmed` per row, and the status cell shows \"Sent (unconfirmed)\" for such rows.",
7
+ "migration": "New nullable column store_delivery_attempts.confirmed. Consumers generate the migration with the generator (kumiko-schema generate) and apply it; existing rows stay null."
8
+ },
9
+ {
10
+ "version": "0.343.0",
11
+ "type": "breaking",
12
+ "title": "Delivery errors are fixed codes instead of raw messages",
13
+ "detail": "Delivery stores and returns only fixed error codes, never raw error messages. `delivery_attempts.error`, the `deliveryAttempt` event, `NotifyResult.deliveries[].error` and the job failure now hold one of `DELIVERY_FAILURE_CODES` (`timeout`, `network_error`, `redirect_blocked`, `host_not_allowed`, `missing_credentials`, `invalid_address`, `unexpected_response`, `render_failed`, `send_failed`, `channel_error`), one of `DELIVERY_SKIP_REASONS`, or `http_<status>`. A throwing channel ends as `send_failed` or `render_failed`; a failure around resolve or dispatch ends as `channel_error`. The full error goes to the log with URLs and email addresses redacted. `redactUrls` and `redactErrorText` are new next to `redactEmailAddresses`, which moved out of the step dispatcher.\n`@cosmicdrift/kumiko-framework/engine` exports `DELIVERY_FAILURE_CODES`, `DELIVERY_SKIP_REASONS`, `isDeliveryErrorCode` and the types `DeliveryErrorCode`, `DeliveryFailureCode`, `DeliverySkipReason` and `ChatSendFailureCode`. `NotifyDelivery.error`, `ChannelResult.error` and `DeliveryLogEntry.error` are now `DeliveryErrorCode`. `delivery:query:log` returns `channel_error` for stored rows that still hold free text.",
14
+ "migration": "NotifyDelivery.error is now a DeliveryErrorCode. Code that matches on error text must match the code instead (for example send_failed). Custom channels must return a DeliveryErrorCode in ChannelResult.error. Existing attempt events keep their free text; reading them through delivery:query:log masks it as channel_error."
15
+ },
16
+ {
17
+ "version": "0.343.0",
18
+ "type": "improvement",
19
+ "title": "Delivery log shows time and error and translates its values",
20
+ "detail": "The delivery log screen shows the time and the error of each attempt and translates type, channel, status and error. Error codes map to `delivery.error.<code>` (`http_<status>` to `delivery.error.http` with a `status` parameter), statuses to `delivery.status.<status>`, channels to `delivery.channel.<name>`. Every bundled channel feature registers its own channel label. Notification types are shown through `<scope>.notification.<name>` for a type `<scope>:notify:<name>`; apps register those labels, and the short name is shown when none exists. English, German and Spanish texts are included for the bundled keys.\n`translateOrRaw` moved from the tenant web folder to `shared/web`; it is internal."
21
+ },
22
+ {
23
+ "version": "0.343.0",
24
+ "type": "improvement",
25
+ "title": "Delivery log shows the recipient name",
26
+ "detail": "`delivery:query:log` returns a `recipientLabel` per row and the delivery log screen shows it in the recipient column: the user's display name, else the decrypted address, else the recipient id. Names are resolved with one batched lookup per page, and only when the user feature is mounted. The new `resolveUserDisplayNames(db, userIds)` is exported from the user feature. The recipient column is not sortable."
27
+ },
28
+ {
29
+ "version": "0.343.0",
30
+ "type": "improvement",
31
+ "title": "createDeliveryTestContext matches production notify",
32
+ "detail": "`createDeliveryTestContext` now behaves like the production `ctx.notify`. It takes a `secrets` option for inline-delivered chat channels and passes the calling context's job dispatcher to `notify()` on every call. Queued channels now go through the delivery jobs when the stack has a job consumer (`jobs: { consumerLane: \"worker\" }`); call `stack.drainJobs()` before asserting on the result. With `jobs: {}` and no `consumerLane` the attempts stay queued and `drainJobs()` never finishes. Without `jobs` they still deliver inline.\nNew export `createDeliveryNotifyFactory(deliveryService, { deliverQueuedInline? })` builds this `NotifyFactory` for custom setups."
33
+ },
2
34
  {
3
35
  "version": "0.341.0",
4
36
  "type": "breaking",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.343.0",
4
+ "type": "improvement",
5
+ "title": "ctx.crossTenantReads for read-only cross-tenant job reads",
6
+ "detail": "Jobs whose `escapeHatch` grants `unsafeRaw` get `ctx.crossTenantReads` with `selectMany`, `fetchOne` and `count` across all tenants, read-only. Each call is reported as the new escape-hatch kind `cross-tenant-read`; system crons audit it once per process and the metric `kumiko_escape_hatch_uses_total` counts every call. `EscapeHatchKind` and the `escape-hatch-used` audit schema gain `cross-tenant-read`.\n`tenant-lifecycle:job:run-tenant-destruction` checks through `ctx.crossTenantReads` whether a tenant is due and only then calls `ctx.db.unsafeRaw()`, so idle minutes no longer write an `unsafe-raw` audit event."
7
+ },
2
8
  {
3
9
  "version": "0.336.0",
4
10
  "type": "improvement",
@@ -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",
@@ -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,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.343.0",
4
+ "type": "improvement",
5
+ "title": "Recognise the invalid-secret-value error",
6
+ "detail": "`secrets` exports `isInvalidSecretValueError`, which recognises the validation error that `secrets:write:set` and `ctx.secrets.set` raise when a value fails the key's `valueSchema`. It also exports `SECRETS_ERROR_KEYS` (the i18n keys of the secrets write errors) and `INVALID_SECRET_VALUE_CODE` (the field error code), so callers can map the failure to their own message instead of matching strings."
7
+ },
2
8
  {
3
9
  "version": "0.341.0",
4
10
  "type": "improvement",
File without changes