@cosmicdrift/kumiko-bundled-features 0.364.0 → 0.365.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 (161) hide show
  1. package/dist/audit/feature.js +1 -1
  2. package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
  3. package/dist/audit/run-escape-hatch-retention.js +39 -12
  4. package/dist/auth-email-password/bootstrap.d.ts +1 -3
  5. package/dist/auth-email-password/feature.js +2 -0
  6. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -3
  7. package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -3
  8. package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
  9. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -0
  10. package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -1
  11. package/dist/auth-email-password/magic-link-mail.js +6 -3
  12. package/dist/auth-email-password/mail-i18n.d.ts +3 -0
  13. package/dist/auth-email-password/mail-i18n.js +10 -0
  14. package/dist/billing-foundation/consumer-protection/termination-record.js +8 -1
  15. package/dist/billing-foundation/i18n.js +6 -0
  16. package/dist/billing-foundation/subscription-tier-sync.js +8 -2
  17. package/dist/cap-counter/book-cap-usage.js +8 -0
  18. package/dist/cap-counter/cap-reservation.js +7 -3
  19. package/dist/cap-counter/handlers/increment-rolling.write.d.ts +1 -0
  20. package/dist/cap-counter/handlers/increment-rolling.write.js +2 -0
  21. package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -1
  22. package/dist/cap-overview/handlers/tenant-options.query.js +3 -1
  23. package/dist/channel-discord/discord-channel.js +3 -2
  24. package/dist/channel-email/email-channel.js +24 -19
  25. package/dist/channel-email/smtp-transport.js +6 -1
  26. package/dist/channel-slack/slack-channel.js +3 -2
  27. package/dist/channel-teams/teams-channel.js +3 -2
  28. package/dist/crypto-shredding/handlers/forget-subject.write.js +2 -2
  29. package/dist/custom-fields/db/queries/retention.d.ts +1 -1
  30. package/dist/custom-fields/db/queries/retention.js +2 -1
  31. package/dist/data-retention/run-retention-cleanup.js +4 -5
  32. package/dist/delivery/attachment-ref.js +16 -1
  33. package/dist/delivery/delivery-service.js +71 -82
  34. package/dist/feature-toggles/toggle-runtime.d.ts +5 -3
  35. package/dist/feature-toggles/toggle-runtime.js +25 -12
  36. package/dist/files-tenant-data/index.d.ts +1 -0
  37. package/dist/files-tenant-data/index.js +2 -1
  38. package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
  39. package/dist/form-draft/db/queries/owned-file-refs.js +9 -2
  40. package/dist/form-draft/handlers/discard.write.js +1 -1
  41. package/dist/jobs/job-run-logger.js +5 -2
  42. package/dist/metrics/filters.d.ts +16 -1
  43. package/dist/metrics/filters.js +27 -11
  44. package/dist/personal-access-tokens/revoke-for-user.js +3 -7
  45. package/dist/secrets/handlers/delete.write.js +3 -1
  46. package/dist/secrets/write-gate.d.ts +3 -1
  47. package/dist/secrets/write-gate.js +6 -3
  48. package/dist/shared/index.d.ts +1 -0
  49. package/dist/shared/index.js +1 -0
  50. package/dist/shared/resource-image-formats.d.ts +23 -0
  51. package/dist/shared/resource-image-formats.js +21 -0
  52. package/dist/shared/sharp-input-limits.js +7 -8
  53. package/dist/step-dispatcher/webhook-runner.js +2 -2
  54. package/dist/template-resolver/compile-document-body.d.ts +3 -0
  55. package/dist/template-resolver/compile-document-body.js +63 -12
  56. package/dist/template-resolver/compile-steps-block.d.ts +10 -0
  57. package/dist/template-resolver/compile-steps-block.js +45 -0
  58. package/dist/template-resolver/constants.d.ts +4 -1
  59. package/dist/template-resolver/constants.js +10 -1
  60. package/dist/template-resolver/document-sanitizer.js +3 -0
  61. package/dist/template-resolver/document-schema.d.ts +2 -1
  62. package/dist/template-resolver/document-schema.js +6 -1
  63. package/dist/template-resolver/document-steps.d.ts +17 -0
  64. package/dist/template-resolver/document-steps.js +93 -0
  65. package/dist/template-resolver/feature.js +9 -0
  66. package/dist/template-resolver/handlers/bundle-import.d.ts +7 -1
  67. package/dist/template-resolver/handlers/bundle-import.js +63 -38
  68. package/dist/template-resolver/handlers/collection-set.write.js +4 -3
  69. package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
  70. package/dist/template-resolver/handlers/copy-locale.write.js +83 -55
  71. package/dist/template-resolver/handlers/diff-to-system.query.js +1 -0
  72. package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
  73. package/dist/template-resolver/handlers/import-bundle-system.write.js +16 -5
  74. package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
  75. package/dist/template-resolver/handlers/import-bundle.write.js +11 -1
  76. package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
  77. package/dist/template-resolver/handlers/overview.query.js +9 -1
  78. package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -2
  79. package/dist/template-resolver/handlers/resource-upload.write.js +35 -5
  80. package/dist/template-resolver/handlers/validate.query.js +4 -1
  81. package/dist/template-resolver/index.d.ts +3 -3
  82. package/dist/template-resolver/index.js +3 -3
  83. package/dist/template-resolver/layout-appearance.d.ts +1 -1
  84. package/dist/template-resolver/layout-appearance.js +19 -9
  85. package/dist/template-resolver/mail-constants.d.ts +2 -0
  86. package/dist/template-resolver/mail-constants.js +3 -0
  87. package/dist/template-resolver/resource-image.d.ts +2 -9
  88. package/dist/template-resolver/resource-image.js +2 -11
  89. package/dist/template-resolver/seeding.js +2 -0
  90. package/dist/template-resolver/store-template-resource.d.ts +53 -9
  91. package/dist/template-resolver/store-template-resource.js +250 -97
  92. package/dist/template-resolver/template-bundle.js +4 -0
  93. package/dist/template-resolver/template-draft.d.ts +3 -0
  94. package/dist/template-resolver/template-draft.js +1 -1
  95. package/dist/template-resolver/template-meta.d.ts +5 -1
  96. package/dist/template-resolver/template-meta.js +7 -0
  97. package/dist/template-resolver/template-preview.js +1 -2
  98. package/dist/template-resolver/template-resources.d.ts +0 -5
  99. package/dist/template-resolver/template-resources.js +2 -34
  100. package/dist/template-resolver/template-validation.js +12 -9
  101. package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
  102. package/dist/template-resolver/web/document-editor-extensions.js +26 -1
  103. package/dist/template-resolver/web/document-editor.js +5 -1
  104. package/dist/template-resolver/web/document-tiptap-editor.js +30 -7
  105. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +5 -5
  106. package/dist/template-resolver/web/document-toolbar-blocks.js +2 -0
  107. package/dist/template-resolver/web/i18n.js +5 -0
  108. package/dist/template-resolver/web/index.d.ts +1 -0
  109. package/dist/template-resolver/web/index.js +1 -0
  110. package/dist/template-resolver/web/placeholder-line-editor.d.ts +4 -2
  111. package/dist/template-resolver/web/placeholder-line-editor.js +8 -6
  112. package/dist/tenant/handlers/update-member-roles.write.js +3 -6
  113. package/dist/tenant/seeding.js +23 -24
  114. package/dist/tenant-handover/move-entity-graph.js +14 -4
  115. package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
  116. package/dist/tenant-lifecycle/run-tenant-destroy.js +8 -1
  117. package/dist/tenant-lifecycle/stages.js +3 -2
  118. package/dist/tier-engine/constants.d.ts +2 -0
  119. package/dist/tier-engine/constants.js +4 -0
  120. package/dist/tier-engine/entity.d.ts +15 -14
  121. package/dist/tier-engine/entity.js +4 -3
  122. package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +5 -0
  123. package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
  124. package/dist/tier-engine/index.d.ts +1 -1
  125. package/dist/tier-engine/index.js +1 -1
  126. package/dist/tier-engine/tier-resolver.d.ts +13 -13
  127. package/dist/user/constants.d.ts +1 -0
  128. package/dist/user/invited-global-roles.d.ts +2 -4
  129. package/dist/user-data-rights/email-templates.js +0 -1
  130. package/dist/user-data-rights/i18n.js +5 -0
  131. package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +2 -0
  132. package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -2
  133. package/dist/user-data-rights/run-forget-cleanup.js +2 -2
  134. package/dist/waitlist/feature.js +1 -1
  135. package/dist/waitlist/handlers/invite.write.js +21 -3
  136. package/dist/waitlist/handlers/reject.write.js +3 -16
  137. package/dist/waitlist/handlers/submit.write.js +29 -7
  138. package/dist/waitlist/i18n.js +9 -0
  139. package/dist/waitlist/lib.d.ts +2 -1
  140. package/dist/waitlist/lib.js +15 -0
  141. package/dist/workflow-runner/feature.js +1 -1
  142. package/package.json +10 -10
  143. package/src/audit/changes.json +12 -0
  144. package/src/auth-email-password/changes.json +18 -0
  145. package/src/billing-foundation/changes.json +12 -0
  146. package/src/cap-counter/changes.json +13 -0
  147. package/src/cap-overview/changes.json +6 -0
  148. package/src/channel-email/changes.json +18 -0
  149. package/src/delivery/changes.json +12 -0
  150. package/src/document-ingest-foundation/changes.json +6 -0
  151. package/src/feature-toggles/changes.json +7 -0
  152. package/src/jobs/changes.json +12 -0
  153. package/src/metrics/changes.json +6 -0
  154. package/src/personal-access-tokens/changes.json +6 -0
  155. package/src/secrets/changes.json +6 -0
  156. package/src/template-resolver/changes.json +81 -0
  157. package/src/tenant/changes.json +6 -0
  158. package/src/tenant-lifecycle/changes.json +6 -0
  159. package/src/tier-engine/changes.json +12 -0
  160. package/src/user-data-rights/changes.json +6 -0
  161. package/src/waitlist/changes.json +18 -0
