@cosmicdrift/kumiko-bundled-features 0.350.0 → 0.352.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/dist/audit/handlers/list.query.js +1 -1
  2. package/dist/auth-email-password/email-templates.js +1 -1
  3. package/dist/auth-email-password/handlers/invite-create.write.js +1 -1
  4. package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
  5. package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
  6. package/dist/auth-email-password/reset-token.d.ts +1 -1
  7. package/dist/auth-email-password/unlock-token.d.ts +1 -1
  8. package/dist/auth-email-password/verification-token.d.ts +1 -1
  9. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
  10. package/dist/auth-mfa/handlers/enable-confirm.write.js +1 -1
  11. package/dist/auth-mfa/hmac-token-codec.d.ts +1 -1
  12. package/dist/auth-mfa/hmac-token-codec.js +1 -1
  13. package/dist/auth-mfa/mfa-challenge-token.d.ts +1 -1
  14. package/dist/auth-mfa/mfa-challenge-token.js +1 -1
  15. package/dist/auth-mfa/mfa-preauth-setup-token.d.ts +1 -1
  16. package/dist/auth-mfa/mfa-preauth-setup-token.js +1 -1
  17. package/dist/auth-mfa/mfa-setup-token.d.ts +1 -1
  18. package/dist/auth-mfa/mfa-setup-token.js +1 -1
  19. package/dist/billing-foundation/constants.d.ts +2 -2
  20. package/dist/billing-foundation/constants.js +3 -12
  21. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +1 -1
  22. package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
  23. package/dist/billing-foundation/feature.js +2 -9
  24. package/dist/billing-foundation/handlers/sync-subscription.write.js +3 -6
  25. package/dist/cap-counter/book-cap-usage.d.ts +5 -1
  26. package/dist/cap-counter/book-cap-usage.js +66 -45
  27. package/dist/cap-counter/cap-reservation.d.ts +12 -0
  28. package/dist/cap-counter/cap-reservation.js +135 -0
  29. package/dist/cap-counter/constants.d.ts +3 -0
  30. package/dist/cap-counter/constants.js +6 -0
  31. package/dist/cap-counter/enforce-cap.d.ts +1 -1
  32. package/dist/cap-counter/enforce-cap.js +1 -8
  33. package/dist/cap-counter/feature.js +7 -2
  34. package/dist/cap-counter/handlers/increment-rolling.write.d.ts +5 -0
  35. package/dist/cap-counter/handlers/increment-rolling.write.js +4 -0
  36. package/dist/cap-counter/index.d.ts +4 -2
  37. package/dist/cap-counter/index.js +3 -1
  38. package/dist/cap-counter/payload-field.d.ts +1 -0
  39. package/dist/cap-counter/payload-field.js +11 -0
  40. package/dist/cap-counter/reservation-chain.d.ts +2 -0
  41. package/dist/cap-counter/reservation-chain.js +44 -0
  42. package/dist/cap-counter/stock-cap-guard.d.ts +3 -0
  43. package/dist/cap-counter/tables.d.ts +3 -0
  44. package/dist/cap-counter/tables.js +36 -0
  45. package/dist/cap-counter/value-cap-guard.d.ts +18 -0
  46. package/dist/cap-counter/value-cap-guard.js +33 -0
  47. package/dist/cap-counter/with-cap-enforcement.d.ts +5 -7
  48. package/dist/cap-counter/with-cap-enforcement.js +35 -51
  49. package/dist/channel-discord/feature.js +1 -0
  50. package/dist/channel-slack/feature.js +1 -0
  51. package/dist/channel-teams/feature.js +1 -0
  52. package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -0
  53. package/dist/delivery/attempt-log-retention.d.ts +9 -0
  54. package/dist/delivery/attempt-log-retention.js +23 -0
  55. package/dist/delivery/attempt-log.d.ts +1 -1
  56. package/dist/delivery/attempt-log.js +21 -2
  57. package/dist/delivery/constants.d.ts +1 -1
  58. package/dist/delivery/constants.js +1 -1
  59. package/dist/delivery/events.d.ts +1 -0
  60. package/dist/delivery/events.js +2 -0
  61. package/dist/delivery/feature.d.ts +1 -0
  62. package/dist/delivery/feature.js +39 -3
  63. package/dist/delivery/handlers/log.query.d.ts +1 -2
  64. package/dist/delivery/handlers/log.query.js +1 -1
  65. package/dist/delivery/index.d.ts +2 -1
  66. package/dist/delivery/index.js +2 -1
  67. package/dist/delivery/mask-recipient-address.d.ts +1 -0
  68. package/dist/delivery/mask-recipient-address.js +41 -0
  69. package/dist/delivery/public-names.d.ts +2 -0
  70. package/dist/delivery/public-names.js +3 -0
  71. package/dist/delivery/types.d.ts +1 -0
  72. package/dist/delivery/types.js +6 -2
  73. package/dist/feature-toggles/handlers/set.write.js +1 -1
  74. package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
  75. package/dist/form-draft/db/queries/cleanup.js +1 -1
  76. package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
  77. package/dist/form-draft/lookup.d.ts +1 -1
  78. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -1
  79. package/dist/inbound-mail-foundation/oauth-access-token.d.ts +1 -1
  80. package/dist/inbound-mail-foundation/oauth-access-token.js +1 -1
  81. package/dist/inbound-mail-foundation/oauth-state.d.ts +1 -1
  82. package/dist/inbound-mail-foundation/oauth-state.js +1 -1
  83. package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
  84. package/dist/inbound-provider-imap/feature.js +1 -1
  85. package/dist/inbound-provider-imap/imap-client.js +1 -1
  86. package/dist/jobs/job-instant.d.ts +1 -1
  87. package/dist/jobs/job-instant.js +1 -1
  88. package/dist/ledger/entity.d.ts +3 -0
  89. package/dist/metrics/compute.d.ts +1 -1
  90. package/dist/metrics/feature.js +1 -1
  91. package/dist/metrics/types.d.ts +1 -1
  92. package/dist/metrics/window.d.ts +1 -1
  93. package/dist/metrics/window.js +1 -1
  94. package/dist/personal-access-tokens/expiry.js +1 -1
  95. package/dist/personal-access-tokens/handlers/create.write.js +1 -1
  96. package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
  97. package/dist/personal-access-tokens/revoke-for-user.js +1 -1
  98. package/dist/renderer-foundation/types.d.ts +1 -0
  99. package/dist/renderer-simple/feature.js +4 -6
  100. package/dist/renderer-simple/index.d.ts +1 -0
  101. package/dist/renderer-simple/mail-blocks.d.ts +21 -0
  102. package/dist/renderer-simple/mail-blocks.js +107 -0
  103. package/dist/renderer-simple/simple-renderer.js +21 -3
  104. package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
  105. package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
  106. package/dist/sessions/handlers/revoke.write.js +1 -1
  107. package/dist/sessions/session-callbacks.js +1 -1
  108. package/dist/shared/grace-period.d.ts +2 -1
  109. package/dist/shared/grace-period.js +3 -7
  110. package/dist/shared/row-bound-grant.d.ts +1 -1
  111. package/dist/shared/signed-token.d.ts +1 -1
  112. package/dist/shared/signed-token.js +1 -1
  113. package/dist/subscription-mollie/verify-webhook.js +1 -1
  114. package/dist/subscription-stripe/verify-webhook.js +1 -1
  115. package/dist/template-resolver/api.d.ts +1 -1
  116. package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
  117. package/dist/template-resolver/handlers/by-tenant.query.d.ts +2 -2
  118. package/dist/template-resolver/handlers/collection-item.query.d.ts +1 -1
  119. package/dist/template-resolver/handlers/collection-list.query.d.ts +1 -1
  120. package/dist/template-resolver/handlers/collection-shared.d.ts +2 -2
  121. package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
  122. package/dist/template-resolver/handlers/list.query.d.ts +1 -1
  123. package/dist/template-resolver/table.d.ts +9 -1
  124. package/dist/template-resolver/user-content-table.d.ts +4 -0
  125. package/dist/tenant/handlers/team-list.query.d.ts +1 -1
  126. package/dist/tenant/handlers/team-list.query.js +1 -1
  127. package/dist/tenant/invitation-table.d.ts +2 -0
  128. package/dist/tenant/invited-membership.d.ts +1 -1
  129. package/dist/tenant/invited-membership.js +1 -1
  130. package/dist/tenant/schema/tenant.d.ts +2 -0
  131. package/dist/tenant-handover/grant.d.ts +1 -1
  132. package/dist/tier-engine/index.d.ts +1 -0
  133. package/dist/tier-engine/index.js +1 -0
  134. package/dist/tier-engine/tier-option-gate.d.ts +32 -0
  135. package/dist/tier-engine/tier-option-gate.js +62 -0
  136. package/dist/user/schema/user.d.ts +2 -0
  137. package/dist/user-data-rights/db/queries/export-jobs.d.ts +1 -1
  138. package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
  139. package/dist/user-data-rights/db/queries/forget-cleanup.js +2 -8
  140. package/dist/user-data-rights/deletion-token.d.ts +1 -1
  141. package/dist/user-data-rights/email-templates.js +1 -1
  142. package/dist/user-data-rights/handlers/list-download-attempts.query.js +1 -1
  143. package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
  144. package/dist/user-data-rights/schema/export-job.d.ts +2 -0
  145. package/dist/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +25 -6
  146. package/dist/waitlist/entity.d.ts +2 -0
  147. package/dist/waitlist/handlers/invite.write.js +1 -1
  148. package/dist/waitlist/handlers/submit.write.js +1 -1
  149. package/package.json +10 -10
  150. package/src/cap-counter/changes.json +28 -0
  151. package/src/delivery/changes.json +12 -0
  152. package/src/derivatives-sharp/changes.json +6 -0
  153. package/src/legal-pages/changes.json +6 -0
  154. package/src/renderer-simple/changes.json +6 -0
  155. package/src/tier-engine/changes.json +6 -0
