@cosmicdrift/kumiko-samples 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 (119) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +2 -2
  3. package/packages/bundled-features/src/audit/handlers/list.query.ts +1 -1
  4. package/packages/bundled-features/src/auth-email-password/email-templates.ts +1 -1
  5. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -1
  6. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +1 -1
  7. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -1
  8. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +1 -1
  10. package/packages/bundled-features/src/auth-email-password/reset-token.ts +1 -1
  11. package/packages/bundled-features/src/auth-email-password/unlock-token.ts +1 -1
  12. package/packages/bundled-features/src/auth-email-password/verification-token.ts +1 -1
  13. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
  14. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm.write.ts +1 -1
  15. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +1 -1
  16. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +1 -1
  17. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +1 -1
  18. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +1 -1
  19. package/packages/bundled-features/src/billing-foundation/constants.ts +3 -17
  20. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +1 -1
  21. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +1 -1
  22. package/packages/bundled-features/src/billing-foundation/feature.ts +2 -9
  23. package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +3 -11
  24. package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +96 -55
  25. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +247 -0
  26. package/packages/bundled-features/src/cap-counter/changes.json +28 -0
  27. package/packages/bundled-features/src/cap-counter/constants.ts +8 -0
  28. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +1 -9
  29. package/packages/bundled-features/src/cap-counter/feature.ts +7 -0
  30. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +5 -0
  31. package/packages/bundled-features/src/cap-counter/index.ts +9 -0
  32. package/packages/bundled-features/src/cap-counter/payload-field.ts +10 -0
  33. package/packages/bundled-features/src/cap-counter/reservation-chain.ts +51 -0
  34. package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +3 -0
  35. package/packages/bundled-features/src/cap-counter/tables.ts +53 -0
  36. package/packages/bundled-features/src/cap-counter/value-cap-guard.ts +70 -0
  37. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +63 -70
  38. package/packages/bundled-features/src/channel-discord/feature.ts +1 -0
  39. package/packages/bundled-features/src/channel-slack/feature.ts +1 -0
  40. package/packages/bundled-features/src/channel-teams/feature.ts +1 -0
  41. package/packages/bundled-features/src/delivery/attempt-log-retention.ts +40 -0
  42. package/packages/bundled-features/src/delivery/attempt-log.ts +26 -3
  43. package/packages/bundled-features/src/delivery/changes.json +12 -0
  44. package/packages/bundled-features/src/delivery/constants.ts +1 -0
  45. package/packages/bundled-features/src/delivery/events.ts +3 -0
  46. package/packages/bundled-features/src/delivery/feature.ts +50 -2
  47. package/packages/bundled-features/src/delivery/handlers/log.query.ts +1 -1
  48. package/packages/bundled-features/src/delivery/index.ts +6 -0
  49. package/packages/bundled-features/src/delivery/mask-recipient-address.ts +40 -0
  50. package/packages/bundled-features/src/delivery/public-names.ts +5 -0
  51. package/packages/bundled-features/src/delivery/types.ts +12 -2
  52. package/packages/bundled-features/src/derivatives-sharp/changes.json +6 -0
  53. package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +1 -1
  54. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +1 -1
  55. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +1 -1
  56. package/packages/bundled-features/src/form-draft/lookup.ts +1 -1
  57. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
  58. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +1 -1
  59. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +1 -1
  60. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
  61. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +1 -1
  62. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +1 -1
  63. package/packages/bundled-features/src/jobs/job-instant.ts +1 -1
  64. package/packages/bundled-features/src/legal-pages/README.md +3 -3
  65. package/packages/bundled-features/src/legal-pages/changes.json +6 -0
  66. package/packages/bundled-features/src/metrics/compute.ts +1 -1
  67. package/packages/bundled-features/src/metrics/feature.ts +1 -1
  68. package/packages/bundled-features/src/metrics/types.ts +1 -1
  69. package/packages/bundled-features/src/metrics/window.ts +1 -1
  70. package/packages/bundled-features/src/personal-access-tokens/expiry.ts +1 -1
  71. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -1
  72. package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
  73. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +1 -1
  74. package/packages/bundled-features/src/renderer-foundation/types.ts +1 -1
  75. package/packages/bundled-features/src/renderer-simple/changes.json +6 -0
  76. package/packages/bundled-features/src/renderer-simple/feature.ts +4 -6
  77. package/packages/bundled-features/src/renderer-simple/index.ts +1 -0
  78. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +138 -0
  79. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +33 -4
  80. package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
  81. package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
  82. package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
  83. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -1
  84. package/packages/bundled-features/src/shared/grace-period.ts +3 -10
  85. package/packages/bundled-features/src/shared/row-bound-grant.ts +1 -1
  86. package/packages/bundled-features/src/shared/signed-token.ts +1 -1
  87. package/packages/bundled-features/src/subscription-mollie/verify-webhook.ts +1 -1
  88. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +1 -1
  89. package/packages/bundled-features/src/template-resolver/api.ts +1 -1
  90. package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -1
  91. package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +1 -1
  92. package/packages/bundled-features/src/template-resolver/table.ts +1 -1
  93. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +1 -1
  94. package/packages/bundled-features/src/tenant/invited-membership.ts +1 -1
  95. package/packages/bundled-features/src/tenant-handover/grant.ts +1 -1
  96. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  97. package/packages/bundled-features/src/tier-engine/index.ts +7 -0
  98. package/packages/bundled-features/src/tier-engine/tier-option-gate.ts +148 -0
  99. package/packages/bundled-features/src/user-data-rights/db/queries/export-jobs.ts +1 -1
  100. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +3 -9
  101. package/packages/bundled-features/src/user-data-rights/deletion-token.ts +1 -1
  102. package/packages/bundled-features/src/user-data-rights/email-templates.ts +1 -1
  103. package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
  104. package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
  105. package/packages/bundled-features/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +36 -6
  106. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +1 -1
  107. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +1 -1
  108. package/samples/apps/marketing-demo/package.json +1 -0
  109. package/samples/apps/marketing-demo/src/app/seed-data.ts +2 -2
  110. package/samples/apps/marketing-demo/src/app/seed.ts +1 -1
  111. package/samples/apps/styleguide/src/app/shell.tsx +1 -1
  112. package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +9 -3
  113. package/samples/apps/ui-walkthrough/src/app/assistant-action.tsx +6 -5
  114. package/samples/apps/ui-walkthrough/src/app/shell.tsx +3 -2
  115. package/samples/apps/ui-walkthrough/src/features/tasks/i18n.ts +8 -0
  116. package/samples/apps/use-all-bundled/package.json +1 -0
  117. package/samples/apps/use-all-bundled/src/app/seed.ts +1 -1
  118. package/samples/recipes/files-post-processing/package.json +1 -1
  119. package/samples/recipes/seed-migration/README.md +13 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-samples",