@@ -1,3 +1,4 @@
1
+ import { type TierAssignmentSource } from "./constants.js";
1
2
  export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
2
3
  tier: {
3
4
  readonly type: "text";
@@ -23,25 +24,25 @@ export declare const tierAssignmentEntity: import("@cosmicdrift/kumiko-types/fie
23
24
  required: true;
24
25
  };
25
26
  source: {
26
- readonly type: "text";
27
+ readonly type: "select";
27
28
  readonly description?: string;
28
- readonly maxLength?: number;
29
- readonly minLength?: number;
30
- readonly autoComplete?: string;
29
+ readonly options: ("billing" | "default" | "manual")[];
31
30
  readonly required?: boolean;
32
- readonly searchable?: boolean;
33
31
  readonly sortable?: boolean;
34
32
  readonly filterable?: boolean;
35
- readonly encrypted?: boolean;
36
33
  readonly sensitive?: boolean;
37
- readonly writeOnly?: boolean;
38
- readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password";
40
- readonly default?: string;
34
+ readonly default?: "billing" | "default" | "manual" | undefined;
41
35
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
- readonly multiline?: boolean | {
43
- readonly rows?: number;
44
- };
36
+ readonly display?: "radio" | "dropdown";
37
+ readonly optionTones?: {
38
+ readonly billing?: "bad" | "neutral" | "ok" | "warn" | undefined;
39
+ readonly default?: "bad" | "neutral" | "ok" | "warn" | undefined;
40
+ readonly manual?: "bad" | "neutral" | "ok" | "warn" | undefined;
41
+ } | undefined;
42
+ readonly optionsQuery?: string;
43
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
44
+ readonly optionsAvailabilityQuery?: string;
45
+ readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"billing" | "default" | "manual">[] | undefined;
45
46
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
46
47
  required: false;
47
48
  };
@@ -50,6 +51,6 @@ export type TierAssignmentRow = {
50
51
  readonly id: string;
51
52
  readonly version: number;
52
53
  readonly tier: string;
53
- readonly source: string | null;
54
+ readonly source: TierAssignmentSource | null;
54
55
  readonly tenantId: string;
55
56
  };
@@ -1,4 +1,5 @@
1
- import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { createEntity, createSelectField, createTextField, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { TierAssignmentSources } from "./constants.js";
2
3
  // tier-assignment — pro Plattform-Tenant genau ein Aggregat. Aggregate-ID
3
4
  // wird deterministisch aus tenantId abgeleitet (uuidv5, siehe aggregate-id.ts).
4
5
  //
@@ -33,9 +34,9 @@ export const tierAssignmentEntity = createEntity({
33
34
  }),
34
35
  // Origin of the assignment, see TierAssignmentSources. Optional for back-compat with
35
36
  // rows without source. Protects manual grants from being overwritten by the billing sync.
36
- source: createTextField({
37
+ source: createSelectField({
38
+ options: Object.values(TierAssignmentSources),
37
39
  required: false,
38
- maxLength: 20,
39
40
  personal: false,
40
41
  reason: "technical_reference",
41
42
  }),
@@ -15,6 +15,11 @@ export type SetTenantTierOptions = {
15
15
  export declare function createSetTenantTierWrite(opts?: SetTenantTierOptions): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"set-tenant-tier", z.ZodObject<{
16
16
  tenantId: z.ZodString;
17
17
  tier: z.ZodString;
18
+ source: z.ZodDefault<z.ZodEnum<{
19
+ readonly manual: "manual";
20
+ readonly billing: "billing";
21
+ readonly default: "default";
22
+ }>>;
18
23
  }, z.core.$strip>, {
19
24
  tenantId: string;
20
25
  tier: string;
@@ -13,7 +13,8 @@ import { tierAssignmentEntity } from "../entity.js";
13
13
  // Cross-tenant mechanics: ctx.forTenant(tenantId) yields a tenant-mode db bound to the target plus
14
14
  // the streamTenantId, so the event lands in the target's stream with the operator as actor.
15
15
  //
16
- // `source: TierAssignmentSources.manual` marks the grant so the billing sync skips it.
16
+ // `source` defaults to manual and marks the grant so the billing sync skips it; passing
17
+ // `billing` hands the row back to the sync, which overwrites it on the next webhook.
17
18
  // Upsert: one aggregate per tenant (deterministic aggregate id).
18
19
  //
19
20
  // Effective-set invalidation: the executor write does not fire the
@@ -30,7 +31,7 @@ const executor = createEventStoreExecutor(tierAssignmentTable, tierAssignmentEnt
30
31
  export function createSetTenantTierWrite(opts = {}) {
31
32
  return defineWriteHandler({
32
33
  name: "set-tenant-tier",
33
- description: "Assigns a tier to any tenant as a manual grant without a billing purchase, marking it so a later billing sync will not overwrite it, and applies the new feature set immediately.",
34
+ description: "Assigns a tier to any tenant as a manual grant without a billing purchase, marking it so a later billing sync will not overwrite it (or, with source billing, handing the row back to the sync), and applies the new feature set immediately.",
34
35
  schema: z.object({
35
36
  tenantId: z.string().min(1),
36
37
  tier: z
@@ -38,6 +39,7 @@ export function createSetTenantTierWrite(opts = {}) {
38
39
  .min(1)
39
40
  .max(50)
40
41
  .refine((t) => !opts.validTiers || opts.validTiers.has(t), { message: "unknown tier" }),
42
+ source: z.enum(TierAssignmentSources).default(TierAssignmentSources.manual),
41
43
  }),
42
44
  access: { roles: ["SystemAdmin"] },
43
45
  agent: { risk: "high" },
@@ -53,12 +55,13 @@ export function createSetTenantTierWrite(opts = {}) {
53
55
  const tenantId = event.payload.tenantId; // @cast-boundary engine-bridge
54
56
  const { db: tdb, streamTenantId } = requireForTenant(ctx, tenantId);
55
57
  const tier = event.payload.tier;
58
+ const source = event.payload.source;
56
59
  const existing = await fetchOne(tdb, tierAssignmentTable, { tenantId });
57
60
  if (existing) {
58
61
  const result = await executor.update({
59
62
  id: existing.id,
60
63
  version: existing.version,
61
- changes: { tier, source: TierAssignmentSources.manual },
64
+ changes: { tier, source },
62
65
  }, event.user, tdb, { streamTenantId });
63
66
  if (!result.isSuccess)
64
67
  return result;
@@ -68,7 +71,7 @@ export function createSetTenantTierWrite(opts = {}) {
68
71
  const result = await executor.create({
69
72
  id: tierAssignmentAggregateId(tenantId),
70
73
  tier,
71
- source: TierAssignmentSources.manual,
74
+ source,
72
75
  tenantId,
73
76
  }, event.user, tdb, { streamTenantId });
74
77
  if (!result.isSuccess)
@@ -1,6 +1,6 @@
1
1
  export { tierAssignmentAggregateId } from "./aggregate-id.js";
2
2
  export { type AddOnDefinition, type AddOnMap, type ComposeAppInput, type ComposedApp, composeApp, type TierDefinition, type TierMap, } from "./compose-app.js";
3
- export { TIER_ENGINE_FEATURE, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, } from "./constants.js";
3
+ export { isTierAssignmentSource, TIER_ENGINE_FEATURE, type TierAssignmentSource, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, } from "./constants.js";
4
4
  export { tierAssignmentEntity } from "./entity.js";
5
5
  export { type CreateTierEngineOptions, createTierEngineFeature, tierEngineFeature, } from "./feature.js";
6
6
  export { createTierOptionGate, type TierOptionAvailability, type TierOptionGate, type TierOptionGateDeps, type TierOptionSpec, } from "./tier-option-gate.js";
@@ -8,7 +8,7 @@
8
8
  // - `TierEngineHandlers` / `TierEngineQueries` — qualified handler names
9
9
  export { tierAssignmentAggregateId } from "./aggregate-id.js";
10
10
  export { composeApp, } from "./compose-app.js";
11
- export { TIER_ENGINE_FEATURE, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, } from "./constants.js";
11
+ export { isTierAssignmentSource, TIER_ENGINE_FEATURE, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, } from "./constants.js";
12
12
  export { tierAssignmentEntity } from "./entity.js";
13
13
  export { createTierEngineFeature, tierEngineFeature, } from "./feature.js";
14
14
  export { createTierOptionGate, } from "./tier-option-gate.js";
@@ -31,25 +31,25 @@ export declare function createTierResolver<TTier extends string, TCaps>(deps: Ti
31
31
  required: true;
32
32
  };
33
33
  source: {
34
- readonly type: "text";
34
+ readonly type: "select";
35
35
  readonly description?: string;
36
- readonly maxLength?: number;
37
- readonly minLength?: number;
38
- readonly autoComplete?: string;
36
+ readonly options: ("billing" | "default" | "manual")[];
39
37
  readonly required?: boolean;
40
- readonly searchable?: boolean;
41
38
  readonly sortable?: boolean;
42
39
  readonly filterable?: boolean;
43
- readonly encrypted?: boolean;
44
40
  readonly sensitive?: boolean;
45
- readonly writeOnly?: boolean;
46
- readonly readAsInstruction?: boolean;
47
- readonly format?: "email" | "url" | "phone" | "password";
48
- readonly default?: string;
41
+ readonly default?: "billing" | "default" | "manual" | undefined;
49
42
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
50
- readonly multiline?: boolean | {
51
- readonly rows?: number;
52
- };
43
+ readonly display?: "radio" | "dropdown";
44
+ readonly optionTones?: {
45
+ readonly billing?: "bad" | "neutral" | "ok" | "warn" | undefined;
46
+ readonly default?: "bad" | "neutral" | "ok" | "warn" | undefined;
47
+ readonly manual?: "bad" | "neutral" | "ok" | "warn" | undefined;
48
+ } | undefined;
49
+ readonly optionsQuery?: string;
50
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
51
+ readonly optionsAvailabilityQuery?: string;
52
+ readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"billing" | "default" | "manual">[] | undefined;
53
53
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
54
54
  required: false;
55
55
  };
@@ -3,6 +3,7 @@ export declare const USER_FEATURE: "user";
3
3
  * platform operator. Membership roles (TenantAdmin/Admin/User/…) live on
4
4
  * tenant-membership, not here. */
5
5
  export declare const GLOBAL_ROLE_OPTIONS: readonly ["SystemAdmin"];
6
+ export type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
6
7
  export declare const UserHandlers: {
7
8
  readonly create: "user:write:user:create";
8
9
  readonly update: "user:write:user:update";
@@ -1,14 +1,12 @@
1
1
  import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
- import { GLOBAL_ROLE_OPTIONS } from "./constants.js";
3
- type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
2
+ import { type GlobalRole } from "./constants.js";
4
3
  export declare function parseInvitedGlobalRoles(raw: unknown): readonly GlobalRole[];
5
4
  export declare function grantInvitedGlobalRoles(db: DbRunner, options: {
6
5
  readonly userId: string;
7
- readonly globalRoles: readonly string[];
6
+ readonly globalRoles: readonly GlobalRole[];
8
7
  }): Promise<import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure | {
9
8
  readonly isSuccess: true;
10
9
  readonly data: {
11
10
  readonly roles: readonly string[];
12
11
  };
13
12
  }>;
14
- export {};
@@ -25,7 +25,6 @@ export const GDPR_MAIL_EN = {
25
25
  "gdpr.mail.deletionRequested.cancel": "If you didn't request this, sign in and cancel the deletion in your account settings before the deadline.",
26
26
  "gdpr.mail.deletionExecuted.subject": "{app} — Your account has been deleted",
27
27
  "gdpr.mail.deletionExecuted.intro": "your {app} account and the associated personal data were deleted on {when}. This action is permanent.",
28
- "gdpr.mail.fallbackUrl": "If the button doesn't work, copy this link into your browser:",
29
28
  };
30
29
  registerMailTranslations("en", GDPR_MAIL_EN);
31
30
  export function formatGdprMailTimestamp(iso) {
@@ -1,4 +1,9 @@
1
1
  export const USER_DATA_RIGHTS_I18N = {
2
+ "user-data-rights:feature.label": { en: "Privacy" },
3
+ "user-data-rights:notify.export-ready.label": { en: "Data export ready" },
4
+ "user-data-rights:notify.export-failed.label": { en: "Data export failed" },
5
+ "user-data-rights:notify.deletion-requested.label": { en: "Deletion requested" },
6
+ "user-data-rights:notify.deletion-executed.label": { en: "Deletion completed" },
2
7
  "user-data-rights:nav.exportJobs": { en: "GDPR exports" },
3
8
  "screen:export-job-list.title": { en: "GDPR exports" },
4
9
  "screen:export-job-detail.title": { en: "Export job" },
@@ -1,6 +1,8 @@
1
1
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
2
+ import { type WritePurpose } from "@cosmicdrift/kumiko-framework/db";
2
3
  export type UpdateUserLifecycleOptions = {
3
4
  readonly expect?: Readonly<Record<string, string | number | boolean | null>>;
5
+ readonly writePurpose?: WritePurpose;
4
6
  };
5
7
  export declare function updateUserLifecycle(conn: DbRunner, userId: string, changes: Record<string, unknown>, options?: UpdateUserLifecycleOptions): Promise<{
6
8
  readonly applied: boolean;
@@ -1,5 +1,5 @@
1
1
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { createSystemUser, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
5
5
  import { USER_STATUS, userEntity, userTable } from "../../user/index.js";
@@ -39,7 +39,11 @@ export async function updateUserLifecycle(conn, userId, changes, options) {
39
39
  // version to pass as `payload.version`, which update() requires otherwise.
40
40
  // Callers needing a guard use `options.expect` instead.
41
41
  const tenantDb = createTenantDb(conn, SYSTEM_TENANT_ID, "system");
42
- const result = await userExecutor.update({ id: userId, changes }, createSystemUser(SYSTEM_TENANT_ID), tenantDb, { skipOptimisticLock: true, ...(options?.expect && { expect: options.expect }) });
42
+ const result = await userExecutor.update({ id: userId, changes }, createSystemUser(SYSTEM_TENANT_ID), tenantDb, {
43
+ skipOptimisticLock: true,
44
+ ...(options?.expect && { expect: options.expect }),
45
+ ...(options?.writePurpose && { writePurpose: options.writePurpose }),
46
+ });
43
47
  if (result.isSuccess)
44
48
  return { applied: true };
45
49
  if (options?.expect &&
@@ -36,7 +36,7 @@ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
36
36
  // retried automatisch).
37
37
  import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
38
38
  import { configuredPiiSubjectKms, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
39
- import { createTenantDb, entityEventName, executeRawQuery, nullBlindIndexesForSubject, } from "@cosmicdrift/kumiko-framework/db";
39
+ import { createTenantDb, entityEventName, executeRawQuery, nullBlindIndexesForSubject, WritePurposes, } from "@cosmicdrift/kumiko-framework/db";
40
40
  import { EXT_USER_DATA, EXT_USER_DATA_ORDER, extensionUsageEscapeHatchReason, } from "@cosmicdrift/kumiko-framework/engine";
41
41
  import { createEscapeHatchReporter, UNATTRIBUTED_ACTOR, } from "@cosmicdrift/kumiko-framework/pipeline";
42
42
  import { purgeSearchDocumentsForSubject, } from "@cosmicdrift/kumiko-framework/search";
@@ -287,7 +287,7 @@ async function processUser(args) {
287
287
  // geworfen hat, kommen wir hier nicht an — die Tx rollback'd
288
288
  // alles, der User bleibt im DeletionRequested-Status, naechster
289
289
  // Run retried.
290
- await updateUserLifecycle(tx, userId, { status: USER_STATUS.Deleted });
290
+ await updateUserLifecycle(tx, userId, { status: USER_STATUS.Deleted }, { writePurpose: WritePurposes.erasure });
291
291
  txSucceeded = true;
292
292
  });
293
293
  }
@@ -12,7 +12,7 @@ import { waitlistListScreen } from "./screens.js";
12
12
  import { waitlistEntryDeleteHook, waitlistEntryExportHook } from "./user-data-hooks.js";
13
13
  export function createWaitlistFeature(opts = {}) {
14
14
  return defineFeature(WAITLIST_FEATURE, (r) => {
15
- r.describe("Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response never reveals whether an address was already listed), and mails the submitter a confirmation. SystemAdmins review entries in a list screen and invite or reject them; an invite creates the entrant's own tenant (or joins one shared tenant) and mails an accept link with the configured membership role, never a global role. Entries are platform-wide, their personal data is crypto-shredding-ready, and the EXT_USER_DATA hooks export and erase entries matching a user's email.");
15
+ r.describe("Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response body is identical whether or not an address was already listed), and mails the submitter a confirmation. SystemAdmins review entries in a list screen and invite or reject them; an invite creates the entrant's own tenant (or joins one shared tenant) and mails an accept link with the configured membership role, never a global role. Entries are platform-wide, their personal data is crypto-shredding-ready, and the EXT_USER_DATA hooks export and erase entries matching a user's email.");
16
16
  r.uiHints({ displayLabel: "Waitlist", category: "data", recommended: false });
17
17
  r.systemScope();
18
18
  r.requires("tenant", "auth-email-password", "user-data-rights");
@@ -1,4 +1,5 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { acquireNamespacedAdvisoryLock } from "@cosmicdrift/kumiko-framework/db";
2
3
  import { createSystemUser, defineWriteHandler, parseTenantId, } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import { InternalError, NotFoundError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
4
5
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
@@ -6,8 +7,11 @@ import { AuthErrors, AuthHandlers } from "../../auth-email-password/index.js";
6
7
  import { TenantHandlers, tenantTable } from "../../tenant/index.js";
7
8
  import { DEFAULT_OWN_TENANT_INVITE_ROLE, WAITLIST_STATUS, WaitlistErrors } from "../constants.js";
8
9
  import { isOpenWaitlistStatus } from "../entity.js";
9
- import { adminAccess, platformActor, waitlistDb, waitlistExecutor } from "../lib.js";
10
+ import { adminAccess, isTenantMember, platformActor, waitlistDb, waitlistExecutor, } from "../lib.js";
10
11
  import { WaitlistIdSchema } from "../payloads.js";
12
+ // pg_advisory_xact_lock namespace (int4): 'wlin' as ASCII, disjoint from the submit lock.
13
+ const INVITE_LOCK_NAMESPACE = 0x776c696e;
14
+ const INVITE_LOCK_REASON = "takes the per-entry advisory lock so parallel invites cannot both create a tenant and invitation; TenantDb has no lock API";
11
15
  const DEFAULT_INVITE = {
12
16
  mode: "own-tenant",
13
17
  role: DEFAULT_OWN_TENANT_INVITE_ROLE,
@@ -17,7 +21,7 @@ function tenantKeyForEntry(entryId) {
17
21
  return `waitlist-${entryId}`;
18
22
  }
19
23
  function readEntryRow(row) {
20
- const { id, version, email, name, company, linkedTenantId } = row;
24
+ const { id, version, email, name, company, linkedTenantId, locale } = row;
21
25
  if (typeof id !== "string" ||
22
26
  typeof version !== "number" ||
23
27
  typeof email !== "string" ||
@@ -32,6 +36,7 @@ function readEntryRow(row) {
32
36
  name,
33
37
  company: typeof company === "string" && company !== "" ? company : null,
34
38
  linkedTenantId: typeof linkedTenantId === "string" ? linkedTenantId : null,
39
+ locale: typeof locale === "string" && locale !== "" ? locale : undefined,
35
40
  };
36
41
  }
37
42
  function isInviteAlreadyAccepted(details) {
@@ -75,8 +80,13 @@ export function createInviteHandler(invite = DEFAULT_INVITE) {
75
80
  description: "Invites a pending or already-invited waitlist entry: creates its tenant (or uses the configured shared tenant), mails the entrant a fresh accept link with the configured role and marks the entry invited; rejected entries cannot be invited. The invitee never receives a global role.",
76
81
  // Sends an invite link by email, which cannot be recalled.
77
82
  agent: { risk: "high" },
83
+ escapeHatch: { grants: ["unsafeRaw"], reason: INVITE_LOCK_REASON },
78
84
  handler: async (event, ctx) => {
79
85
  const db = waitlistDb(ctx);
86
+ if (!ctx.systemDb) {
87
+ throw new InternalError({ message: "waitlist:invite requires ctx.systemDb" });
88
+ }
89
+ await acquireNamespacedAdvisoryLock(ctx.systemDb.unsafeRaw(INVITE_LOCK_REASON), INVITE_LOCK_NAMESPACE, event.payload.id);
80
90
  const row = await waitlistExecutor.detail({ id: event.payload.id }, platformActor(), db);
81
91
  if (!row)
82
92
  return writeFailure(new NotFoundError("waitlistEntry", event.payload.id));
@@ -87,10 +97,18 @@ export function createInviteHandler(invite = DEFAULT_INVITE) {
87
97
  const target = await resolveInviteTarget(invite, entry, ctx);
88
98
  if ("isSuccess" in target)
89
99
  return target;
100
+ if (await isTenantMember(db, entry.email, target.tenantId)) {
101
+ return writeFailure(new UnprocessableError(WaitlistErrors.notInvitable));
102
+ }
90
103
  // inviteCreate is admin-only (no system role); the system variant with an
91
104
  // empty globalRoles list is the only in-process path, and a waitlist
92
105
  // invite must never grant a global role.
93
- const invited = await ctx.writeAs(createSystemUser(target.tenantId), AuthHandlers.systemInviteCreate, { email: entry.email.toLowerCase(), role: target.role, globalRoles: [] });
106
+ const invited = await ctx.writeAs(createSystemUser(target.tenantId), AuthHandlers.systemInviteCreate, {
107
+ email: entry.email.toLowerCase(),
108
+ role: target.role,
109
+ globalRoles: [],
110
+ ...(entry.locale !== undefined && { locale: entry.locale }),
111
+ });
94
112
  if (!invited.isSuccess) {
95
113
  // Re-inviting a person who already accepted would reset their live invitation.
96
114
  if (isInviteAlreadyAccepted(invited.error.details)) {
@@ -1,24 +1,11 @@
1
- import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { createSystemUser, defineWriteHandler, parseTenantId, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { InternalError, NotFoundError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
4
- import { cancelPendingInvitation, INVITATION_STATUS, tenantInvitationsTable, tenantMembershipsTable, } from "../../tenant/index.js";
5
- import { userTable } from "../../user/index.js";
4
+ import { cancelPendingInvitation, INVITATION_STATUS, tenantInvitationsTable, } from "../../tenant/index.js";
6
5
  import { WAITLIST_STATUS, WaitlistErrors } from "../constants.js";
7
6
  import { isOpenWaitlistStatus } from "../entity.js";
8
- import { adminAccess, normalizeEmail, platformActor, waitlistDb, waitlistExecutor, } from "../lib.js";
7
+ import { adminAccess, isTenantMember, normalizeEmail, platformActor, waitlistDb, waitlistExecutor, } from "../lib.js";
9
8
  import { WaitlistIdSchema } from "../payloads.js";
10
- // An admin re-invite can flip an accepted invitation back to pending, so the
11
- // invitation status alone cannot prove the person holds no access.
12
- async function isTenantMember(db, email, tenantId) {
13
- const user = await fetchOne(db, userTable, {
14
- email: normalizeEmail(email),
15
- isDeleted: false,
16
- });
17
- if (!user)
18
- return false;
19
- const membership = await fetchOne(db, tenantMembershipsTable, { userId: user.id, tenantId });
20
- return membership !== undefined && membership !== null;
21
- }
22
9
  // An invited entry already has a live invitation link; rejecting must kill it,
23
10
  // otherwise the rejected person can still sign up. Once accepted they hold
24
11
  // access, which only tenant member removal may revoke.
@@ -13,8 +13,10 @@ const SUBMIT_LOCK_NAMESPACE = 0x776c7374;
13
13
  const SUBMIT_LOCK_REASON = "takes the per-email advisory lock so parallel submits cannot both pass the duplicate check; TenantDb has no lock API";
14
14
  // Logged-in callers carry no anonymous role, so every signed-in role must be listed.
15
15
  const SUBMIT_ROLES = access.roles(...access.anonymous, ...access.authenticated, "Member", "TenantAdmin");
16
- // Identical for new, duplicate and honeypot submits: the response must not
17
- // reveal whether the email is already on the list.
16
+ const OPEN_STATUSES = [WAITLIST_STATUS.Pending, WAITLIST_STATUS.Invited];
17
+ // Identical body for new, duplicate and honeypot submits so the response does not
18
+ // state whether the email is already on the list. Timing still differs: only the
19
+ // new-entry path writes and sends mail.
18
20
  const SUBMITTED = {
19
21
  isSuccess: true,
20
22
  data: { kind: "submitted" },
@@ -31,8 +33,26 @@ function isHoneypotFilled(payload) {
31
33
  return payload.website !== undefined && payload.website.trim() !== "";
32
34
  }
33
35
  async function hasOpenEntryForEmail(email, ctx) {
34
- const found = await waitlistExecutor.list({ filter: { field: "email", op: "eq", value: email }, limit: 50 }, platformActor(), waitlistDb(ctx));
35
- return found.rows.some((row) => row["status"] === WAITLIST_STATUS.Pending || row["status"] === WAITLIST_STATUS.Invited);
36
+ const found = await waitlistExecutor.list({
37
+ filter: { field: "email", op: "eq", value: email },
38
+ filters: [{ field: "status", op: "in", value: OPEN_STATUSES }],
39
+ limit: 1,
40
+ }, platformActor(), waitlistDb(ctx));
41
+ return found.rows.length > 0;
42
+ }
43
+ // The resolver usually reads config; run it outside the handler transaction's
44
+ // critical section so a failing statement cannot abort the transaction that holds
45
+ // the entry, and a slow resolver does not extend the per-email advisory lock.
46
+ async function resolveAdminRecipient(opts, ctx) {
47
+ try {
48
+ return (await opts.notifyRecipient?.(ctx)) || null;
49
+ }
50
+ catch (err) {
51
+ ctx.log?.warn("[waitlist:submit] admin notice recipient resolution failed", {
52
+ error: err instanceof Error ? err.message : String(err),
53
+ });
54
+ return null;
55
+ }
36
56
  }
37
57
  async function sendBestEffort(ctx, label, send) {
38
58
  try {
@@ -48,13 +68,15 @@ async function sendBestEffort(ctx, label, send) {
48
68
  }
49
69
  }
50
70
  export function createSubmitHandler(opts) {
71
+ const emailRateLimits = opts.emailRateLimits ?? DEFAULT_SUBMIT_EMAIL_RATE_LIMITS;
51
72
  return defineWriteHandler({
52
73
  name: "submit",
53
74
  schema: WaitlistSubmitSchema,
54
75
  access: { roles: SUBMIT_ROLES, personalData: "public-intake" },
55
76
  rateLimit: opts.rateLimit ?? DEFAULT_SUBMIT_RATE_LIMIT,
56
- additionalRateLimits: opts.emailRateLimits ?? DEFAULT_SUBMIT_EMAIL_RATE_LIMITS,
57
- description: "Public waitlist sign-up: registers an interest entry and sends the submitter a confirmation mail. Always reports success, whether the address is new or already on the list.",
77
+ // An empty array means "disabled"; boot validation rejects an empty additionalRateLimits.
78
+ ...(emailRateLimits.length > 0 && { additionalRateLimits: emailRateLimits }),
79
+ description: "Public waitlist sign-up: registers an interest entry and sends the submitter a confirmation mail. Reports success whether the address is new or already on the list; the response body is identical, response timing can still differ.",
58
80
  escapeHatch: { grants: ["unsafeRaw"], reason: SUBMIT_LOCK_REASON },
59
81
  handler: async (event, ctx) => {
60
82
  const payload = event.payload;
@@ -66,6 +88,7 @@ export function createSubmitHandler(opts) {
66
88
  message: "waitlist:submit requires ctx.systemDb — is r.systemScope() set?",
67
89
  });
68
90
  }
91
+ const adminRecipient = await resolveAdminRecipient(opts, ctx);
69
92
  await acquireNamespacedAdvisoryLock(ctx.systemDb.unsafeRaw(SUBMIT_LOCK_REASON), SUBMIT_LOCK_NAMESPACE, email);
70
93
  if (await hasOpenEntryForEmail(email, ctx))
71
94
  return SUBMITTED;
@@ -89,7 +112,6 @@ export function createSubmitHandler(opts) {
89
112
  locale: resolveWaitlistMailLocale(payload.locale),
90
113
  }));
91
114
  await sendBestEffort(ctx, "admin notice", async (notify) => {
92
- const adminRecipient = await opts.notifyRecipient?.(ctx);
93
115
  // skip: no admin recipient configured
94
116
  if (!adminRecipient)
95
117
  return;
@@ -1,4 +1,13 @@
1
1
  export const WAITLIST_I18N = {
2
+ "waitlist:feature.label": { en: "Waitlist", de: "Warteliste" },
3
+ "waitlist:notify.confirmation.label": {
4
+ en: "Waitlist confirmation",
5
+ de: "Bestätigung der Warteliste",
6
+ },
7
+ "waitlist:notify.admin-notice.label": {
8
+ en: "New waitlist entry",
9
+ de: "Neuer Eintrag auf der Warteliste",
10
+ },
2
11
  "screen:waitlist-list.title": { en: "Waitlist", de: "Warteliste" },
3
12
  "waitlist.nav.waitlist": { en: "Waitlist", de: "Warteliste" },
4
13
  "waitlist:entity:waitlistEntry:field:name": { en: "Name", de: "Name" },
@@ -1,5 +1,5 @@
1
1
  import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
- import { type HandlerContext, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type HandlerContext, type SessionUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  export declare const WAITLIST_CROSS_TENANT_REASON = "waitlist is platform-wide (systemScope)";
4
4
  export declare const adminAccess: {
5
5
  readonly roles: readonly string[];
@@ -8,3 +8,4 @@ export declare const waitlistExecutor: import("@cosmicdrift/kumiko-types/event-s
8
8
  export declare function normalizeEmail(email: string): string;
9
9
  export declare function waitlistDb(ctx: HandlerContext): TenantDb;
10
10
  export declare function platformActor(): SessionUser;
11
+ export declare function isTenantMember(db: TenantDb, email: string, tenantId: TenantId): Promise<boolean>;
@@ -1,6 +1,9 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
3
  import { access, createSystemUser, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
3
4
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
5
+ import { tenantMembershipsTable } from "../tenant/index.js";
6
+ import { userTable } from "../user/index.js";
4
7
  import { waitlistEntryEntity, waitlistEntryTable } from "./entity.js";
5
8
  export const WAITLIST_CROSS_TENANT_REASON = "waitlist is platform-wide (systemScope)";
6
9
  export const adminAccess = { roles: access.systemAdmin };
@@ -22,3 +25,15 @@ export function waitlistDb(ctx) {
22
25
  export function platformActor() {
23
26
  return createSystemUser(SYSTEM_TENANT_ID);
24
27
  }
28
+ // An admin re-invite can flip an accepted invitation back to pending, so the
29
+ // invitation status alone cannot prove the person holds no access.
30
+ export async function isTenantMember(db, email, tenantId) {
31
+ const user = await fetchOne(db, userTable, {
32
+ email: normalizeEmail(email),
33
+ isDeleted: false,
34
+ });
35
+ if (!user)
36
+ return false;
37
+ const membership = await fetchOne(db, tenantMembershipsTable, { userId: user.id, tenantId });
38
+ return membership !== undefined && membership !== null;
39
+ }
@@ -30,7 +30,7 @@ import { registerWorkflowRunPendingProjection } from "./pending-projection.js";
30
30
  import { workflowRunPendingTableMeta } from "./tables.js";
31
31
  // Kebab-case matching the "./workflow-runner" export subpath — every other
32
32
  // bundled feature's runtime name follows this convention, and
33
- // FEATURE_IMPORT_REGISTRY (use-all-bundled) keys off it.
33
+ // use-all-bundled's check-coverage matches exports to mounted features by it.
34
34
  const FEATURE_NAME = "workflow-runner";
35
35
  export const workflowRunnerFeature = defineFeature(FEATURE_NAME, (r) => {
36
36
  r.describe("Writes the run-envelope (`workflow.run-started` / `workflow.run-completed` / `workflow.run-failed`) for a workflow run onto its `workflow-run` aggregate stream. Provides `startAndRunWorkflow` and `registerEventTrigger` for consumer features to wire up `defineWorkflow`-based workflows against domain events. Registers no workflows itself. Runs the resume loop for suspended `wait`/`retry`/`waitForEvent` steps (`resume-due-runs` job + `resume-run` handler); `registerEventTrigger` wires an event-wakeup subscriber per workflow that declares `awaits` so a `waitForEvent` step resumes as soon as its awaited event arrives, not just on timeout.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.364.0",
3
+ "version": "0.365.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>",
@@ -524,12 +524,12 @@
524
524
  }
525
525
  },
526
526
  "dependencies": {
527
- "@cosmicdrift/kumiko-dispatcher-live": "0.364.0",
528
- "@cosmicdrift/kumiko-framework": "0.364.0",
529
- "@cosmicdrift/kumiko-headless": "0.364.0",
530
- "@cosmicdrift/kumiko-renderer": "0.364.0",
531
- "@cosmicdrift/kumiko-renderer-web": "0.364.0",
532
- "@cosmicdrift/kumiko-types": "0.364.0",
527
+ "@cosmicdrift/kumiko-dispatcher-live": "0.365.0",
528
+ "@cosmicdrift/kumiko-framework": "0.365.0",
529
+ "@cosmicdrift/kumiko-headless": "0.365.0",
530
+ "@cosmicdrift/kumiko-renderer": "0.365.0",
531
+ "@cosmicdrift/kumiko-renderer-web": "0.365.0",
532
+ "@cosmicdrift/kumiko-types": "0.365.0",
533
533
  "@mollie/api-client": "^4.5.0",
534
534
  "@node-rs/argon2": "^2.0.2",
535
535
  "@tiptap/core": "3.31.3",
@@ -1078,9 +1078,9 @@
1078
1078
  ],
1079
1079
  "devDependencies": {
1080
1080
  "@testing-library/user-event": "^14.6.1",
1081
- "@cosmicdrift/kumiko-locale-de": "0.364.0",
1082
- "@cosmicdrift/kumiko-locale-es": "0.364.0",
1083
- "@cosmicdrift/kumiko-renderer-mail-html": "0.364.0",
1081
+ "@cosmicdrift/kumiko-locale-de": "0.365.0",
1082
+ "@cosmicdrift/kumiko-locale-es": "0.365.0",
1083
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.365.0",
1084
1084
  "jsqr": "^1.4.0",
1085
1085
  "unpdf": "^1.8.1"
1086
1086
  }
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.365.0",
4
+ "type": "fix",
5
+ "title": "Escape-hatch retention keeps the operator floor and deletes in bounded batches",
6
+ "detail": "An explicitly configured retention key now acts as a lower bound for tenant compliance-profile cutoffs, so a tenant profile can no longer delete events younger than the operator allows. A key left at its default does not count. Pruning runs in batches of 5000 events, each in its own transaction, until a batch comes back short."
7
+ },
8
+ {
9
+ "version": "0.365.0",
10
+ "type": "fix",
11
+ "title": "Audit retention events always use the system user",
12
+ "detail": "Events written by the audit feature no longer fall back to the calling user id and always record the system user."
13
+ },
2
14
  {
3
15
  "version": "0.364.0",
4
16
  "type": "fix",
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "version": "0.365.0",
4
+ "type": "fix",
5
+ "title": "Magic-link mail expiry and app name use the template locale",
6
+ "detail": "The expiry text, the app-name fallback and the notify locale follow the locale the auth mail template actually renders in, while the link keeps the request locale."
7
+ },
8
+ {
9
+ "version": "0.365.0",
10
+ "type": "improvement",
11
+ "title": "systemInviteCreate takes an optional locale tag",
12
+ "detail": "The payload accepts a locale (BCP 47 tag) that wins over the request locale when rendering the invitation mail, so system callers such as the waitlist can invite in the recipient's language."
13
+ },
14
+ {
15
+ "version": "0.365.0",
16
+ "type": "improvement",
17
+ "title": "Auth mails carry designer labels",
18
+ "detail": "The feature registers <feature>:feature.label and notify labels for password-reset, account-unlock, email-verification, signup-activation and invite, so the document designer lists them by name. German and Spanish ship in the locale packages."
19
+ },
2
20
  {
3
21
  "version": "0.364.0",
4
22
  "type": "fix",