@cosmicdrift/kumiko-bundled-features 0.341.0 → 0.343.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 (117) hide show
  1. package/dist/audit/constants.d.ts +7 -0
  2. package/dist/audit/constants.js +7 -0
  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/feature.js +34 -7
  6. package/dist/audit/run-escape-hatch-retention.d.ts +11 -0
  7. package/dist/audit/run-escape-hatch-retention.js +91 -0
  8. package/dist/auth-email-password/bootstrap.d.ts +8 -2
  9. package/dist/auth-email-password/bootstrap.js +7 -2
  10. package/dist/auth-email-password/email-templates.d.ts +4 -0
  11. package/dist/auth-email-password/email-templates.js +69 -16
  12. package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
  13. package/dist/auth-email-password/handlers/signup-request.write.js +4 -1
  14. package/dist/auth-email-password/handlers/token-request-handler.js +3 -0
  15. package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
  16. package/dist/auth-email-password/magic-link-mail.js +2 -0
  17. package/dist/auth-email-password/web/auth-form-primitives.d.ts +7 -1
  18. package/dist/auth-email-password/web/auth-form-primitives.js +5 -5
  19. package/dist/auth-email-password/web/confirm-account-unlock-screen.js +3 -3
  20. package/dist/auth-email-password/web/forgot-password-screen.js +2 -2
  21. package/dist/auth-email-password/web/invite-accept-screen.js +9 -9
  22. package/dist/auth-email-password/web/login-screen.js +8 -8
  23. package/dist/auth-email-password/web/request-account-unlock-screen.js +2 -2
  24. package/dist/auth-email-password/web/reset-password-screen.js +3 -3
  25. package/dist/auth-email-password/web/session-bootstrap-error.js +3 -3
  26. package/dist/auth-email-password/web/signup-complete-screen.js +3 -3
  27. package/dist/auth-email-password/web/signup-screen.js +2 -2
  28. package/dist/auth-email-password/web/verify-email-screen.js +3 -3
  29. package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +5 -5
  30. package/dist/auth-mfa/web/mfa-verify-screen.js +3 -3
  31. package/dist/channel-discord/feature.js +1 -0
  32. package/dist/channel-email/email-channel.js +7 -1
  33. package/dist/channel-email/feature.js +1 -0
  34. package/dist/channel-email/pii-guard.js +6 -5
  35. package/dist/channel-email/smtp-transport.js +22 -1
  36. package/dist/channel-email/types.d.ts +1 -0
  37. package/dist/channel-in-app/feature.js +1 -0
  38. package/dist/channel-push/feature.js +1 -0
  39. package/dist/channel-slack/feature.js +1 -0
  40. package/dist/channel-teams/feature.js +1 -0
  41. package/dist/channel-teams/teams-channel.d.ts +2 -1
  42. package/dist/channel-teams/teams-channel.js +12 -0
  43. package/dist/channel-telegram/feature.js +1 -0
  44. package/dist/delivery/chat-webhook-channel.d.ts +2 -1
  45. package/dist/delivery/chat-webhook-channel.js +5 -2
  46. package/dist/delivery/chat-webhook-sender.d.ts +8 -1
  47. package/dist/delivery/chat-webhook-sender.js +45 -4
  48. package/dist/delivery/constants.d.ts +1 -1
  49. package/dist/delivery/constants.js +1 -1
  50. package/dist/delivery/delivery-service.d.ts +2 -1
  51. package/dist/delivery/delivery-service.js +55 -4
  52. package/dist/delivery/events.d.ts +19 -1
  53. package/dist/delivery/events.js +11 -1
  54. package/dist/delivery/feature.js +29 -5
  55. package/dist/delivery/handlers/log.query.d.ts +4 -2
  56. package/dist/delivery/handlers/log.query.js +46 -18
  57. package/dist/delivery/i18n.js +36 -0
  58. package/dist/delivery/index.d.ts +2 -1
  59. package/dist/delivery/index.js +1 -0
  60. package/dist/delivery/jobs.js +24 -10
  61. package/dist/delivery/notify-factory.d.ts +5 -0
  62. package/dist/delivery/notify-factory.js +6 -0
  63. package/dist/delivery/public-names.d.ts +4 -0
  64. package/dist/delivery/public-names.js +4 -0
  65. package/dist/delivery/tables.js +3 -0
  66. package/dist/delivery/testing.d.ts +2 -0
  67. package/dist/delivery/testing.js +2 -1
  68. package/dist/delivery/types.d.ts +5 -3
  69. package/dist/delivery/web/client-plugin.js +9 -1
  70. package/dist/delivery/web/delivery-channel-cell.d.ts +3 -0
  71. package/dist/delivery/web/delivery-channel-cell.js +10 -0
  72. package/dist/delivery/web/delivery-error-cell.d.ts +3 -0
  73. package/dist/delivery/web/delivery-error-cell.js +16 -0
  74. package/dist/delivery/web/delivery-status-cell.d.ts +1 -1
  75. package/dist/delivery/web/delivery-status-cell.js +11 -1
  76. package/dist/delivery/web/delivery-time-cell.d.ts +3 -0
  77. package/dist/delivery/web/delivery-time-cell.js +6 -0
  78. package/dist/delivery/web/delivery-type-cell.d.ts +3 -0
  79. package/dist/delivery/web/delivery-type-cell.js +17 -0
  80. package/dist/secrets/constants.d.ts +6 -0
  81. package/dist/secrets/constants.js +8 -0
  82. package/dist/secrets/feature.d.ts +3 -0
  83. package/dist/secrets/feature.js +3 -0
  84. package/dist/secrets/i18n.js +4 -3
  85. package/dist/secrets/index.d.ts +3 -2
  86. package/dist/secrets/index.js +3 -2
  87. package/dist/secrets/write-gate.d.ts +1 -0
  88. package/dist/secrets/write-gate.js +14 -6
  89. package/dist/shared/index.d.ts +1 -0
  90. package/dist/shared/index.js +1 -0
  91. package/dist/shared/redact.d.ts +4 -0
  92. package/dist/shared/redact.js +21 -0
  93. package/dist/step-dispatcher/feature.js +2 -1
  94. package/dist/step-dispatcher/mail-runner.d.ts +0 -1
  95. package/dist/step-dispatcher/mail-runner.js +0 -5
  96. package/dist/tenant/web/member-roles-cell.js +1 -1
  97. package/dist/tenant/web/member-status-cell.js +1 -1
  98. package/dist/tenant-lifecycle/feature.js +8 -2
  99. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +2 -1
  100. package/dist/tenant-lifecycle/run-tenant-destroy.js +16 -7
  101. package/dist/user/index.d.ts +1 -0
  102. package/dist/user/index.js +1 -0
  103. package/dist/user/resolve-display-names.d.ts +2 -0
  104. package/dist/user/resolve-display-names.js +24 -0
  105. package/dist/waitlist/handlers/submit.write.js +1 -1
  106. package/dist/waitlist/options.d.ts +3 -2
  107. package/package.json +9 -9
  108. package/src/audit/changes.json +6 -0
  109. package/src/auth-email-password/changes.json +19 -0
  110. package/src/channel-email/changes.json +6 -0
  111. package/src/channel-teams/changes.json +8 -0
  112. package/src/delivery/changes.json +32 -0
  113. package/src/jobs/changes.json +6 -0
  114. package/src/secrets/changes.json +6 -0
  115. package/src/waitlist/changes.json +6 -0
  116. /package/dist/{tenant → shared}/web/translate-or-raw.d.ts +0 -0
  117. /package/dist/{tenant → shared}/web/translate-or-raw.js +0 -0