3
- "version": "0.350.0",
3
+ "version": "0.352.0",
4
4
  "description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.350.0",
3
+ "version": "0.352.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>",
@@ -164,7 +164,7 @@
164
164
  "nodemailer": "^10.0.13",
165
165
  "qrcode": "^1.5.4",
166
166
  "react": "^19.2.6",
167
- "sharp": "^0.35.4",
167
+ "sharp": "^0.35.5",
168
168
  "stripe": "^22.6.2",
169
169
  "tailwind-merge": "^3.6.0",
170
170
  "temporal-polyfill": "^0.3.2",
@@ -17,7 +17,7 @@
17
17
  import { selectMany, type WhereObject } from "@cosmicdrift/kumiko-framework/bun-db";
18
18
  import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
19
19
  import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
20
- import { Temporal } from "temporal-polyfill";
20
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
21
21
  import * as z from "zod";
22
22
  import { AUDIT_SCOPE_VALUES } from "../constants.js";
23
23
  import { resolveAuditScopeFilter } from "./resolve-audit-tenant.js";
@@ -9,7 +9,7 @@
9
9
  // (localeDe() / localeEs() call registerMailTranslations).
10
10
 
11
11
  import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
12
- import { Temporal } from "temporal-polyfill";
12
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
13
13
 