@@ -16,6 +16,12 @@ export const CAP_COUNTER_ROLLING_AGGREGATE_TYPE = "cap-counter-rolling";
16
16
  // Beide MÜSSEN konsistent sein (drift-pin im feature-test).
17
17
  export const ROLLING_INCREMENTED_EVENT_SHORT = "rolling-incremented";
18
18
  export const ROLLING_INCREMENTED_EVENT_QN = "cap-counter:event:rolling-incremented";
19
+ // Gives back a reserved rolling amount; the window sum is incremented minus released.
20
+ export const ROLLING_RELEASED_EVENT_SHORT = "rolling-released";
21
+ export const ROLLING_RELEASED_EVENT_QN = "cap-counter:event:rolling-released";
22
+ // A reservation row older than this is treated as a lost release and given back before the next
23
+ // reservation for the same cap, which bounds the over-count a failed release can leave behind.
24
+ export const CAP_RESERVATION_TTL_MINUTES = 60;
19
25
  // Qualified write handler names (QN format: scope:type:name).
20
26
  export const CapCounterHandlers = {
21
27
  increment: "cap-counter:write:increment",
@@ -1,6 +1,6 @@
1
- /// <reference types="temporal-polyfill/global" preserve="true" />
2
1
  import { type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { KumikoError } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
4
  /**
5
5
  * Cap-Toleranz-Profile — pro Cap-Typ asymmetrisch. Quelle: Memory
6
6
  * `project_pricing_byok_caps` §3 + `docs/plans/marketing/produkt/
@@ -1,15 +1,8 @@
1
- /// <reference types="temporal-polyfill/global" preserve="true" />
2
1
  import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { KumikoError, reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
4
  import { markCapSoftWarned, readRollingCapUsage } from "./book-cap-usage.js";
5
5
  import { capCounterEntity } from "./entity.js";
6
- // Temporal globally provided by the framework's polyfill init
7
- // (ensureTemporalPolyfill() in time/polyfill.ts, called from
8
- // setupTestStack/boot). Importing from "temporal-polyfill" gives us
9
- // the polyfill-package types which don't quite match drizzle's
10
- // `instant()`-customType (temporal-spec narrowing of `until(...).sign`).
11
- // Mirror the audit-handler pattern: rely on the global ambient
12
- // declaration from temporal-spec.
13
6
  const { table } = createEntityExecutor("cap-counter", capCounterEntity);
14
7
  export const CAP_TOLERANCES = {
15
8
  /** Mails / Tokens — billig, BYOK ab Pro. Burst-Buffer großzügig. */
@@ -45,13 +45,14 @@
45
45
  // config, kein secrets, kein tenant-feature nötig. Tenant-Scoping kommt
46
46
  // vom Framework-Default (Base-Column tenantId).
47
47
  import { access, defineEntityListHandler, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
48
- import { CAP_COUNTER_FEATURE, CAP_COUNTER_LIST_SCREEN_ID, ROLLING_INCREMENTED_EVENT_SHORT, } from "./constants.js";
48
+ import { CAP_COUNTER_FEATURE, CAP_COUNTER_LIST_SCREEN_ID, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
49
49
  import { capCounterEntity } from "./entity.js";
50
50
  import { getCounterQuery } from "./handlers/get-counter.query.js";
51
51
  import { incrementCapHandler } from "./handlers/increment.write.js";
52
- import { incrementRollingCapHandler, rollingIncrementedSchema, } from "./handlers/increment-rolling.write.js";
52
+ import { incrementRollingCapHandler, rollingIncrementedSchema, rollingReleasedSchema, } from "./handlers/increment-rolling.write.js";
53
53
  import { markSoftWarnedHandler } from "./handlers/mark-soft-warned.write.js";
54
54
  import { CAP_COUNTER_I18N } from "./i18n.js";
55
+ import { capReservationsTableMeta } from "./tables.js";
55
56
  export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
56
57
  r.describe("Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection). Use `enforceCap` / `enforceRollingCap` (or the `withCapEnforcement` / `withRollingCapEnforcement` handler wrappers) in your write-handlers to check limits with soft-warn and hard-block tolerances; call `enforceCapAndMaybeNotify` when you also want to trigger a delivery notification on soft-threshold hits.");
57
58
  r.uiHints({
@@ -60,12 +61,16 @@ export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
60
61
  recommended: false,
61
62
  });
62
63
  r.entity("cap-counter", capCounterEntity);
64
+ r.storeTable(capReservationsTableMeta, {
65
+ reason: "coordination.cap_reservation_ledger",
66
+ });
63
67
  // Custom Domain-Event für Rolling-Counter. r.defineEvent registriert
64
68
  // das Schema beim Registry; ctx.unsafeAppendEvent im Handler nutzt
65
69
  // dasselbe Schema für Append-Time-Validation. QN nach Prefixing:
66
70
  // "cap-counter:event:rolling-incremented" (siehe
67
71
  // ROLLING_INCREMENTED_EVENT_QN).
68
72
  r.defineEvent(ROLLING_INCREMENTED_EVENT_SHORT, rollingIncrementedSchema, { piiFields: "none" });
73
+ r.defineEvent(ROLLING_RELEASED_EVENT_SHORT, rollingReleasedSchema, { piiFields: "none" });
69
74
  // Custom write-handlers.
70
75
  // - increment: Calendar-Period (CRUD via projection-row).
71
76
  // - increment-rolling: Rolling-Window (Custom-Event, no projection).
@@ -6,4 +6,9 @@ export declare const rollingIncrementedSchema: z.ZodObject<{
6
6
  capName: z.ZodString;
7
7
  amount: z.ZodDefault<z.ZodNumber>;
8
8
  }, z.core.$strip>;
9
+ /** Payload of the event that gives a reserved rolling amount back. */
10
+ export declare const rollingReleasedSchema: z.ZodObject<{
11
+ capName: z.ZodString;
12
+ amount: z.ZodNumber;
13
+ }, z.core.$strip>;
9
14
  export declare const incrementRollingCapHandler: WriteHandlerDef;
@@ -31,6 +31,10 @@ const incrementRollingSchema = z.object({
31
31
  /** Schema des emittierten Custom-Events. Identisch zum Input-Schema:
32
32
  * der Caller zahlt amount, wir hängen es 1:1 an den Stream. */
33
33
  export const rollingIncrementedSchema = incrementRollingSchema;
34
+ /** Payload of the event that gives a reserved rolling amount back. */
35
+ export const rollingReleasedSchema = incrementRollingSchema.extend({
36
+ amount: z.number().int().positive(),
37
+ });
34
38
  // Rolling-Increment-Handler — append-only. Race-frei: zwei parallele
35
39
  // Increments für (tenant, cap) hängen sich am selben aggregate-stream
36
40
  // in unterschiedlichen versions auf, das event-store ordert.
@@ -1,8 +1,10 @@
1
1
  export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
2
2
  export { type BookCapUsageOptions, bookCapUsage, type MarkCapSoftWarnedOptions, markCapSoftWarned, type ReadRollingCapUsageOptions, readRollingCapUsage, } from "./book-cap-usage.js";
3
- export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, } from "./constants.js";
3
+ export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
4
4
  export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
5
5
  export { capCounterEntity } from "./entity.js";
6
6
  export { capCounterFeature } from "./feature.js";
7
- export { type CapLimitContext, createStockCapGuard, type StockCapGuard, type StockCapSpec, } from "./stock-cap-guard.js";
7
+ export { readPayloadField } from "./payload-field.js";
8
+ export { type CapGuardContext, type CapLimitContext, createStockCapGuard, type StockCapGuard, type StockCapSpec, } from "./stock-cap-guard.js";
9
+ export { createValueCapGuard, type ValueCapGuard, type ValueCapSpec, } from "./value-cap-guard.js";
8
10
  export { type CalendarCapDef, type CalendarCapResolver, type RollingCapDef, type RollingCapResolver, withCapEnforcement, withRollingCapEnforcement, } from "./with-cap-enforcement.js";
@@ -1,9 +1,11 @@
1
1
  // Public API of the cap-counter bundled-feature.
2
2
  export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
3
3
  export { bookCapUsage, markCapSoftWarned, readRollingCapUsage, } from "./book-cap-usage.js";
4
- export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, } from "./constants.js";
4
+ export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
5
5
  export { CAP_TOLERANCES, CapExceededError, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
6
6
  export { capCounterEntity } from "./entity.js";
7
7
  export { capCounterFeature } from "./feature.js";
8
+ export { readPayloadField } from "./payload-field.js";
8
9
  export { createStockCapGuard, } from "./stock-cap-guard.js";
10
+ export { createValueCapGuard, } from "./value-cap-guard.js";
9
11
  export { withCapEnforcement, withRollingCapEnforcement, } from "./with-cap-enforcement.js";
@@ -0,0 +1 @@
1
+ export declare function readPayloadField(payload: unknown, field: string): unknown;
@@ -0,0 +1,11 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-framework/utils";
2
+ // Create payloads are flat, entity update payloads carry the edited fields under `changes`
3
+ // (`{ id, version, changes }`), so a guard that follows one field has to look at both.
4
+ export function readPayloadField(payload, field) {
5
+ if (!isPlainObject(payload))
6
+ return undefined;
7
+ if (field in payload)
8
+ return payload[field];
9
+ const { changes } = payload;
10
+ return isPlainObject(changes) ? changes[field] : undefined;
11
+ }
@@ -0,0 +1,2 @@
1
+ import type { PreTransactionReservation } from "@cosmicdrift/kumiko-types/handlers";
2
+ export declare function chainReservations(inner: PreTransactionReservation | undefined, own: PreTransactionReservation): PreTransactionReservation;
@@ -0,0 +1,44 @@
1
+ function toHandle(outcome) {
2
+ if (outcome === undefined)
3
+ return { release: async () => { }, confirmInTransaction: async () => { } };
4
+ if (typeof outcome === "function") {
5
+ return { release: outcome, confirmInTransaction: async () => { } };
6
+ }
7
+ return outcome;
8
+ }
9
+ // Runs `inner` first, then `own`; a failing `own` gives the inner reservation back before it
10
+ // throws. Release goes in reverse order, confirm covers both.
11
+ export function chainReservations(inner, own) {
12
+ if (!inner)
13
+ return own;
14
+ return async (event, ctx) => {
15
+ const innerHandle = toHandle(await inner(event, ctx));
16
+ let ownHandle;
17
+ try {
18
+ ownHandle = toHandle(await own(event, ctx));
19
+ }
20
+ catch (error) {
21
+ try {
22
+ await innerHandle.release();
23
+ }
24
+ catch (releaseError) {
25
+ ctx.log?.error("releasing the inner cap reservation failed", { error: releaseError });
26
+ }
27
+ throw error;
28
+ }
29
+ return {
30
+ release: async () => {
31
+ try {
32
+ await ownHandle.release();
33
+ }
34
+ finally {
35
+ await innerHandle.release();
36
+ }
37
+ },
38
+ confirmInTransaction: async (confirm) => {
39
+ await innerHandle.confirmInTransaction(confirm);
40
+ await ownHandle.confirmInTransaction(confirm);
41
+ },
42
+ };
43
+ };
44
+ }
@@ -4,6 +4,9 @@ import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  export type CapLimitContext = {
5
5
  readonly config?: ConfigAccessor;
6
6
  };
7
+ export type CapGuardContext = CapLimitContext & {
8
+ readonly db: TenantDb;
9
+ };
7
10
  export type StockCapSpec<TCaps> = {
8
11
  readonly table: SchemaTable | EntityTableMeta;
9
12
  readonly limit: (caps: TCaps) => number;
@@ -0,0 +1,3 @@
1
+ import { type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
2
+ export declare const capReservationsTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
3
+ export declare const capReservationsTableMeta: EntityTableMeta;
@@ -0,0 +1,36 @@
1
+ import { defineUnmanagedTable, index, instant, integer, table as pgTable, text, uuid, } from "@cosmicdrift/kumiko-framework/db";
2
+ const CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX = "store_cap_reservations_tenant_id_cap_name_expires_at_idx";
3
+ // One row per capacity that was booked before a handler transaction and is not settled yet. The
4
+ // handler transaction deletes it on its own commit (confirm); a release or the TTL sweep deletes it
5
+ // together with the counter decrement. Unmanaged: the rows are a coordination aid, not state with
6
+ // an event history.
7
+ export const capReservationsTable = pgTable("store_cap_reservations", {
8
+ id: uuid("id").primaryKey(),
9
+ tenantId: uuid("tenant_id").notNull(),
10
+ capName: text("cap_name").notNull(),
11
+ kind: text("kind").notNull().$type(),
12
+ // Calendar only: raw string the counter's aggregate id was derived from; a normalized instant could point at another counter.
13
+ periodStartIso: text("period_start_iso"),
14
+ amount: integer("amount").notNull(),
15
+ expiresAt: instant("expires_at").notNull(),
16
+ createdAt: instant("created_at").notNull(),
17
+ }, (t) => [index(CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX).on(t.tenantId, t.capName, t.expiresAt)]);
18
+ export const capReservationsTableMeta = defineUnmanagedTable({
19
+ tableName: "store_cap_reservations",
20
+ indexes: [
21
+ {
22
+ name: CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX,
23
+ columns: ["tenant_id", "cap_name", "expires_at"],
24
+ },
25
+ ],
26
+ columns: [
27
+ { name: "id", pgType: "uuid", notNull: true, primaryKey: true },
28
+ { name: "tenant_id", pgType: "uuid", notNull: true },
29
+ { name: "cap_name", pgType: "text", notNull: true },
30
+ { name: "kind", pgType: "text", notNull: true },
31
+ { name: "period_start_iso", pgType: "text", notNull: false },
32
+ { name: "amount", pgType: "integer", notNull: true },
33
+ { name: "expires_at", pgType: "timestamptz", notNull: true },
34
+ { name: "created_at", pgType: "timestamptz", notNull: true },
35
+ ],
36
+ });
@@ -0,0 +1,18 @@
1
+ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import type { CapGuardContext, CapLimitContext } from "./stock-cap-guard.js";
5
+ export type ValueCapSpec<TCaps> = {
6
+ readonly field: string;
7
+ /** `undefined` = no lower bound for this tier. */
8
+ readonly min?: (caps: TCaps) => number | undefined;
9
+ /** `undefined` = no upper bound for this tier. */
10
+ readonly max?: (caps: TCaps) => number | undefined;
11
+ readonly code: string;
12
+ readonly i18nKey: string;
13
+ };
14
+ export type ValueCapGuard<TCaps> = {
15
+ readonly checkValueCap: (ctx: CapGuardContext, value: unknown, spec: ValueCapSpec<TCaps>) => Promise<WriteFailure | null>;
16
+ readonly withValueCap: (handler: WriteHandlerDef, spec: ValueCapSpec<TCaps>) => WriteHandlerDef;
17
+ };
18
+ export declare function createValueCapGuard<TCaps>(resolveTierCaps: (db: TenantDb, context: CapLimitContext) => Promise<TCaps>): ValueCapGuard<TCaps>;
@@ -0,0 +1,33 @@
1
+ // Per-tier bounds on a single written number (e.g. a retention period or a page size),
2
+ // next to the stock-cap guard, which bounds how many rows may exist.
3
+ import { UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
4
+ import { readPayloadField } from "./payload-field.js";
5
+ function isOutOfBounds(value, min, max) {
6
+ return (min !== undefined && value < min) || (max !== undefined && value > max);
7
+ }
8
+ export function createValueCapGuard(resolveTierCaps) {
9
+ async function checkValueCap(ctx, value, spec) {
10
+ // skip: an absent or non-numeric value is the schema's business, not a cap violation.
11
+ if (typeof value !== "number")
12
+ return null;
13
+ const caps = await resolveTierCaps(ctx.db, ctx.config ? { config: ctx.config } : {});
14
+ const min = spec.min?.(caps);
15
+ const max = spec.max?.(caps);
16
+ if (!isOutOfBounds(value, min, max))
17
+ return null;
18
+ return writeFailure(new UnprocessableError(spec.code, {
19
+ i18nKey: spec.i18nKey,
20
+ details: { field: spec.field, value, min, max },
21
+ }));
22
+ }
23
+ function withValueCap(handler, spec) {
24
+ return {
25
+ ...handler,
26
+ handler: async (event, ctx) => {
27
+ const failure = await checkValueCap(ctx, readPayloadField(event.payload, spec.field), spec);
28
+ return failure ?? handler.handler(event, ctx);
29
+ },
30
+ };
31
+ }
32
+ return { checkValueCap, withValueCap };
33
+ }
@@ -44,13 +44,11 @@ export type RollingCapResolver = (event: WriteEvent, ctx: HandlerContext) => Pro
44
44
  /**
45
45
  * Wrap a write-handler with rolling-window cap-enforcement.
46
46
  *
47
- * Same flow as `withCapEnforcement` but uses
48
- * `enforceRollingCapAndMaybeNotify` + dispatches
49
- * `cap-counter:write:increment-rolling` post-success.
47
+ * Same flow as `withCapEnforcement` but uses `enforceRollingCapAndMaybeNotify` and reserves by
48
+ * appending a `rolling-incremented` event before the handler transaction; a release appends
49
+ * `rolling-released`.
50
50
  *
51
- * **Notification-Storm-Caveat:** rolling-counter trackt KEIN
52
- * lastSoftWarnedAt — der Notifier feuert bei JEDEM Call solange
53
- * der counter im soft-Bereich ist. Caller sollte einen TTL-Cache
54
- * (`Map<capName, lastNotifiedAt>`) im notify-callback einbauen.
51
+ * Rolling counters track no lastSoftWarnedAt, so the notifier fires on every call while usage
52
+ * sits in the soft range; throttle inside the notify callback (e.g. a per-capName TTL cache).
55
53
  */
56
54
  export declare function withRollingCapEnforcement(handler: WriteHandlerDef, capResolver: RollingCapResolver): WriteHandlerDef;
@@ -6,26 +6,26 @@
6
6
  // the wrapper keeps the pattern explicit and co-located.
7
7
  //
8
8
  // **Calendar reservation:** the wrapper only declares the cap; the dispatcher runs the
9
- // reservation (reserveBeforeTransaction) before the handler transaction opens, in its own short
10
- // committed write, and releases it after that transaction ended without committing (rollback,
11
- // failure result, failed COMMIT). No connection is held across the handler, so capped requests
12
- // cannot exhaust the pool, and the counter stream is not locked while the handler runs. If a
13
- // COMMIT fails with an unknown outcome the release can under-count; a wrapped handler must not
14
- // book the same counter itself. Reached through a nested ctx.write the cap is not reserved, so
15
- // the dispatcher rejects that call unless the top-level batch reserved the same handler.
9
+ // reservation (reserveBeforeTransaction) after all pre-handler gates and before the handler
10
+ // transaction opens, in its own short committed write (counter increment plus a
11
+ // store_cap_reservations row). The handler transaction deletes the row on its own commit; a release
12
+ // after a rollback, failure result or failed COMMIT only gives back what is still booked, so an
13
+ // unknown COMMIT outcome cannot under-count. No connection is held across the handler. Rows a
14
+ // crashed process left behind expire after CAP_RESERVATION_TTL_MINUTES and are given back before the
15
+ // next reserve of the same cap. Reached through a nested ctx.write the cap is not reserved, so the
16
+ // dispatcher rejects that call unless the top-level batch reserved the same handler.
16
17
  //
17
- // Rolling booking still dispatches the SystemAdmin-only increment-rolling
18
- // handler and has no reservation: it would need a compensating event type,
19
- // a changed readRollingCapUsage and a version-guarded append behind the
20
- // SystemAdmin dispatch. Rolling callers need a SystemAdmin identity until
21
- // cap-counter declares an explicit foreign-booking opt-in.
18
+ // The wrapper spreads the wrapped handler and chains its reserveBeforeTransaction: inner reserves
19
+ // first, release runs in reverse, and an outer failure gives the inner reservation back.
20
+ //
21
+ // Rolling caps reserve the same way: a version-guarded `rolling-incremented` append plus a
22
+ // reservation row, undone by a `rolling-released` event. The window sum is incremented minus released.
22
23
  //
23
24
  // No automatic markSoftWarned here — that's inside enforceCapAndMaybeNotify
24
25
  // (enforce-cap.ts).
25
- import { reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
26
- import { bookCapUsage, releaseCapUsage } from "./book-cap-usage.js";
27
- import { CapCounterHandlers } from "./constants.js";
26
+ import { releaseExpiredCapReservations, reserveCalendarCap, reserveRollingCap, } from "./cap-reservation.js";
28
27
  import { assertBelowHardCap, enforceCapAndMaybeNotify, enforceRollingCapAndMaybeNotify, } from "./enforce-cap.js";
28
+ import { chainReservations } from "./reservation-chain.js";
29
29
  /**
30
30
  * Wrap a write-handler with calendar-period cap-enforcement.
31
31
  *
@@ -39,8 +39,9 @@ import { assertBelowHardCap, enforceCapAndMaybeNotify, enforceRollingCapAndMaybe
39
39
  export function withCapEnforcement(handler, capResolver) {
40
40
  return {
41
41
  ...handler,
42
- reserveBeforeTransaction: async (event, ctx) => {
42
+ reserveBeforeTransaction: chainReservations(handler.reserveBeforeTransaction, async (event, ctx) => {
43
43
  const cap = await capResolver(event, ctx);
44
+ await releaseExpiredCapReservations(ctx, cap.capName);
44
45
  await enforceCapAndMaybeNotify(ctx, {
45
46
  capName: cap.capName,
46
47
  periodStartIso: cap.periodStartIso,
@@ -51,47 +52,31 @@ export function withCapEnforcement(handler, capResolver) {
51
52
  ...(cap.amount !== undefined && { amount: cap.amount }),
52
53
  });
53
54
  const amount = cap.amount ?? 1;
54
- const reserved = await bookCapUsage(ctx, {
55
+ return reserveCalendarCap(ctx, {
55
56
  capName: cap.capName,
56
57
  amount,
57
58
  periodStartIso: cap.periodStartIso,
58
- outsideTransaction: true,
59
59
  guardCurrentValue: (currentValue) => assertBelowHardCap(currentValue + amount - 1, cap),
60
60
  });
61
- if (!reserved.isSuccess)
62
- throw reraiseAsKumikoError(reserved.error);
63
- return async () => {
64
- const released = await releaseCapUsage(ctx, {
65
- capName: cap.capName,
66
- amount,
67
- periodStartIso: cap.periodStartIso,
68
- outsideTransaction: true,
69
- });
70
- if (!released.isSuccess)
71
- throw reraiseAsKumikoError(released.error);
72
- };
73
- },
61
+ }),
74
62
  };
75
63
  }
76
64
  /**
77
65
  * Wrap a write-handler with rolling-window cap-enforcement.
78
66
  *
79
- * Same flow as `withCapEnforcement` but uses
80
- * `enforceRollingCapAndMaybeNotify` + dispatches
81
- * `cap-counter:write:increment-rolling` post-success.
67
+ * Same flow as `withCapEnforcement` but uses `enforceRollingCapAndMaybeNotify` and reserves by
68
+ * appending a `rolling-incremented` event before the handler transaction; a release appends
69
+ * `rolling-released`.
82
70
  *
83
- * **Notification-Storm-Caveat:** rolling-counter trackt KEIN
84
- * lastSoftWarnedAt — der Notifier feuert bei JEDEM Call solange
85
- * der counter im soft-Bereich ist. Caller sollte einen TTL-Cache
86
- * (`Map<capName, lastNotifiedAt>`) im notify-callback einbauen.
71
+ * Rolling counters track no lastSoftWarnedAt, so the notifier fires on every call while usage
72
+ * sits in the soft range; throttle inside the notify callback (e.g. a per-capName TTL cache).
87
73
  */
88
74
  export function withRollingCapEnforcement(handler, capResolver) {
89
75
  return {
90
- name: handler.name,
91
- schema: handler.schema,
92
- access: handler.access,
93
- handler: async (event, ctx) => {
76
+ ...handler,
77
+ reserveBeforeTransaction: chainReservations(handler.reserveBeforeTransaction, async (event, ctx) => {
94
78
  const cap = await capResolver(event, ctx);
79
+ await releaseExpiredCapReservations(ctx, cap.capName);
95
80
  await enforceRollingCapAndMaybeNotify(ctx, {
96
81
  capName: cap.capName,
97
82
  windowDays: cap.windowDays,
@@ -99,14 +84,13 @@ export function withRollingCapEnforcement(handler, capResolver) {
99
84
  profile: cap.profile,
100
85
  notify: cap.notify,
101
86
  });
102
- const result = await handler.handler(event, ctx);
103
- if (result.isSuccess) {
104
- await ctx.write(CapCounterHandlers.incrementRolling, {
105
- capName: cap.capName,
106
- amount: cap.amount ?? 1,
107
- });
108
- }
109
- return result;
110
- },
87
+ const amount = cap.amount ?? 1;
88
+ return reserveRollingCap(ctx, {
89
+ capName: cap.capName,
90
+ windowDays: cap.windowDays,
91
+ amount,
92
+ guardCurrentValue: (usage) => assertBelowHardCap(usage + amount - 1, cap),
93
+ });
94
+ }),
111
95
  };
112
96
  }
@@ -30,6 +30,7 @@ export function createChannelDiscordFeature(options = {}) {
30
30
  r.translations({ keys: { "delivery.channel.discord": { en: "Discord" } } });
31
31
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "discord", {
32
32
  mode: channel.mode,
33
+ addressKind: "connection-name",
33
34
  send: channel.send,
34
35
  });
35
36
  });
@@ -27,6 +27,7 @@ export function createChannelSlackFeature(options = {}) {
27
27
  r.translations({ keys: { "delivery.channel.slack": { en: "Slack" } } });
28
28
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "slack", {
29
29
  mode: channel.mode,
30
+ addressKind: "connection-name",
30
31
  send: channel.send,
31
32
  });
32
33
  });
@@ -31,6 +31,7 @@ export function createChannelTeamsFeature(options = {}) {
31
31
  r.translations({ keys: { "delivery.channel.teams": { en: "Microsoft Teams" } } });
32
32
  r.useExtension(DELIVERY_CHANNEL_EXTENSION, "teams", {
33
33
  mode: channel.mode,
34
+ addressKind: "connection-name",
34
35
  send: channel.send,
35
36
  });
36
37
  });
@@ -18,6 +18,7 @@ export declare const tenantComplianceProfileEntity: import("@cosmicdrift/kumiko-
18
18
  } | undefined;
19
19
  readonly optionsQuery?: string;
20
20
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
21
+ readonly optionsAvailabilityQuery?: string;
21
22
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
22
23
  required: true;
23
24
  };
@@ -58,6 +59,7 @@ export declare const tenantComplianceProfileTable: import("@cosmicdrift/kumiko-f
58
59
  } | undefined;
59
60
  readonly optionsQuery?: string;
60
61
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
62
+ readonly optionsAvailabilityQuery?: string;
61
63
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
62
64
  required: true;
63
65
  };
@@ -0,0 +1,9 @@
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import { type PruneEventsOptions } from "@cosmicdrift/kumiko-framework/pipeline";
3
+ export declare const DEFAULT_ATTEMPT_LOG_RETENTION_DAYS = 90;
4
+ export type AttemptLogRetentionOptions = Pick<PruneEventsOptions, "olderThan" | "olderThanDays">;
5
+ export declare function resolveAttemptLogRetentionDays(option: number | false | undefined): number | undefined;
6
+ export declare function runAttemptLogRetention(db: DbConnection, options: AttemptLogRetentionOptions): Promise<{
7
+ readonly deletedEvents: number;
8
+ readonly deletedRows: number;
9
+ }>;
@@ -0,0 +1,23 @@
1
+ import { deleteManyBatched } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { pruneEvents } from "@cosmicdrift/kumiko-framework/pipeline";
3
+ import { deliveryAttemptsTable } from "./tables.js";
4
+ export const DEFAULT_ATTEMPT_LOG_RETENTION_DAYS = 90;
5
+ const ROW_DELETE_BATCH_SIZE = 1000;
6
+ export function resolveAttemptLogRetentionDays(option) {
7
+ if (option === false)
8
+ return undefined;
9
+ const days = option ?? DEFAULT_ATTEMPT_LOG_RETENTION_DAYS;
10
+ if (!Number.isInteger(days) || days <= 0) {
11
+ throw new Error(`delivery: attemptLogRetentionDays must be a positive integer or false, got ${String(option)}`);
12
+ }
13
+ return days;
14
+ }
15
+ // Prunes the attempt events first and then the projection rows older than the same cutoff: a
16
+ // rebuild would not recreate rows whose events are gone, so they would otherwise stay forever.
17
+ // A lagging event consumer makes pruneEvents throw ConsumerLagError before anything is deleted;
18
+ // the caller retries on its next run.
19
+ export async function runAttemptLogRetention(db, options) {
20
+ const pruned = await pruneEvents(db, { ...options, aggregateTypes: ["deliveryAttempt"] });
21
+ const rows = await deleteManyBatched(db, deliveryAttemptsTable, { createdAt: { lt: pruned.cutoff } }, { limit: ROW_DELETE_BATCH_SIZE });
22
+ return { deletedEvents: pruned.deletedCount, deletedRows: rows.deleted };
23
+ }
@@ -1,5 +1,5 @@
1
1
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
2
  import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
3
- import type { DeliveryLogEntry } from "./types.js";
3
+ import { type DeliveryLogEntry } from "./types.js";
4
4
  export declare function appendAttemptEvent(db: DbConnection, registry: Registry, attemptId: string, entry: DeliveryLogEntry): Promise<void>;
5
5
  export declare function logAttempt(db: DbConnection, registry: Registry, entry: DeliveryLogEntry): Promise<string>;
@@ -5,16 +5,35 @@
5
5
  import { append, getStreamVersion } from "@cosmicdrift/kumiko-framework/event-store";
6
6
  import { runProjectionsForEvent } from "@cosmicdrift/kumiko-framework/pipeline";
7
7
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
8
- import { DELIVERY_ATTEMPT_EVENT } from "./constants.js";
8
+ import { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_EXTENSION } from "./constants.js";
9
9
  import { deliveryAttemptSchema } from "./events.js";
10
+ import { maskRecipientAddress } from "./mask-recipient-address.js";
11
+ import { isDeliveryChannelPlugin } from "./types.js";
12
+ // Unknown channels count as personal: masking is the fail-closed default.
13
+ function addressIsConnectionName(registry, channelName) {
14
+ return registry
15
+ .getExtensionUsages(DELIVERY_CHANNEL_EXTENSION)
16
+ .some((usage) => usage.entityName === channelName &&
17
+ isDeliveryChannelPlugin(usage.options) &&
18
+ usage.options.addressKind === "connection-name");
19
+ }
20
+ function loggedAddress(registry, entry) {
21
+ return addressIsConnectionName(registry, entry.channel)
22
+ ? entry.recipientAddress
23
+ : maskRecipientAddress(entry.recipientAddress);
24
+ }
10
25
  // Shared append + inline-projection write (low-level append() does not
11
26
  // auto-fire projections — only the dispatcher/executor paths do).
12
27
  async function writeAttemptEvent(db, registry, attemptId, expectedVersion, entry) {
13
28
  const { tenantId, ...rest } = entry;
29
+ // Masked here, the only place attempt events are written, so no event or projection row ever holds the full address.
14
30
  // Schema-parse to match ctx.appendEvent's guarantee: a payload drift between
15
31
  // service/job + feature-registration fails loudly here instead of landing on
16
32
  // the events-table and crashing a consumer later.
17
- const payload = deliveryAttemptSchema.parse(rest);
33
+ const payload = deliveryAttemptSchema.parse({
34
+ ...rest,
35
+ recipientAddress: loggedAddress(registry, entry),
36
+ });
18
37
  const stored = await append(db, {
19
38
  aggregateId: attemptId,
20
39
  aggregateType: "deliveryAttempt",
@@ -1,6 +1,6 @@
1
1
  import type { NotifyPriority } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare const DELIVERY_CHANNEL_EXTENSION: "deliveryChannel";
3
- export { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
3
+ export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, type DeliveryStatusValue, } from "./public-names.js";
4
4
  export declare const deliveryPriorityRank: Record<NotifyPriority, number>;
5
5
  export declare const DeliveryJobs: {
6
6
  readonly render: string;
@@ -3,7 +3,7 @@ import { DELIVERY_FEATURE, DeliveryJobNames } from "./public-names.js";
3
3
  // Extension-point name for delivery-channel plugins (channel-email,
4
4
  // channel-in-app, channel-push, ...).
5
5
  export const DELIVERY_CHANNEL_EXTENSION = "deliveryChannel";
6
- export { DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
6
+ export { DELIVERY_ATTEMPT_ADDRESS_ERASED_EVENT, DELIVERY_ATTEMPT_EVENT, DELIVERY_CHANNEL_CELL_COMPONENT, DELIVERY_ERROR_CELL_COMPONENT, DELIVERY_FEATURE, DELIVERY_LOG_SCREEN_ID, DELIVERY_RESUBSCRIBE_PATH, DELIVERY_STATUS_CELL_COMPONENT, DELIVERY_TIME_CELL_COMPONENT, DELIVERY_TYPE_CELL_COMPONENT, DELIVERY_UNSUBSCRIBE_PATH, DeliveryErrors, DeliveryHandlers, DeliveryJobNames, DeliveryQueries, DeliveryStatus, } from "./public-names.js";
7
7
  // notify() priority → BullMQ job priority. Lower number = processed first; all
8
8
  // > 0 so prioritised delivery jobs never mix with BullMQ's "0 = unprioritised
9
9
  // FIFO" bucket. critical jobs jump ahead of normal/low in the worker queue.
@@ -36,3 +36,4 @@ export declare const deliveryAttemptSchema: z.ZodObject<{
36
36
  confirmed: z.ZodOptional<z.ZodLiteral<false>>;
37
37
  }, z.core.$strip>;
38
38
  export type DeliveryAttemptPayload = z.infer<typeof deliveryAttemptSchema>;
39
+ export declare const deliveryAttemptAddressErasedSchema: z.ZodObject<{}, z.core.$strip>;
@@ -28,3 +28,5 @@ export const deliveryAttemptSchema = z.object({
28
28
  // Provider accepted the request but does not confirm delivery (e.g. Teams 202).
29
29
  confirmed: z.literal(false).optional(),
30
30
  });
31
+ // No payload: the event only marks the attempt row's address as erased.
32
+ export const deliveryAttemptAddressErasedSchema = z.object({});