@@ -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
+ }
@@ -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;
@@ -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
+ }
@@ -89,7 +89,7 @@ export function createSubmitHandler(opts) {
89
89
  locale: resolveWaitlistMailLocale(payload.locale),
90
90
  }));
91
91
  await sendBestEffort(ctx, "admin notice", async (notify) => {
92
- const adminRecipient = opts.notifyRecipient?.(ctx);
92
+ const adminRecipient = await opts.notifyRecipient?.(ctx);
93
93
  // skip: no admin recipient configured
94
94
  if (!adminRecipient)
95
95
  return;
@@ -13,8 +13,9 @@ export type WaitlistInviteOptions = {
13
13
  };
14
14
  export type WaitlistOptions = {
15
15
  /** Address for an admin notification mail on each new entry. Null, an empty
16
- * string or an absent option sends none. Best effort, never fails the submit. */
17
- readonly notifyRecipient?: (ctx: HandlerContext) => string | null;
16
+ * string or an absent option sends none. Best effort, never fails the submit,
17
+ * also when the resolver throws or rejects. */
18
+ readonly notifyRecipient?: (ctx: HandlerContext) => string | null | Promise<string | null>;
18
19
  /** Default `{ mode: "own-tenant", role: "TenantAdmin" }`. */
19
20
  readonly invite?: WaitlistInviteOptions;
20
21
  /** Default: 5 submits per IP per 10 minutes. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.341.0",
3
+ "version": "0.343.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.341.0",
507
- "@cosmicdrift/kumiko-framework": "0.341.0",
508
- "@cosmicdrift/kumiko-headless": "0.341.0",
509
- "@cosmicdrift/kumiko-renderer": "0.341.0",
510
- "@cosmicdrift/kumiko-renderer-web": "0.341.0",
511
- "@cosmicdrift/kumiko-types": "0.341.0",
506
+ "@cosmicdrift/kumiko-dispatcher-live": "0.343.0",
507
+ "@cosmicdrift/kumiko-framework": "0.343.0",
508
+ "@cosmicdrift/kumiko-headless": "0.343.0",
509
+ "@cosmicdrift/kumiko-renderer": "0.343.0",
510
+ "@cosmicdrift/kumiko-renderer-web": "0.343.0",
511
+ "@cosmicdrift/kumiko-types": "0.343.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.341.0",
1029
- "@cosmicdrift/kumiko-locale-es": "0.341.0",
1028
+ "@cosmicdrift/kumiko-locale-de": "0.343.0",
1029
+ "@cosmicdrift/kumiko-locale-es": "0.343.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,23 @@
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
+ },
9
+ {
10
+ "version": "0.342.0",
11
+ "type": "improvement",
12
+ "title": "AuthCard styling hooks (className, headerClassName, titleClassName, bodyClassName)",
13
+ "detail": "AuthCard takes className, headerClassName, titleClassName and bodyClassName\n`AuthCard` renders the body wrapper (`p-6 pt-0 flex flex-col gap-4`) itself and accepts `className` (Card), `headerClassName`, `titleClassName` and `bodyClassName`, merged with tailwind-merge over the defaults. The auth-email-password and auth-mfa screens no longer carry their own body wrapper; the rendered DOM stays the same. The session bootstrap error screen keeps its former bottom padding through `bodyClassName`."
14
+ },
15
+ {
16
+ "version": "0.342.0",
17
+ "type": "improvement",
18
+ "title": "Bootstrap seed can write with app tenant roles",
19
+ "detail": "Bootstrap seed gets `dispatchWriteAs` to write with app tenant roles instead of SystemAdmin."
20
+ },
2
21
  {
3
22
  "version": "0.341.0",
4
23
  "type": "breaking",
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.342.0",
4
+ "type": "improvement",
5
+ "title": "EmailMessage.fromName sets the From display name",
6
+ "detail": "channel-email: fromName sets the display name of the From header\n`EmailMessage` gets an optional `fromName`, and the email channel passes `data.fromName` through like `replyTo`. The SMTP transport combines it with the address of `message.from` or the transport default, so `from: \"App <noreply@x>\"` plus `fromName: \"Tenant via veridom\"` sends from `noreply@x` under the new name. Control characters in the name are replaced by spaces before sending, and an empty name behaves like no name. The PII ciphertext guard refuses a ciphertext `fromName`."
7
+ },
2
8
  {
3
9
  "version": "0.335.0",
4
10
  "type": "fix",