14
14
  export type AuthMailLocale = string;
15
15
 
@@ -31,7 +31,7 @@ import {
31
31
  type TenantId,
32
32
  } from "@cosmicdrift/kumiko-framework/engine";
33
33
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
34
- import type { Temporal } from "temporal-polyfill";
34
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
35
35
  import * as z from "zod";
36
36
  import { decryptStoredPii, sessionLocaleField, sessionTimezoneField } from "../../shared/index.js";
37
37
  // kumiko-lint-ignore cross-feature-import invite-flow
@@ -23,7 +23,7 @@ import {
23
23
  type TenantId,
24
24
  } from "@cosmicdrift/kumiko-framework/engine";
25
25
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
26
- import type { Temporal } from "temporal-polyfill";
26
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
27
27
  import * as z from "zod";
28
28
  import { decryptStoredPii } from "../../shared/index.js";
29
29
  // kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link), DB-row-owner ist tenant-feature
@@ -32,7 +32,7 @@ import {
32
32
  type WriteFailure,
33
33
  writeFailure,
34
34
  } from "@cosmicdrift/kumiko-framework/errors";
35
- import { Temporal } from "temporal-polyfill";
35
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
36
36
  import * as z from "zod";
37
37
  // kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link-Pattern), DB-row-owner ist tenant-feature
