@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.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +2 -2
- package/packages/bundled-features/src/audit/handlers/list.query.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/reset-token.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/unlock-token.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/verification-token.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm.write.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/constants.ts +3 -17
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +2 -9
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +3 -11
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +96 -55
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +247 -0
- package/packages/bundled-features/src/cap-counter/changes.json +28 -0
- package/packages/bundled-features/src/cap-counter/constants.ts +8 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +1 -9
- package/packages/bundled-features/src/cap-counter/feature.ts +7 -0
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +5 -0
- package/packages/bundled-features/src/cap-counter/index.ts +9 -0
- package/packages/bundled-features/src/cap-counter/payload-field.ts +10 -0
- package/packages/bundled-features/src/cap-counter/reservation-chain.ts +51 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +3 -0
- package/packages/bundled-features/src/cap-counter/tables.ts +53 -0
- package/packages/bundled-features/src/cap-counter/value-cap-guard.ts +70 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +63 -70
- package/packages/bundled-features/src/channel-discord/feature.ts +1 -0
- package/packages/bundled-features/src/channel-slack/feature.ts +1 -0
- package/packages/bundled-features/src/channel-teams/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/attempt-log-retention.ts +40 -0
- package/packages/bundled-features/src/delivery/attempt-log.ts +26 -3
- package/packages/bundled-features/src/delivery/changes.json +12 -0
- package/packages/bundled-features/src/delivery/constants.ts +1 -0
- package/packages/bundled-features/src/delivery/events.ts +3 -0
- package/packages/bundled-features/src/delivery/feature.ts +50 -2
- package/packages/bundled-features/src/delivery/handlers/log.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +6 -0
- package/packages/bundled-features/src/delivery/mask-recipient-address.ts +40 -0
- package/packages/bundled-features/src/delivery/public-names.ts +5 -0
- package/packages/bundled-features/src/delivery/types.ts +12 -2
- package/packages/bundled-features/src/derivatives-sharp/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/handlers/set.write.ts +1 -1
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +1 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +1 -1
- package/packages/bundled-features/src/form-draft/lookup.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +1 -1
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +1 -1
- package/packages/bundled-features/src/jobs/job-instant.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +3 -3
- package/packages/bundled-features/src/legal-pages/changes.json +6 -0
- package/packages/bundled-features/src/metrics/compute.ts +1 -1
- package/packages/bundled-features/src/metrics/feature.ts +1 -1
- package/packages/bundled-features/src/metrics/types.ts +1 -1
- package/packages/bundled-features/src/metrics/window.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/expiry.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +1 -1
- package/packages/bundled-features/src/renderer-foundation/types.ts +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +6 -0
- package/packages/bundled-features/src/renderer-simple/feature.ts +4 -6
- package/packages/bundled-features/src/renderer-simple/index.ts +1 -0
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +138 -0
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +33 -4
- package/packages/bundled-features/src/sessions/handlers/revoke-all-for-user.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke-all-others.write.ts +1 -1
- package/packages/bundled-features/src/sessions/handlers/revoke.write.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -1
- package/packages/bundled-features/src/shared/grace-period.ts +3 -10
- package/packages/bundled-features/src/shared/row-bound-grant.ts +1 -1
- package/packages/bundled-features/src/shared/signed-token.ts +1 -1
- package/packages/bundled-features/src/subscription-mollie/verify-webhook.ts +1 -1
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +1 -1
- package/packages/bundled-features/src/template-resolver/api.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +1 -1
- package/packages/bundled-features/src/template-resolver/table.ts +1 -1
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +1 -1
- package/packages/bundled-features/src/tenant/invited-membership.ts +1 -1
- package/packages/bundled-features/src/tenant-handover/grant.ts +1 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/index.ts +7 -0
- package/packages/bundled-features/src/tier-engine/tier-option-gate.ts +148 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/export-jobs.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +3 -9
- package/packages/bundled-features/src/user-data-rights/deletion-token.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/list-download-attempts.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/handlers/my-audit-log.query.ts +1 -1
- package/packages/bundled-features/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +36 -6
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +1 -1
- package/samples/apps/marketing-demo/package.json +1 -0
- package/samples/apps/marketing-demo/src/app/seed-data.ts +2 -2
- package/samples/apps/marketing-demo/src/app/seed.ts +1 -1
- package/samples/apps/styleguide/src/app/shell.tsx +1 -1
- package/samples/apps/styleguide/src/features/gallery/web/Gallery.tsx +9 -3
- package/samples/apps/ui-walkthrough/src/app/assistant-action.tsx +6 -5
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +3 -2
- package/samples/apps/ui-walkthrough/src/features/tasks/i18n.ts +8 -0
- package/samples/apps/use-all-bundled/package.json +1 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +1 -1
- package/samples/recipes/files-post-processing/package.json +1 -1
- 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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
12
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
13
13
|
|
|
14
14
|
export type AuthMailLocale = string;
|
|
15
15
|
|
package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Temporal } from "temporal
|
|
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
|
|
|
@@ -55,10 +55,7 @@ import {
|
|
|
55
55
|
EXT_TENANT_DATA,
|
|
56
56
|
type FeatureDefinition,
|
|
57
57
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
58
|
-
|
|
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
|
-
|
|
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
|
}),
|
package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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(() =>
|
|
166
|
+
return retryCounterWriteOnVersionConflict(() =>
|
|
167
|
+
runInOwnTransaction(outsideDb, (txDb) => applyCapDeltaOn(txDb, ctx.user, options, direction)),
|
|
168
|
+
);
|
|
146
169
|
}
|
|
147
|
-
return retryCounterWriteOnVersionConflict(() =>
|
|
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
|
-
|
|
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
|
|
246
|
+
type,
|
|
223
247
|
createdAt: { gte: cutoff },
|
|
224
248
|
});
|
|
225
249
|
|
|
226
|
-
let
|
|
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
|
-
|
|
257
|
+
total += payload.amount;
|
|
234
258
|
}
|
|
235
259
|
}
|
|
260
|
+
return total;
|
|
261
|
+
}
|
|
236
262
|
|
|
237
|
-
|
|
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
|
}
|