@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,7 +16,7 @@
16
16
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
17
17
  import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
18
18
  import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
19
- import { Temporal } from "temporal-polyfill";
19
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
20
20
  import * as z from "zod";
21
21
  import { AUDIT_SCOPE_VALUES } from "../constants.js";
22
22
  import { resolveAuditScopeFilter } from "./resolve-audit-tenant.js";
@@ -8,7 +8,7 @@
8
8
  // English ships here. Other languages register via @cosmicdrift/kumiko-locale-*
9
9
  // (localeDe() / localeEs() call registerMailTranslations).
10
10
  import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
11
- import { Temporal } from "temporal-polyfill";
11
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
12
12
  export const AUTH_MAIL_EN = {
13
13
  "auth.mail.appNameDefault": "Account",
14
14
  "auth.mail.reset.subject": "{app} — Reset your password",
@@ -20,7 +20,7 @@ import { generateToken } from "@cosmicdrift/kumiko-framework/api";
20
20
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
21
21
  import { access, assignableAppRolesOf, defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
22
22
  import { InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
23
- import { Temporal } from "temporal-polyfill";
23
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
24
24
  import * as z from "zod";
25
25
  // kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link-Pattern), DB-row-owner ist tenant-feature
26
26
  import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../../tenant/invitation-table.js";
@@ -23,7 +23,7 @@
23
23
  import { generateToken } from "@cosmicdrift/kumiko-framework/api";
24
24
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
25
25
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
26
- import { Temporal } from "temporal-polyfill";
26
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
27
27
  import * as z from "zod";
28
28
  import { findSignupHandoverProvider } from "../../shared/index.js";
29
29
  import { AUTH_SIGNUP_DEFAULT_TTL_MINUTES } from "../constants.js";
@@ -1,4 +1,4 @@
1
- import type { Temporal } from "temporal-polyfill";
1
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  import * as z from "zod";
3
3
  import { type AuthUserRow } from "../auth-user-row.js";
4
4
  import type { AuthMailContent, AuthMailLocale, RenderTokenContentArgs } from "../email-templates.js";
@@ -1,4 +1,4 @@
1
- import type { Temporal } from "temporal-polyfill";
1
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  export type VerifyResult = {
3
3
  readonly ok: true;
4
4
  readonly userId: string;
@@ -1,4 +1,4 @@
1
- import type { Temporal } from "temporal-polyfill";
1
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  export type VerifyResult = {
3
3
  readonly ok: true;
4
4
  readonly userId: string;
@@ -1,4 +1,4 @@
1
- import type { Temporal } from "temporal-polyfill";
1
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  export type VerifyResult = {
3
3
  readonly ok: true;
4
4
  readonly userId: string;
@@ -2,7 +2,7 @@ import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-fr
2
2
  import { buildSessionRoles, createSystemUser, defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
5
- import { Temporal } from "temporal-polyfill";
5
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
6
  import * as z from "zod";
7
7
  import { burnToken, sessionLocaleField, sessionTimezoneField } from "../../shared/index.js";
8
8
  import { USER_STATUS, UserQueries } from "../../user/index.js";
@@ -1,7 +1,7 @@
1
1
  import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
- import { Temporal } from "temporal-polyfill";
4
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
5
5
  import * as z from "zod";
6
6
  import { burnToken } from "../../shared/index.js";
7
7
  import { base32Decode } from "../base32.js";
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  export type HmacTokenVerifyResult<TPayload> = {
3
3
  readonly ok: true;
4
4
  readonly payload: TPayload;
@@ -1,5 +1,5 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
- import { Temporal } from "temporal-polyfill";
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
3
  function hmacSign(input, secret) {
4
4
  return createHmac("sha256", secret).update(input).digest("base64url");
5
5
  }
@@ -1,5 +1,5 @@
1
1
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
- import { Temporal } from "temporal-polyfill";
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
3
  import { type HmacTokenVerifyResult } from "./hmac-token-codec.js";
4
4
  export type MfaChallengePayload = {
5
5
  readonly userId: string;
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  import { createHmacTokenCodec } from "./hmac-token-codec.js";
3
3
  function isPayload(value) {
4
4
  if (typeof value !== "object" || value === null)
@@ -1,5 +1,5 @@
1
1
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
- import { Temporal } from "temporal-polyfill";
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
3
  import { type HmacTokenVerifyResult } from "./hmac-token-codec.js";
4
4
  export type MfaPreauthSetupPayload = {
5
5
  readonly userId: string;
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  import { createHmacTokenCodec } from "./hmac-token-codec.js";
3
3
  function isPayload(value) {
4
4
  if (typeof value !== "object" || value === null)
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  export type MfaSetupPayload = {
3
3
  readonly userId: string;
4
4
  readonly totpSecretBase32: string;
@@ -10,7 +10,7 @@
10
10
  // signed-token.ts (userId-only payload) because setup needs to carry
11
11
  // generated secret material, not just re-derive it from a lookup.
12
12
  import { createHmac, timingSafeEqual } from "node:crypto";
13
- import { Temporal } from "temporal-polyfill";
13
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
14
14
  function sign(input, secret) {
15
15
  return createHmac("sha256", secret).update(input).digest("base64url");
16
16
  }
@@ -1,5 +1,5 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
2
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
3
  export declare const BILLING_FOUNDATION_FEATURE: "billing-foundation";
4
4
  export declare const SUBSCRIPTION_PROVIDER_EXTENSION: "subscriptionProvider";
5
5
  /** delivery notification type of the § 312f contract confirmation mail. */
@@ -119,7 +119,7 @@ export declare const BillingPlanActions: {
119
119
  export type BillingPlanAction = (typeof BillingPlanActions)[keyof typeof BillingPlanActions];
120
120
  export declare const TERMINAL_SUBSCRIPTION_STATUSES: ReadonlySet<string>;
121
121
  export declare function isTerminalSubscriptionStatus(status: string): boolean;
122
- export declare const STALE_INCOMPLETE_AFTER: TemporalPolyfill.Duration;
122
+ export declare const STALE_INCOMPLETE_AFTER: globalThis.Temporal.Duration;
123
123
  /** Time-aware terminal check for the checkout gate: a canceled subscription
124
124
  * is terminal (see `isTerminalSubscriptionStatus`), and so is an
125
125
  * `incomplete` one once it's older than `STALE_INCOMPLETE_AFTER` — every
@@ -1,12 +1,6 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
2
  // @runtime client
3
- // temporal-polyfill is a plain npm dependency (not a framework runtime
4
- // module), so importing it here doesn't break browser bundles. Aliased —
5
- // same reason as event-store.ts's own import: the un-aliased name would
6
- // shadow the ambient global `Temporal` TYPE every `Temporal.Instant`
7
- // annotation in this file (and its callers, e.g. StoredEvent.createdAt)
8
- // resolves against, see #1438.
9
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
3
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
10
4
  // Feature name
11
5
  export const BILLING_FOUNDATION_FEATURE = "billing-foundation";
12
6
  // Extension-point name fuer Provider-Plugins (subscription-stripe,
@@ -158,7 +152,7 @@ export function isTerminalSubscriptionStatus(status) {
158
152
  // after roughly 23 hours; without a matching staleness cutoff on our side, a
159
153
  // stale incomplete row would block that tenant from ever starting a new
160
154
  // checkout, since the projection has no expiry-triggered event of its own.
161
- export const STALE_INCOMPLETE_AFTER = TemporalPolyfill.Duration.from({ hours: 24 });
155
+ export const STALE_INCOMPLETE_AFTER = Temporal.Duration.from({ hours: 24 });
162
156
  /** Time-aware terminal check for the checkout gate: a canceled subscription
163
157
  * is terminal (see `isTerminalSubscriptionStatus`), and so is an
164
158
  * `incomplete` one once it's older than `STALE_INCOMPLETE_AFTER` — every
@@ -170,10 +164,7 @@ export function isSubscriptionBlockingCheckout(sub, now) {
170
164
  if (sub.status !== SubscriptionStatuses.incomplete)
171
165
  return true;
172
166
  const staleAt = sub.lastChangedAt.add(STALE_INCOMPLETE_AFTER);
173
- // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
174
- // at runtime — row types resolve against ambient Temporal; polyfill Instant
175
- // is a separate nominal type across the two .d.ts sources.
176
- return (TemporalPolyfill.Instant.compare(now, staleAt) < 0);
167
+ return Temporal.Instant.compare(now, staleAt) < 0;
177
168
  }
178
169
  // A subscription in one of these statuses can be switched to a different
179
170
  // plan tier via the provider's confirmation page (Stripe portal
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  import { CONSENT_TEXTS, resolveConsentLocale } from "./consent-text.js";
3
3
  const LABELS = {
4
4
  de: {
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  import { TERMINATION_TEXTS } from "./termination-texts.js";
3
3
  const TIME_ZONE_BY_LOCALE = {
4
4
  de: "Europe/Berlin",
@@ -50,10 +50,7 @@
50
50
  // (z.B. für analytics: "wie viele Wechsel im Monat?"), kommt ein
51
51
  // `subscription-provider-changed`-event-type später.
52
52
  import { defineFeature, EXT_TENANT_DATA, } from "@cosmicdrift/kumiko-framework/engine";
53
- // Aliased — an un-aliased `Temporal` would shadow the ambient global
54
- // `Temporal` TYPE `ResolvedBillingFoundationOptions.now`'s return type
55
- // resolves against, see event-store.ts's own import comment (#1438).
56
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
53
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
57
54
  import { BILLING_FOUNDATION_FEATURE, SUBSCRIPTION_PROVIDER_EXTENSION, SubscriptionFoundationHandlers, } from "./constants.js";
58
55
  import { registerConsumerProtection } from "./consumer-protection/register-consent.js";
59
56
  import { paymentEntity, subscriptionEntity } from "./entities.js";
@@ -87,11 +84,7 @@ export function createBillingFoundationFeature(options = {}) {
87
84
  // ever calls `Temporal.Now.instant` itself.
88
85
  const widened = {
89
86
  ...options,
90
- // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
91
- // at runtime — `now` is typed against the ambient Temporal global;
92
- // TemporalPolyfill.Now.instant() returns the polyfill's own nominal
93
- // Instant type across the two .d.ts sources.
94
- now: options.now ?? (() => TemporalPolyfill.Now.instant()),
87
+ now: options.now ?? (() => Temporal.Now.instant()),
95
88
  ...(options.baseUrl !== undefined && {
96
89
  baseUrl: options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl,
97
90
  }),
@@ -14,23 +14,20 @@
14
14
  // the same reason. A human SystemAdmin can still dispatch this directly.
15
15
  import { createHash } from "node:crypto";
16
16
  import { SYSTEM_ROLE } from "@cosmicdrift/kumiko-framework/engine";
17
- // Aliased — an un-aliased `Temporal` would shadow the ambient global
18
- // `Temporal` TYPE `SubscriptionView.currentPeriodEnd`/`.cancelAt` resolve
19
- // against, same reasoning as constants.ts's own import.
20
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
17
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
18
  import * as z from "zod";
22
19
  import { findProviderPlugin } from "../checkout-core.js";
23
20
  import { isTerminalSubscriptionStatus, SubscriptionEventTypes } from "../constants.js";
24
21
  import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
25
22
  import { appendSubscriptionEvent } from "./process-event.write.js";
26
23
  export const syncSubscriptionSchema = z.object({}).strict();
27
- // Parses both sides fresh via the polyfill before comparing — tolerant of a
24
+ // Parses both sides fresh via Temporal before comparing — tolerant of a
28
25
  // provider snapshot's ISO string using a different (but equivalent)
29
26
  // representation than `Temporal.Instant#toString()`'s canonical form
30
27
  // (offset spelling, sub-second precision, ...), unlike a plain `===` on the
31
28
  // raw strings.
32
29
  function isoInstantsEqual(a, b) {
33
- return (TemporalPolyfill.Instant.compare(TemporalPolyfill.Instant.from(a), TemporalPolyfill.Instant.from(b)) === 0);
30
+ return Temporal.Instant.compare(Temporal.Instant.from(a), Temporal.Instant.from(b)) === 0;
34
31
  }
35
32
  // Deterministic per (snapshot-content, current stream head) — not per-call.
36
33
  // Folding in `lastChangedAt` (not just the snapshot's own fields) closes a
@@ -1,5 +1,7 @@
1
1
  import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
- import { type HandlerContext, type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type HandlerContext, type SessionUser, type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
3
+ export declare function retryCounterWriteOnVersionConflict(writeAttempt: () => Promise<WriteResult>): Promise<WriteResult>;
4
+ export declare function retryOnStreamVersionConflict<T>(attempt: () => Promise<T>): Promise<T>;
3
5
  export type BookCapUsageOptions = {
4
6
  readonly capName: string;
5
7
  readonly periodStartIso: string;
@@ -7,8 +9,10 @@ export type BookCapUsageOptions = {
7
9
  readonly outsideTransaction?: boolean;
8
10
  readonly guardCurrentValue?: (currentValue: number) => void;
9
11
  };
12
+ export declare function requireOutsideTransactionDb(ctx: HandlerContext): TenantDb;
10
13
  export declare function bookCapUsage(ctx: HandlerContext, options: BookCapUsageOptions): Promise<WriteResult>;
11
14
  export declare function releaseCapUsage(ctx: HandlerContext, options: Omit<BookCapUsageOptions, "guardCurrentValue">): Promise<WriteResult>;
15
+ export declare function applyCapDeltaOn(db: TenantDb, user: SessionUser, options: BookCapUsageOptions, direction: "add" | "subtract"): Promise<WriteResult>;
12
16
  export type MarkCapSoftWarnedOptions = {
13
17
  readonly capName: string;
14
18
  readonly periodStartIso: string;
@@ -1,11 +1,11 @@
1
1
  // In-process cap booking for the caller's own tenant; no SystemAdmin dispatch needed.
2
2
  import { runInOwnTransaction } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { createEntityExecutor, } from "@cosmicdrift/kumiko-framework/engine";
4
- import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
5
- import { Temporal } from "temporal-polyfill";
4
+ import { eventsTable, VersionConflictError } from "@cosmicdrift/kumiko-framework/event-store";
5
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
6
  import * as z from "zod";
7
7
  import { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
8
- import { CAP_COUNTER_ROLLING_AGGREGATE_TYPE, ROLLING_INCREMENTED_EVENT_QN } from "./constants.js";
8
+ import { CAP_COUNTER_ROLLING_AGGREGATE_TYPE, ROLLING_INCREMENTED_EVENT_QN, ROLLING_RELEASED_EVENT_QN, } from "./constants.js";
9
9
  import { capCounterEntity } from "./entity.js";
10
10
  const { table, executor } = createEntityExecutor("cap-counter", capCounterEntity);
11
11
  // periodStartIso feeds the aggregate id and Temporal.Instant.from(); a non-instant string would
@@ -26,7 +26,7 @@ const BACKOFF_JITTER_MS_PER_ATTEMPT = 5;
26
26
  function sleepMs(ms) {
27
27
  return new Promise((resolve) => setTimeout(resolve, ms));
28
28
  }
29
- async function retryCounterWriteOnVersionConflict(writeAttempt) {
29
+ export async function retryCounterWriteOnVersionConflict(writeAttempt) {
30
30
  let result = await writeAttempt();
31
31
  for (let attempt = 1; attempt < MAX_COUNTER_WRITE_ATTEMPTS && isLostCounterRace(result); attempt++) {
32
32
  await sleepMs(Math.random() * attempt * BACKOFF_JITTER_MS_PER_ATTEMPT);
@@ -34,7 +34,21 @@ async function retryCounterWriteOnVersionConflict(writeAttempt) {
34
34
  }
35
35
  return result;
36
36
  }
37
- function requireOutsideTransactionDb(ctx) {
37
+ // Event-store appends signal a lost race by throwing; the whole attempt (read, guard, append) is repeated.
38
+ export async function retryOnStreamVersionConflict(attempt) {
39
+ for (let tries = 1;; tries++) {
40
+ try {
41
+ return await attempt();
42
+ }
43
+ catch (error) {
44
+ if (!(error instanceof VersionConflictError) || tries >= MAX_COUNTER_WRITE_ATTEMPTS) {
45
+ throw error;
46
+ }
47
+ await sleepMs(Math.random() * tries * BACKOFF_JITTER_MS_PER_ATTEMPT);
48
+ }
49
+ }
50
+ }
51
+ export function requireOutsideTransactionDb(ctx) {
38
52
  if (!ctx.dbOutsideTransaction) {
39
53
  throw new Error("cap-counter.bookCapUsage: outsideTransaction requested but ctx.dbOutsideTransaction is undefined");
40
54
  }
@@ -47,45 +61,46 @@ export async function bookCapUsage(ctx, options) {
47
61
  export async function releaseCapUsage(ctx, options) {
48
62
  return applyCapDelta(ctx, options, "subtract");
49
63
  }
50
- async function applyCapDelta(ctx, options, direction) {
64
+ // One attempt against an already-open db handle; a lost race comes back as a version_conflict failure the caller retries.
65
+ export async function applyCapDeltaOn(db, user, options, direction) {
51
66
  const parsed = capBookingSchema.parse(options);
52
- const aggregateId = capCounterAggregateId(ctx.user.tenantId, parsed.capName, parsed.periodStartIso);
53
- async function attemptWrite(db) {
54
- const existing = await db.selectMany(table, { id: aggregateId }, { limit: 1 });
55
- if (existing.length === 0) {
56
- options.guardCurrentValue?.(0);
57
- if (direction === "subtract")
58
- return { isSuccess: true, data: {} };
59
- return executor.create({
60
- id: aggregateId,
61
- capName: parsed.capName,
62
- value: parsed.amount,
63
- periodStart: Temporal.Instant.from(parsed.periodStartIso),
64
- lastSoftWarnedAt: null,
65
- }, ctx.user, db);
66
- }
67
- const currentRow = existing[0];
68
- if (!currentRow) {
69
- throw new Error("cap-counter.bookCapUsage: row vanished between length-check and read");
70
- }
71
- const currentValue = currentRow["value"]; // @cast-boundary db-row
72
- const currentVersion = currentRow["version"]; // @cast-boundary db-row
73
- options.guardCurrentValue?.(currentValue);
74
- return executor.update({
67
+ const aggregateId = capCounterAggregateId(user.tenantId, parsed.capName, parsed.periodStartIso);
68
+ const existing = await db.selectMany(table, { id: aggregateId }, { limit: 1 });
69
+ if (existing.length === 0) {
70
+ options.guardCurrentValue?.(0);
71
+ if (direction === "subtract")
72
+ return { isSuccess: true, data: {} };
73
+ return executor.create({
75
74
  id: aggregateId,
76
- version: currentVersion,
77
- changes: {
78
- value: direction === "add"
79
- ? currentValue + parsed.amount
80
- : Math.max(0, currentValue - parsed.amount),
81
- },
82
- }, ctx.user, db);
75
+ capName: parsed.capName,
76
+ value: parsed.amount,
77
+ periodStart: Temporal.Instant.from(parsed.periodStartIso),
78
+ lastSoftWarnedAt: null,
79
+ }, user, db);
80
+ }
81
+ const currentRow = existing[0];
82
+ if (!currentRow) {
83
+ throw new Error("cap-counter.bookCapUsage: row vanished between length-check and read");
83
84
  }
85
+ const currentValue = currentRow["value"]; // @cast-boundary db-row
86
+ const currentVersion = currentRow["version"]; // @cast-boundary db-row
87
+ options.guardCurrentValue?.(currentValue);
88
+ return executor.update({
89
+ id: aggregateId,
90
+ version: currentVersion,
91
+ changes: {
92
+ value: direction === "add"
93
+ ? currentValue + parsed.amount
94
+ : Math.max(0, currentValue - parsed.amount),
95
+ },
96
+ }, user, db);
97
+ }
98
+ async function applyCapDelta(ctx, options, direction) {
84
99
  if (options.outsideTransaction) {
85
100
  const outsideDb = requireOutsideTransactionDb(ctx);
86
- return retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb, attemptWrite));
101
+ return retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb, (txDb) => applyCapDeltaOn(txDb, ctx.user, options, direction)));
87
102
  }
88
- return retryCounterWriteOnVersionConflict(() => attemptWrite(ctx.db));
103
+ return retryCounterWriteOnVersionConflict(() => applyCapDeltaOn(ctx.db, ctx.user, options, direction));
89
104
  }
90
105
  export async function markCapSoftWarned(ctx, options) {
91
106
  const parsed = capPeriodSchema.parse(options);
@@ -117,27 +132,33 @@ export async function markCapSoftWarned(ctx, options) {
117
132
  }
118
133
  return retryCounterWriteOnVersionConflict(() => attemptMark(ctx.db));
119
134
  }
120
- // Sums usage without throwing — for callers that only need the raw number
121
- // (e.g. a CapSpec.usage callback), not the enforce-and-throw path.
122
- export async function readRollingCapUsage(db, tenantId, options) {
135
+ async function sumRollingEventAmounts(db, tenantId, options, type) {
123
136
  const aggregateId = rollingCapAggregateId(tenantId, options.capName);
124
137
  const cutoff = Temporal.Now.instant().subtract({ hours: options.windowDays * 24 });
125
138
  const rows = await db.selectMany(eventsTable, {
126
139
  tenantId,
127
140
  aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
128
141
  aggregateId,
129
- type: ROLLING_INCREMENTED_EVENT_QN,
142
+ type,
130
143
  createdAt: { gte: cutoff },
131
144
  });
132
- let value = 0;
145
+ let total = 0;
133
146
  for (const row of rows) {
134
147
  // @cast-boundary engine-payload — events.payload is jsonb (typed as
135
148
  // unknown by drizzle's $type<Record<string,unknown>>); narrowing the
136
149
  // shape here mirrors enforceRollingCap's own read-side contract.
137
150
  const payload = row["payload"];
138
151
  if (typeof payload.amount === "number") {
139
- value += payload.amount;
152
+ total += payload.amount;
140
153
  }
141
154
  }
142
- return value;
155
+ return total;
156
+ }
157
+ // Sums usage without throwing — for callers that only need the raw number
158
+ // (e.g. a CapSpec.usage callback), not the enforce-and-throw path.
159
+ // Incremented minus released amounts inside the window; a release whose increment already left the window cannot push it below 0.
160
+ export async function readRollingCapUsage(db, tenantId, options) {
161
+ const incremented = await sumRollingEventAmounts(db, tenantId, options, ROLLING_INCREMENTED_EVENT_QN);
162
+ const released = await sumRollingEventAmounts(db, tenantId, options, ROLLING_RELEASED_EVENT_QN);
163
+ return Math.max(0, incremented - released);
143
164
  }
@@ -0,0 +1,12 @@
1
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { ReservationHandle } from "@cosmicdrift/kumiko-types/handlers";
3
+ import { type BookCapUsageOptions, type ReadRollingCapUsageOptions } from "./book-cap-usage.js";
4
+ export type ReserveCalendarCapOptions = Required<Pick<BookCapUsageOptions, "capName" | "periodStartIso" | "amount">> & Pick<BookCapUsageOptions, "guardCurrentValue">;
5
+ export declare function reserveCalendarCap(ctx: HandlerContext, options: ReserveCalendarCapOptions): Promise<ReservationHandle>;
6
+ export type ReserveRollingCapOptions = ReadRollingCapUsageOptions & {
7
+ readonly amount: number;
8
+ readonly guardCurrentValue?: (currentValue: number) => void;
9
+ };
10
+ export declare function reserveRollingCap(ctx: HandlerContext, options: ReserveRollingCapOptions): Promise<ReservationHandle>;
11
+ export declare function releaseCapReservation(ctx: HandlerContext, id: string): Promise<void>;
12
+ export declare function releaseExpiredCapReservations(ctx: HandlerContext, capName: string): Promise<void>;
@@ -0,0 +1,135 @@
1
+ import { deleteMany, runInOwnTransaction } from "@cosmicdrift/kumiko-framework/db";
2
+ import { reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { appendEventInTenantDb, getStreamVersionInTenantDb, } from "@cosmicdrift/kumiko-framework/event-store";
4
+ import { generateId } from "@cosmicdrift/kumiko-framework/utils";
5
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
+ import { rollingCapAggregateId } from "./aggregate-id.js";
7
+ import { applyCapDeltaOn, readRollingCapUsage, requireOutsideTransactionDb, retryCounterWriteOnVersionConflict, retryOnStreamVersionConflict, } from "./book-cap-usage.js";
8
+ import { CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CAP_RESERVATION_TTL_MINUTES, ROLLING_INCREMENTED_EVENT_QN, ROLLING_RELEASED_EVENT_QN, } from "./constants.js";
9
+ import { capReservationsTable } from "./tables.js";
10
+ const NOTHING_TO_RELEASE = { isSuccess: true, data: {} };
11
+ async function insertReservationRow(txDb, id, draft) {
12
+ const now = Temporal.Now.instant();
13
+ await txDb.insertOne(capReservationsTable, {
14
+ id,
15
+ capName: draft.capName,
16
+ kind: draft.kind,
17
+ periodStartIso: draft.periodStartIso ?? null,
18
+ amount: draft.amount,
19
+ expiresAt: now.add({ minutes: CAP_RESERVATION_TTL_MINUTES }),
20
+ createdAt: now,
21
+ });
22
+ }
23
+ function toHandle(ctx, id) {
24
+ return {
25
+ release: () => releaseCapReservation(ctx, id),
26
+ confirmInTransaction: (confirm) => confirmCapReservation(confirm, id),
27
+ };
28
+ }
29
+ // The counter increment and the reservation row commit together, so a booked amount never exists
30
+ // without a row that can give it back.
31
+ export async function reserveCalendarCap(ctx, options) {
32
+ const outsideDb = requireOutsideTransactionDb(ctx);
33
+ const id = generateId();
34
+ const reserved = await retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb, async (txDb) => {
35
+ const booked = await applyCapDeltaOn(txDb, ctx.user, options, "add");
36
+ if (!booked.isSuccess)
37
+ return booked;
38
+ await insertReservationRow(txDb, id, {
39
+ capName: options.capName,
40
+ kind: "calendar",
41
+ periodStartIso: options.periodStartIso,
42
+ amount: options.amount,
43
+ });
44
+ return booked;
45
+ }));
46
+ if (!reserved.isSuccess)
47
+ throw reraiseAsKumikoError(reserved.error);
48
+ return toHandle(ctx, id);
49
+ }
50
+ function rollingEvent(user, capName, amount, expectedVersion, type) {
51
+ return {
52
+ aggregateId: rollingCapAggregateId(user.tenantId, capName),
53
+ aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
54
+ expectedVersion,
55
+ type,
56
+ payload: { capName, amount },
57
+ metadata: { userId: user.id },
58
+ };
59
+ }
60
+ // The append is version-guarded: a booker that read the usage before another one appended loses the
61
+ // race and repeats the whole attempt, so the hard-cap check always sees the stream it appends to.
62
+ export async function reserveRollingCap(ctx, options) {
63
+ const outsideDb = requireOutsideTransactionDb(ctx);
64
+ const id = generateId();
65
+ const aggregateId = rollingCapAggregateId(ctx.user.tenantId, options.capName);
66
+ await retryOnStreamVersionConflict(() => runInOwnTransaction(outsideDb, async (txDb) => {
67
+ const version = await getStreamVersionInTenantDb(txDb, aggregateId);
68
+ const usage = await readRollingCapUsage(txDb, ctx.user.tenantId, options);
69
+ options.guardCurrentValue?.(usage);
70
+ await appendEventInTenantDb(txDb, rollingEvent(ctx.user, options.capName, options.amount, version, ROLLING_INCREMENTED_EVENT_QN));
71
+ await insertReservationRow(txDb, id, {
72
+ capName: options.capName,
73
+ kind: "rolling",
74
+ amount: options.amount,
75
+ });
76
+ return NOTHING_TO_RELEASE;
77
+ }));
78
+ return toHandle(ctx, id);
79
+ }
80
+ // Deleting the row takes its row lock inside the handler transaction, so a release started after a
81
+ // failed COMMIT waits for the real outcome and finds the row gone when the commit went through.
82
+ async function confirmCapReservation(confirm, id) {
83
+ await deleteMany(confirm.tx, capReservationsTable, { id, tenantId: confirm.tenantId });
84
+ }
85
+ async function giveBackCalendar(txDb, user, row) {
86
+ if (row.periodStartIso === null) {
87
+ throw new Error(`cap-counter: calendar reservation ${row.id} has no period to release against`);
88
+ }
89
+ return applyCapDeltaOn(txDb, user, { capName: row.capName, periodStartIso: row.periodStartIso, amount: row.amount }, "subtract");
90
+ }
91
+ async function giveBackRolling(txDb, user, row) {
92
+ const version = await getStreamVersionInTenantDb(txDb, rollingCapAggregateId(user.tenantId, row.capName));
93
+ await appendEventInTenantDb(txDb, rollingEvent(user, row.capName, row.amount, version, ROLLING_RELEASED_EVENT_QN));
94
+ return NOTHING_TO_RELEASE;
95
+ }
96
+ async function giveBackReservation(txDb, user, id) {
97
+ const row = await txDb.fetchOne(capReservationsTable, { id });
98
+ if (!row)
99
+ return NOTHING_TO_RELEASE;
100
+ // Waits for a confirm still holding the row; zero rows back means that confirm committed.
101
+ const claimed = await txDb.updateMany(capReservationsTable, { expiresAt: row.expiresAt }, { id });
102
+ if (claimed.length === 0)
103
+ return NOTHING_TO_RELEASE;
104
+ const given = row.kind === "rolling"
105
+ ? await giveBackRolling(txDb, user, row)
106
+ : await giveBackCalendar(txDb, user, row);
107
+ if (!given.isSuccess)
108
+ return given;
109
+ await txDb.deleteMany(capReservationsTable, { id });
110
+ return given;
111
+ }
112
+ export async function releaseCapReservation(ctx, id) {
113
+ const outsideDb = requireOutsideTransactionDb(ctx);
114
+ const released = await retryOnStreamVersionConflict(() => retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb, (txDb) => giveBackReservation(txDb, ctx.user, id))));
115
+ if (!released.isSuccess)
116
+ throw reraiseAsKumikoError(released.error);
117
+ }
118
+ // A failed release leaves its row behind; giving it back once the TTL passed bounds how long that
119
+ // capacity stays counted.
120
+ export async function releaseExpiredCapReservations(ctx, capName) {
121
+ const outsideDb = requireOutsideTransactionDb(ctx);
122
+ const expired = await outsideDb.selectMany(capReservationsTable, {
123
+ tenantId: ctx.user.tenantId,
124
+ capName,
125
+ expiresAt: { lt: Temporal.Now.instant() },
126
+ });
127
+ for (const { id } of expired) {
128
+ try {
129
+ await releaseCapReservation(ctx, id);
130
+ }
131
+ catch (error) {
132
+ ctx.log?.error("releasing an expired cap reservation failed", { id, capName, error });
133
+ }
134
+ }
135
+ }
@@ -3,6 +3,9 @@ export declare const CAP_COUNTER_LIST_SCREEN_ID: "cap-list";
3
3
  export declare const CAP_COUNTER_ROLLING_AGGREGATE_TYPE: "cap-counter-rolling";
4
4
  export declare const ROLLING_INCREMENTED_EVENT_SHORT: "rolling-incremented";
5
5
  export declare const ROLLING_INCREMENTED_EVENT_QN: "cap-counter:event:rolling-incremented";
6
+ export declare const ROLLING_RELEASED_EVENT_SHORT: "rolling-released";
7
+ export declare const ROLLING_RELEASED_EVENT_QN: "cap-counter:event:rolling-released";
8
+ export declare const CAP_RESERVATION_TTL_MINUTES = 60;
6
9
  export declare const CapCounterHandlers: {
7
10
  readonly increment: "cap-counter:write:increment";
8
11
  readonly incrementRolling: "cap-counter:write:increment-rolling";