38
38
  import {
@@ -24,7 +24,7 @@
24
24
  import { generateToken } from "@cosmicdrift/kumiko-framework/api";
25
25
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
26
26
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
27
- import { Temporal } from "temporal-polyfill";
27
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
28
28
  import * as z from "zod";
29
29
  import { findSignupHandoverProvider, type SignupHandoverBinding } from "../../shared/index.js";
30
30
  import { AUTH_SIGNUP_DEFAULT_TTL_MINUTES } from "../constants.js";
@@ -15,7 +15,7 @@
15
15
  import { createSystemUser, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
16
16
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
17
17
  import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
18
- import type { Temporal } from "temporal-polyfill";
18
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
19
19
  import * as z from "zod";
20
20
  import { UserQueries } from "../../user/index.js";
21
21
  import { type AuthUserRow, parseAuthUserRow } from "../auth-user-row.js";
@@ -3,7 +3,7 @@
3
3
  // the shared HMAC logic lives in one place. verification-token.ts mirrors
4
4
  // this pattern with purpose="verify".
5
5
 
6
- import type { Temporal } from "temporal-polyfill";
6
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
7
7
  import { signToken, TokenPurpose, verifyToken } from "./signed-token.js";
8
8
 
9
9
  export type VerifyResult =
@@ -1,7 +1,7 @@
1
1
  // Thin wrapper around signed-token.ts pinning the purpose to "unlock".
2
2
  // Mirrors reset-token.ts / verification-token.ts.
3
3
 
4
- import type { Temporal } from "temporal-polyfill";
4
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
5
5
  import { signToken, TokenPurpose, verifyToken } from "./signed-token.js";
6
6
 
7
7
  export type VerifyResult =
@@ -2,7 +2,7 @@
2
2
  // Mirrors reset-token.ts so callers can import a flow-specific helper
3
3
  // without knowing the underlying HMAC scheme.
4
4
 
5
- import type { Temporal } from "temporal-polyfill";
5
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
6
6
  import { signToken, TokenPurpose, verifyToken } from "./signed-token.js";
7
7
 
8
8
  export type VerifyResult =
@@ -7,7 +7,7 @@ import {
7
7
  } from "@cosmicdrift/kumiko-framework/engine";
8
8
  import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
9
9
  import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
10
- import { Temporal } from "temporal-polyfill";
10
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
11
11
  import * as z from "zod";
12
12
  import { burnToken, sessionLocaleField, sessionTimezoneField } from "../../shared/index.js";
13
13
  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,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
 
4
4
  function hmacSign(input: string, secret: string): string {
5
5
  return createHmac("sha256", secret).update(input).digest("base64url");
@@ -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 { createHmacTokenCodec, type HmacTokenVerifyResult } from "./hmac-token-codec.js";
4
4
 
5
5
  export type MfaChallengePayload = {
@@ -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 { createHmacTokenCodec, type HmacTokenVerifyResult } from "./hmac-token-codec.js";
4
4
 
5
5
  export type MfaPreauthSetupPayload = {
@@ -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
 
15
15
  export type MfaSetupPayload = {
16
16
  readonly userId: string;
@@ -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
 
11
5
  // Feature name
12
6
  export const BILLING_FOUNDATION_FEATURE = "billing-foundation" as const;
@@ -185,7 +179,7 @@ export function isTerminalSubscriptionStatus(status: string): boolean {
185
179
  // after roughly 23 hours; without a matching staleness cutoff on our side, a
186
180
  // stale incomplete row would block that tenant from ever starting a new
187
181
  // checkout, since the projection has no expiry-triggered event of its own.
188
- export const STALE_INCOMPLETE_AFTER = TemporalPolyfill.Duration.from({ hours: 24 });
182
+ export const STALE_INCOMPLETE_AFTER = Temporal.Duration.from({ hours: 24 });
189
183
 
190
184
  /** Time-aware terminal check for the checkout gate: a canceled subscription
191
185
  * is terminal (see `isTerminalSubscriptionStatus`), and so is an
@@ -199,15 +193,7 @@ export function isSubscriptionBlockingCheckout(
199
193
  if (isTerminalSubscriptionStatus(sub.status)) return false;
200
194
  if (sub.status !== SubscriptionStatuses.incomplete) return true;
201
195
  const staleAt = sub.lastChangedAt.add(STALE_INCOMPLETE_AFTER);
202
- // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
203
- // at runtime — row types resolve against ambient Temporal; polyfill Instant
204
- // is a separate nominal type across the two .d.ts sources.
205
- return (
206
- TemporalPolyfill.Instant.compare(
207
- now as unknown as InstanceType<typeof TemporalPolyfill.Instant>,
208
- staleAt as unknown as InstanceType<typeof TemporalPolyfill.Instant>,
209
- ) < 0
210
- );
196
+ return Temporal.Instant.compare(now, staleAt) < 0;
211
197
  }
212
198
 
213
199
  // A subscription in one of these statuses can be switched to a different
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  import type { CheckoutConsentRecordedPayload } from "../events.js";
3
3
  import { CONSENT_TEXTS, type ConsentLocale, resolveConsentLocale } from "./consent-text.js";
4
4
 
@@ -1,4 +1,4 @@
1
- import { Temporal } from "temporal-polyfill";
1
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
2
  import type {
3
3
  ContractTerminationDeclarationType,
4
4
  ContractTerminationKind,
@@ -55,10 +55,7 @@ import {
55
55
  EXT_TENANT_DATA,
56
56
  type FeatureDefinition,
57
57
  } from "@cosmicdrift/kumiko-framework/engine";
58
- // Aliased — an un-aliased `Temporal` would shadow the ambient global
59
- // `Temporal` TYPE `ResolvedBillingFoundationOptions.now`'s return type
60
- // resolves against, see event-store.ts's own import comment (#1438).
61
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
58
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
62
59
  import {
63
60
  BILLING_FOUNDATION_FEATURE,
64
61
  SUBSCRIPTION_PROVIDER_EXTENSION,
@@ -131,11 +128,7 @@ export function createBillingFoundationFeature<TTier extends string = string>(
131
128
  // ever calls `Temporal.Now.instant` itself.
132
129
  const widened: ResolvedBillingFoundationOptions = {
133
130
  ...options,
134
- // @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
135
- // at runtime — `now` is typed against the ambient Temporal global;
136
- // TemporalPolyfill.Now.instant() returns the polyfill's own nominal
137
- // Instant type across the two .d.ts sources.
138
- now: options.now ?? (() => TemporalPolyfill.Now.instant() as unknown as Temporal.Instant),
131
+ now: options.now ?? (() => Temporal.Now.instant()),
139
132
  ...(options.baseUrl !== undefined && {
140
133
  baseUrl: options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl,
141
134
  }),
@@ -15,10 +15,7 @@
15
15
 
16
16
  import { createHash } from "node:crypto";
17
17
  import { SYSTEM_ROLE, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
18
- // Aliased — an un-aliased `Temporal` would shadow the ambient global
19
- // `Temporal` TYPE `SubscriptionView.currentPeriodEnd`/`.cancelAt` resolve
20
- // against, same reasoning as constants.ts's own import.
21
- import { Temporal as TemporalPolyfill } from "temporal-polyfill";
18
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
22
19
  import * as z from "zod";
23
20
  import { findProviderPlugin } from "../checkout-core.js";
24
21
  import { isTerminalSubscriptionStatus, SubscriptionEventTypes } from "../constants.js";
@@ -37,18 +34,13 @@ export type SyncSubscriptionResult =
37
34
  | { readonly synced: true }
38
35
  | { readonly synced: false; readonly reason: SyncSubscriptionSkipReason };
39
36
 
40
- // Parses both sides fresh via the polyfill before comparing — tolerant of a
37
+ // Parses both sides fresh via Temporal before comparing — tolerant of a
41
38
  // provider snapshot's ISO string using a different (but equivalent)
42
39
  // representation than `Temporal.Instant#toString()`'s canonical form
43
40
  // (offset spelling, sub-second precision, ...), unlike a plain `===` on the
44
41
  // raw strings.
45
42
  function isoInstantsEqual(a: string, b: string): boolean {
46
- return (
47
- TemporalPolyfill.Instant.compare(
48
- TemporalPolyfill.Instant.from(a),
49
- TemporalPolyfill.Instant.from(b),
50
- ) === 0
51
- );
43
+ return Temporal.Instant.compare(Temporal.Instant.from(a), Temporal.Instant.from(b)) === 0;
52
44
  }
53
45
 
54
46
  // Deterministic per (snapshot-content, current stream head) — not per-call.
@@ -4,14 +4,19 @@ import { runInOwnTransaction, type TenantDb } from "@cosmicdrift/kumiko-framewor
4
4
  import {
5
5
  createEntityExecutor,
6
6
  type HandlerContext,
7
+ type SessionUser,
7
8
  type TenantId,
8
9
  type WriteResult,
9
10
  } from "@cosmicdrift/kumiko-framework/engine";
10
- import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
11
- import { Temporal } from "temporal-polyfill";
11
+ import { eventsTable, VersionConflictError } from "@cosmicdrift/kumiko-framework/event-store";
12
+ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
12
13
  import * as z from "zod";
13
14
  import { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
14
- import { CAP_COUNTER_ROLLING_AGGREGATE_TYPE, ROLLING_INCREMENTED_EVENT_QN } from "./constants.js";
15
+ import {
16
+ CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
17
+ ROLLING_INCREMENTED_EVENT_QN,
18
+ ROLLING_RELEASED_EVENT_QN,
19
+ } from "./constants.js";
15
20
  import { capCounterEntity } from "./entity.js";
16
21
 
17
22
  const { table, executor } = createEntityExecutor("cap-counter", capCounterEntity);
@@ -39,7 +44,7 @@ function sleepMs(ms: number): Promise<void> {
39
44
  return new Promise((resolve) => setTimeout(resolve, ms));
40
45
  }
41
46
 
42
- async function retryCounterWriteOnVersionConflict(
47
+ export async function retryCounterWriteOnVersionConflict(
43
48
  writeAttempt: () => Promise<WriteResult>,
44
49
  ): Promise<WriteResult> {
45
50
  let result: WriteResult = await writeAttempt();
@@ -54,6 +59,20 @@ async function retryCounterWriteOnVersionConflict(
54
59
  return result;
55
60
  }
56
61
 
62
+ // Event-store appends signal a lost race by throwing; the whole attempt (read, guard, append) is repeated.
63
+ export async function retryOnStreamVersionConflict<T>(attempt: () => Promise<T>): Promise<T> {
64
+ for (let tries = 1; ; tries++) {
65
+ try {
66
+ return await attempt();
67
+ } catch (error) {
68
+ if (!(error instanceof VersionConflictError) || tries >= MAX_COUNTER_WRITE_ATTEMPTS) {
69
+ throw error;
70
+ }
71
+ await sleepMs(Math.random() * tries * BACKOFF_JITTER_MS_PER_ATTEMPT);
72
+ }
73
+ }
74
+ }
75
+
57
76
  export type BookCapUsageOptions = {
58
77
  readonly capName: string;
59
78
  readonly periodStartIso: string;
@@ -63,7 +82,7 @@ export type BookCapUsageOptions = {
63
82
  readonly guardCurrentValue?: (currentValue: number) => void;
64
83
  };
65
84
 
66
- function requireOutsideTransactionDb(ctx: HandlerContext): TenantDb {
85
+ export function requireOutsideTransactionDb(ctx: HandlerContext): TenantDb {
67
86
  if (!ctx.dbOutsideTransaction) {
68
87
  throw new Error(
69
88
  "cap-counter.bookCapUsage: outsideTransaction requested but ctx.dbOutsideTransaction is undefined",
@@ -87,64 +106,70 @@ export async function releaseCapUsage(
87
106
  return applyCapDelta(ctx, options, "subtract");
88
107
  }
89
108
 
90
- async function applyCapDelta(
91
- ctx: HandlerContext,
109
+ // One attempt against an already-open db handle; a lost race comes back as a version_conflict failure the caller retries.
110
+ export async function applyCapDeltaOn(
111
+ db: TenantDb,
112
+ user: SessionUser,
92
113
  options: BookCapUsageOptions,
93
114
  direction: "add" | "subtract",
94
115
  ): Promise<WriteResult> {
95
116
  const parsed = capBookingSchema.parse(options);
96
- const aggregateId = capCounterAggregateId(
97
- ctx.user.tenantId,
98
- parsed.capName,
99
- parsed.periodStartIso,
100
- );
101
-
102
- async function attemptWrite(db: TenantDb): Promise<WriteResult> {
103
- const existing = await db.selectMany(table, { id: aggregateId }, { limit: 1 });
104
- if (existing.length === 0) {
105
- options.guardCurrentValue?.(0);
106
- if (direction === "subtract") return { isSuccess: true, data: {} };
107
- return executor.create(
108
- {
109
- id: aggregateId,
110
- capName: parsed.capName,
111
- value: parsed.amount,
112
- periodStart: Temporal.Instant.from(parsed.periodStartIso),
113
- lastSoftWarnedAt: null,
114
- },
115
- ctx.user,
116
- db,
117
- );
118
- }
117
+ const aggregateId = capCounterAggregateId(user.tenantId, parsed.capName, parsed.periodStartIso);
119
118
 
120
- const currentRow = existing[0];
121
- if (!currentRow) {
122
- throw new Error("cap-counter.bookCapUsage: row vanished between length-check and read");
123
- }
124
- const currentValue = currentRow["value"] as number; // @cast-boundary db-row
125
- const currentVersion = currentRow["version"] as number; // @cast-boundary db-row
126
- options.guardCurrentValue?.(currentValue);
127
- return executor.update(
119
+ const existing = await db.selectMany(table, { id: aggregateId }, { limit: 1 });
120
+ if (existing.length === 0) {
121
+ options.guardCurrentValue?.(0);
122
+ if (direction === "subtract") return { isSuccess: true, data: {} };
123
+ return executor.create(
128
124
  {
129
125
  id: aggregateId,
130
- version: currentVersion,
131
- changes: {
132
- value:
133
- direction === "add"
134
- ? currentValue + parsed.amount
135
- : Math.max(0, currentValue - parsed.amount),
136
- },
126
+ capName: parsed.capName,
127
+ value: parsed.amount,
128
+ periodStart: Temporal.Instant.from(parsed.periodStartIso),
129
+ lastSoftWarnedAt: null,
137
130
  },
138
- ctx.user,
131
+ user,
139
132
  db,
140
133
  );
141
134
  }
142
135
 
136
+ const currentRow = existing[0];
137
+ if (!currentRow) {
138
+ throw new Error("cap-counter.bookCapUsage: row vanished between length-check and read");
139
+ }
140
+ const currentValue = currentRow["value"] as number; // @cast-boundary db-row
141
+ const currentVersion = currentRow["version"] as number; // @cast-boundary db-row
142
+ options.guardCurrentValue?.(currentValue);
143
+ return executor.update(
144
+ {
145
+ id: aggregateId,
146
+ version: currentVersion,
147
+ changes: {
148
+ value:
149
+ direction === "add"
150
+ ? currentValue + parsed.amount
151
+ : Math.max(0, currentValue - parsed.amount),
152
+ },
153
+ },
154
+ user,
155
+ db,
156
+ );
157
+ }
158
+
159
+ async function applyCapDelta(
160
+ ctx: HandlerContext,
161
+ options: BookCapUsageOptions,
162
+ direction: "add" | "subtract",
163
+ ): Promise<WriteResult> {
143
164
  if (options.outsideTransaction) {
144
165
  const outsideDb = requireOutsideTransactionDb(ctx);
145
- return retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb, attemptWrite));
166
+ return retryCounterWriteOnVersionConflict(() =>
167
+ runInOwnTransaction(outsideDb, (txDb) => applyCapDeltaOn(txDb, ctx.user, options, direction)),
168
+ );
146
169
  }
147
- return retryCounterWriteOnVersionConflict(() => attemptWrite(ctx.db));
170
+ return retryCounterWriteOnVersionConflict(() =>
171
+ applyCapDeltaOn(ctx.db, ctx.user, options, direction),
172
+ );
148
173
  }
149
174
 
150
175
  export type MarkCapSoftWarnedOptions = {
@@ -205,12 +230,11 @@ export type ReadRollingCapUsageOptions = {
205
230
  readonly windowDays: number;
206
231
  };
207
232
 
208
- // Sums usage without throwing — for callers that only need the raw number
209
- // (e.g. a CapSpec.usage callback), not the enforce-and-throw path.
210
- export async function readRollingCapUsage(
233
+ async function sumRollingEventAmounts(
211
234
  db: TenantDb,
212
235
  tenantId: TenantId,
213
236
  options: ReadRollingCapUsageOptions,
237
+ type: string,
214
238
  ): Promise<number> {
215
239
  const aggregateId = rollingCapAggregateId(tenantId, options.capName);
216
240
  const cutoff = Temporal.Now.instant().subtract({ hours: options.windowDays * 24 });
@@ -219,20 +243,37 @@ export async function readRollingCapUsage(
219
243
  tenantId,
220
244
  aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
221
245
  aggregateId,
222
- type: ROLLING_INCREMENTED_EVENT_QN,
246
+ type,
223
247
  createdAt: { gte: cutoff },
224
248
  });
225
249
 
226
- let value = 0;
250
+ let total = 0;
227
251
  for (const row of rows) {
228
252
  // @cast-boundary engine-payload — events.payload is jsonb (typed as
229
253
  // unknown by drizzle's $type<Record<string,unknown>>); narrowing the
230
254
  // shape here mirrors enforceRollingCap's own read-side contract.
231
255
  const payload = row["payload"] as { amount?: number };
232
256
  if (typeof payload.amount === "number") {
233
- value += payload.amount;
257
+ total += payload.amount;
234
258
  }
235
259
  }
260
+ return total;
261
+ }
236
262
 
237
- return value;
263
+ // Sums usage without throwing — for callers that only need the raw number
264
+ // (e.g. a CapSpec.usage callback), not the enforce-and-throw path.
265
+ // Incremented minus released amounts inside the window; a release whose increment already left the window cannot push it below 0.
266
+ export async function readRollingCapUsage(
267
+ db: TenantDb,
268
+ tenantId: TenantId,
269
+ options: ReadRollingCapUsageOptions,
270
+ ): Promise<number> {
271
+ const incremented = await sumRollingEventAmounts(
272
+ db,
273
+ tenantId,
274
+ options,
275
+ ROLLING_INCREMENTED_EVENT_QN,
276
+ );
277
+ const released = await sumRollingEventAmounts(db, tenantId, options, ROLLING_RELEASED_EVENT_QN);
278
+ return Math.max(0, incremented - released);
238
279
  }